206

Is there a simple way, in a pretty standard UNIX environment with bash, to run a command to delete all but the most recent X files from a directory?

To give a bit more of a concrete example, imagine some cron job writing out a file (say, a log file or a tar-ed up backup) to a directory every hour. I'd like a way to have another cron job running which would remove the oldest files in that directory until there are less than, say, 5.

And just to be clear, there's only one file present, it should never be deleted.

Matt Sheppard
  • 116,545
  • 46
  • 111
  • 131

20 Answers20

192

The problems with the existing answers:

  • inability to handle filenames with embedded spaces or newlines.
    • in the case of solutions that invoke rm directly on an unquoted command substitution (rm `...`), there's an added risk of unintended globbing.
  • inability to distinguish between files and directories (i.e., if directories happened to be among the 5 most recently modified filesystem items, you'd effectively retain fewer than 5 files, and applying rm to directories will fail).

wnoise's answer addresses these issues, but the solution is GNU-specific (and quite complex).

Here's a pragmatic, POSIX-compliant solution that comes with only one caveat: it cannot handle filenames with embedded newlines - but I don't consider that a real-world concern for most people.

For the record, here's the explanation for why it's generally not a good idea to parse ls output: http://mywiki.wooledge.org/ParsingLs

ls -tp | grep -v '/$' | tail -n +6 | xargs -I {} rm -- {}

Note: This command operates in the current directory; to target a directory explicitly, use a subshell ((...)) with cd:
(cd /path/to && ls -tp | grep -v '/$' | tail -n +6 | xargs -I {} rm -- {})
The same applies analogously to the commands below.

The above is inefficient, because xargs has to invoke rm separately for each filename.
However, your platform's specific xargs implementation may allow you to solve this problem:


A solution that works with GNU xargs is to use -d '\n', which makes xargs consider each input line a separate argument, yet passes as many arguments as will fit on a command line at once:

ls -tp | grep -v '/$' | tail -n +6 | xargs -d '\n' -r rm --

Note: Option -r (--no-run-if-empty) ensures that rm is not invoked if there's no input.

A solution that works with both GNU xargs and BSD xargs (including on macOS) - though technically still not POSIX-compliant - is to use -0 to handle NUL-separated input, after first translating newlines to NUL (0x0) chars., which also passes (typically) all filenames at once:

ls -tp | grep -v '/$' | tail -n +6 | tr '\n' '\0' | xargs -0 rm --

Explanation:

  • ls -tp prints the names of filesystem items sorted by how recently they were modified , in descending order (most recently modified items first) (-t), with directories printed with a trailing / to mark them as such (-p).

    • Note: It is the fact that ls -tp always outputs file / directory names only, not full paths, that necessitates the subshell approach mentioned above for targeting a directory other than the current one ((cd /path/to && ls -tp ...)).
  • grep -v '/$' then weeds out directories from the resulting listing, by omitting (-v) lines that have a trailing / (/$).

    • Caveat: Since a symlink that points to a directory is technically not itself a directory, such symlinks will not be excluded.
  • tail -n +6 skips the first 5 entries in the listing, in effect returning all but the 5 most recently modified files, if any.
    Note that in order to exclude N files, N+1 must be passed to tail -n +.

  • xargs -I {} rm -- {} (and its variations) then invokes on rm on all these files; if there are no matches at all, xargs won't do anything.

    • xargs -I {} rm -- {} defines placeholder {} that represents each input line as a whole, so rm is then invoked once for each input line, but with filenames with embedded spaces handled correctly.
    • -- in all cases ensures that any filenames that happen to start with - aren't mistaken for options by rm.

A variation on the original problem, in case the matching files need to be processed individually or collected in a shell array:

# One by one, in a shell loop (POSIX-compliant):
ls -tp | grep -v '/$' | tail -n +6 | while IFS= read -r f; do echo "$f"; done

# One by one, but using a Bash process substitution (<(...), 
# so that the variables inside the `while` loop remain in scope:
while IFS= read -r f; do echo "$f"; done < <(ls -tp | grep -v '/$' | tail -n +6)

# Collecting the matches in a Bash *array*:
IFS=$'\n' read -d '' -ra files  < <(ls -tp | grep -v '/$' | tail -n +6)
printf '%s\n' "${files[@]}" # print array elements
mklement0
  • 382,024
  • 64
  • 607
  • 775
  • 2
    Certainly better than most other answers here, so I'm happy to lend my support, even inasmuch as I consider ignoring the newline case to be a thing to be done only with caution. – Charles Duffy Jan 18 '16 at 20:13
  • 2
    If you do `ls` not in the current directory, then the paths to files will contain '/', which means that `grep -v '/'` won't match anything. I believe `grep -v '/$'` is what you want to only exclude directories. – waldol1 Feb 29 '16 at 12:33
  • 1
    @waldol1: Thanks; I've updated the answer to include your suggestion, which also makes the `grep` command conceptually clearer. Note, however, that the problem you describe would _not_ have surfaced with a single directory path; e.g., `ls -p /private/var` would still only print mere filenames. Only if you passed _multiple_ file arguments (typically via a glob) would you see actual paths in the output; e.g., `ls -p /private/var/*` (and you'd also see the contents of matching subdirectories, unless you also included `-d`). – mklement0 Feb 29 '16 at 13:41
  • 1
    These commands work on files in the current directory. I wanted to run the above BSD command on files in another directory... /mnt/usb/openwrt. I adapted `ls -tp | grep -v '/$' | tail -n +6 | tr '\n' '\0' | xargs -0 rm --` to this-> `ls -tp /mnt/usb/openwrt | grep -v '/$' | tail -n +6 | sed 's|^|/mnt/usb/openwrt/|' | tr '\n' '\0' | xargs -0 rm --` – FlexMcMurphy Jan 04 '21 at 23:16
  • 1
    @FlexMcMurphy, it occurred to me that using a subshell (`(...)`) with `cd` is simpler and more robust: `(cd /mnt/usb/openwrt && ls -tp | grep -v '/$' | tail -n +6 | tr '\n' '\0' | xargs -0 rm --)` - I've updated the answer accordingly. – mklement0 Jan 06 '21 at 13:22
  • Very nice thanks for sharing and for the detailed explainations – tchartron Feb 28 '23 at 09:13
  • I'm glad to hear the answer is helpful, @tchartron, and thanks for the nice feedback. – mklement0 Feb 28 '23 at 13:42
122

Remove all but 5 (or whatever number) of the most recent files in a directory.

rm `ls -t | awk 'NR>5'`
Espo
  • 41,399
  • 21
  • 132
  • 159
  • 2
    I needed this to only consider my archive files. change `ls -t` to `ls -td *.bz2` – James T Snell Feb 06 '14 at 20:37
  • 3
    I used this for directories by changing it to rm -rf `ls -t | awk 'NR>1'` (I only wanted the most recent). Thanks! – lohiaguitar91 Jul 09 '14 at 18:07
  • 16
    `ls -t | awk 'NR>5' | xargs rm -f` if you prefer pipes and you need to suppress the error if there is nothing to be deleted. – H2ONaCl Jul 30 '14 at 07:58
  • Strangle enough this did not work for me. I had to use @H2ONaCl's xargs version – Ashutosh Jindal Nov 04 '15 at 20:02
  • 23
    Concise and readable, perhaps, but dangerous to use; if trying to delete a file created with `touch 'hello * world'`, this would delete **absolutely everything in the current directory**. – Charles Duffy Jan 18 '16 at 20:16
  • 1
    Even though this was answered in 2008 it works like a charm and just what I needed to simply delete old backups from a specific directory. Awesome. – Rens Tillmann May 29 '17 at 15:28
  • I wanted to give an explicit path here. – Swapnil Mhaske May 29 '20 at 13:39
  • 7
    ***WARNING*** Please make sure you run this from the directory that files are going to be deleted from! I stupidly ran this from a working code directory of 100 files or so and it zapped the f*&$*ing lot!! Fortunately I had just taken a backup 30 mins before (Phew!) (You know that sinking feeling you get when your heart stops and you cant find the files in the trash bin) – joe_evans Jul 20 '20 at 22:24
89
(ls -t|head -n 5;ls)|sort|uniq -u|xargs rm

This version supports names with spaces:

(ls -t|head -n 5;ls)|sort|uniq -u|sed -e 's,.*,"&",g'|xargs rm
Steve Bennett
  • 114,604
  • 39
  • 168
  • 219
thelsdj
  • 9,034
  • 10
  • 45
  • 58
  • 20
    This command will not correctly handle files with spaces in the names. – tylerl Apr 13 '10 at 20:33
  • To fix above use: `(ls -t|head -n 5;ls)|sort|uniq -u|sed -e 's,.*,"&",g'|xargs rm` – BroiSatse Feb 05 '14 at 14:24
  • 1
    This one fails if there are no files to delete. – Mantas Jun 03 '14 at 14:36
  • @Mantas I think that can be solved using `rm -f` (I have only tried `-rf` though, dunno if it works without -r). – Andreas Hultgren Jul 19 '14 at 12:55
  • 5
    `(ls -t|head -n 5;ls)` is a [command group](http://tldp.org/LDP/abs/html/special-chars.html#PARENSREF). It prints the 5 most recent files twice. `sort` puts identical lines together. `uniq -u` removes duplicates, so that all but the 5 most recent files remains. `xargs rm` calls `rm` on each of them. – Fabien Nov 13 '14 at 14:24
  • `(ls -t|head -n 5;ls)|sort|uniq -u|sed -e 's,.*,"&",g'|xargs rm -rf` works for: 1) deleting directories 2) avoiding "rm: missing operand" when there is nothing to delete – Paul Schwarz Feb 09 '15 at 13:00
  • Doesn't work for files like "-000-" `(ls -t|head -n 100;ls)|sort|uniq -u | xargs printf './%s\n' | xargs rm` works in my case. – lqdc Mar 17 '15 at 03:46
  • @Matas is correct. it doesn't work if you have less then 5 files in folder, be careful – Andrey Apr 18 '15 at 21:06
  • 15
    This deletes all your files if you have 5 or less! Add `--no-run-if-empty` to `xargs` as in `(ls -t|head -n 5;ls)|sort|uniq -u|xargs --no-run-if-empty rm` please update the answer. – Gonfi den Tschal Apr 27 '15 at 21:29
  • 1
    Echo Gonfi, this removes the oldest 5 files. That is NOT even remotely the same thing as keeping the newest 5 files. – Canuteson Dec 04 '15 at 21:54
  • 3
    Even the one that "supports names with spaces" is dangerous. Consider a name that contains literal quotes: `touch 'foo " bar'` will throw off the whole rest of the command. – Charles Duffy Jan 18 '16 at 16:55
  • 2
    ...it's safer to use `xargs -d $'\n'` than to inject quotes into your content, though NUL-delimiting the input stream (which requires using something other than `ls` to *really* do right) is the ideal option. – Charles Duffy Jan 18 '16 at 17:00
  • Why not `ls -t | tail -n +6` instead of `(ls -t|head -n 5;ls)|sort|uniq -u`? – jayhendren Nov 06 '17 at 21:38
71

Simpler variant of thelsdj's answer:

ls -tr | head -n -5 | xargs --no-run-if-empty rm 

ls -tr displays all the files, oldest first (-t newest first, -r reverse).

head -n -5 displays all but the 5 last lines (ie the 5 newest files).

xargs rm calls rm for each selected file.

ArgonQQ
  • 1,917
  • 1
  • 11
  • 12
Fabien
  • 6,700
  • 7
  • 35
  • 35
  • 18
    Need to add --no-run-if-empty to xargs so that it doesn't fail when there are fewer than 5 files. – Tom May 07 '14 at 18:31
  • ls -1tr | head -n -5 | xargs rm <---------- you need to add a -1 to the ls or you won't get a list output for head to properly work against – Al Joslin Sep 15 '15 at 21:02
  • 4
    @AlJoslin, `-1` is default when output is to a pipeline, so it isn't mandatory here. This has much larger issues, related to default behavior of `xargs` when parsing names with spaces, quotes, &c. – Charles Duffy Jan 18 '16 at 20:18
  • seems that the `--no-run-if-empty` isn't recognized in my shell. I'm using Cmder on windows. – StayFoolish Sep 06 '18 at 03:39
  • Might need to use the `-0` option if filenames could contain whitespaces. Haven't tested it yet though. [source](https://stackoverflow.com/q/16758525/3006854) – ki9 Nov 12 '18 at 15:00
19
find . -maxdepth 1 -type f -printf '%T@ %p\0' | sort -r -z -n | awk 'BEGIN { RS="\0"; ORS="\0"; FS="" } NR > 5 { sub("^[0-9]*(.[0-9]*)? ", ""); print }' | xargs -0 rm -f

Requires GNU find for -printf, and GNU sort for -z, and GNU awk for "\0", and GNU xargs for -0, but handles files with embedded newlines or spaces.

wnoise
  • 9,764
  • 37
  • 47
  • 2
    If you want to remove directories, just change the -f to a -d and add a -r to the rm. find . -maxdepth 1 -type d -printf '%T@ %p\0' | sort -r -z -n | awk 'BEGIN { RS="\0"; ORS="\0"; FS="" } NR > 5 { sub("^[0-9]*(.[0-9]*)? ", ""); print }' | xargs -0 rm -rf – alex Jan 10 '11 at 19:19
  • 1
    At a glance, I'm surprised at the complexity (or, for that matter, necessity) of the `awk` logic. Am I missing some requirements inside the OP's question that make it necessary? – Charles Duffy Jan 18 '16 at 20:25
  • @Charles Duffy: The sub() removes the timestamp, which is what is sorted on. The timestamp produced by "%T@" may include a fraction part. Splitting on space with FS breaks paths with embedded spaces. I suppose removing up through first space works, but is nearly as difficult to read. The RS and ORS separators can't be set on the command line, because they are NULs. – wnoise Jan 19 '16 at 21:07
  • 1
    @wnoise, my usual approach to this is to pipe into a shell `while read -r -d ' '; IFS= -r -d ''; do ...` loop -- the first read terminates on the space, while the second goes on to the NUL. – Charles Duffy Jan 19 '16 at 21:08
  • @Charles Duffy: I'm always leery of raw shell, perhaps due to byzantine quoting concerns. I now think GNU `sed -z -e 's/[^ ]* //; 1,5d'` is the clearest. (or perhaps `sed -n -z -e 's/[^ ]* //; 6,$p'`. – wnoise Jan 20 '16 at 02:09
  • I'd argue that the quoting isn't so bad once one understands the execution model, but will grant that having previously made that investment it's hard for me to gauge what it would look like were it not in the mists of the past. – Charles Duffy Jan 20 '16 at 03:23
16

All these answers fail when there are directories in the current directory. Here's something that works:

find . -maxdepth 1 -type f | xargs -x ls -t | awk 'NR>5' | xargs -L1 rm

This:

  1. works when there are directories in the current directory

  2. tries to remove each file even if the previous one couldn't be removed (due to permissions, etc.)

  3. fails safe when the number of files in the current directory is excessive and xargs would normally screw you over (the -x)

  4. doesn't cater for spaces in filenames (perhaps you're using the wrong OS?)

Charles Wood
  • 864
  • 8
  • 23
  • 6
    What happens if `find` returns more filenames than can be passed on a single command line to `ls -t`? (Hint: You get multiple runs of `ls -t`, each of which is only individually sorted, rather than having a globally-correct sort order; thus, this answer is badly broken when running with sufficiently large directories). – Charles Duffy Jan 18 '16 at 20:14
13
ls -tQ | tail -n+4 | xargs rm

List filenames by modification time, quoting each filename. Exclude first 3 (3 most recent). Remove remaining.

EDIT after helpful comment from mklement0 (thanks!): corrected -n+3 argument, and note this will not work as expected if filenames contain newlines and/or the directory contains subdirectories.

Mark
  • 6,731
  • 1
  • 40
  • 38
  • The ``-Q`` option doesn't seem to exist on my machine. – Pierre-Adrien Feb 13 '14 at 10:59
  • 4
    Hmm, the option has been in GNU core utils for ~20 years, but is not mentioned in BSD variants. Are you on a mac? – Mark Feb 14 '14 at 02:59
  • I am indeed. Didn't think there was differences for this kind of really basic commands between up-to-date systems. Thanks for your answer ! – Pierre-Adrien Feb 14 '14 at 15:10
  • 3
    @Mark: ++ for `-Q`. Yes, `-Q` is a GNU extension (here's the [POSIX `ls` spec](http://pubs.opengroup.org/onlinepubs/9699919799/utilities/ls.html)). A small caveat (rarely a problem in practice): `-Q` encodes embedded _newlines_ in filenames as literal `\n`, which `rm` won't recognize. To exclude the first _3_, the `xargs` argument must `+4`. Finally, a caveat that applies to most other answers too: your command will only work as intended if there are no _subdirectories_ in the current dir. – mklement0 Jan 18 '16 at 20:09
  • How can I run this between `then` and `fi` in the same line – juliangonzalez Feb 29 '16 at 20:07
  • 1
    When there is nothing to remove, you have call xargs with `--no-run-if-empty` option : `ls -tQ | tail -n+4 | xargs --no-run-if-empty rm` – Olivier Lecrivain Apr 30 '16 at 14:53
8

For Linux (GNU tools), an efficient & robust way to keep the n newest files in the current directory while removing the rest:

n=5

find . -maxdepth 1 -type f -printf '%T@ %p\0' |
sort -z -nrt ' ' -k1,1 |
sed -z -e "1,${n}d" -e 's/[^ ]* //' |
xargs -0r rm -f --

For BSD, find doesn't have the -printf predicate, stat can't output NULL bytes, and sed + awk can't handle NULL-delimited records.

Here's a solution that doesn't support newlines in paths but that safeguards against them by filtering them out:

#!/bin/bash
n=5

find . -maxdepth 1 -type f ! -path $'*\n*' -exec stat -f '%.9Fm %N' {} + |
sort -nrt ' ' -k1,1 |
awk -v n="$n" -F'^[^ ]* ' 'NR > n {printf "%s%c", $2, 0}' |
xargs -0 rm -f --

note: I'm using bash because of the $'\n' notation. For sh you can define a variable containing a literal newline and use it instead.


POSIX solution (inspired from @mklement0 answer).

This one adds the correct escaping for POSIX xargs, but it would still break when a file or directory contains a linefeed in the name; if you want to handle that then there's no other choice than purging or renaming those files.

n=5

ls -tp . |
grep -v '/$' |
head -n +"$((n+1))" |
sed -e 's/"/"\\""/g' -e 's/.*/"&"/' |
xargs rm --

remark: In fact you can replace the grep | head | sed with awk -v n="$n" '/[^/]$/ && --n < 0 {gsub(/"/, "\"\\\\\"\""); print "\"" $0 "\""}'


Solution for UNIX & Linux (inspired from AIX/HP-UX/SunOS/BSD/Linux ls -b):

Some platforms don't provide find -printf, nor stat, nor support NUL-delimited records with stat/sort/awk/sed/xargs. That's why using perl is probably the most portable way to tackle the problem, because it is available by default in almost every OS.

I could have written the whole thing in perl but I didn't. I only use it for substituting stat and for encoding-decoding-escaping the filenames. The core logic is the same as the previous solutions and is implemented with POSIX tools.

note: perl's default stat has a resolution of a second, but starting from perl-5.8.9 you can get sub-second resolution with the stat function of the module Time::HiRes (when both the OS and the filesystem support it). That's what I'm using here; if your perl doesn't provide it then you can remove the ‑MTime::HiRes=stat from the command line.

n=5

find . '(' -name '.' -o -prune ')' -type f -exec \
perl -MTime::HiRes=stat -le '
    foreach (@ARGV) {
        @st = stat($_);
        if ( @st > 0 ) {
            s/([\\\n])/sprintf( "\\%03o", ord($1) )/ge;
            print sprintf( "%.9f %s", $st[9], $_ );
        }
        else { print STDERR "stat: $_: $!"; }
    }
' {} + |

sort -nrt ' ' -k1,1 |

sed -e "1,${n}d" -e 's/[^ ]* //' |

perl -l -ne '
    s/\\([0-7]{3})/chr(oct($1))/ge;
    s/(["\n])/"\\$1"/g;
    print "\"$_\""; 
' |

xargs -E '' sh -c '[ "$#" -gt 0 ] && rm -f -- "$@"' sh

Explanations:

  • For each file found, the first perl gets the modification time and outputs it along the encoded filename (each newline and backslash characters are replaced with the literals \012 and \134 respectively).

  • Now each time filename is guaranteed to be single-line, so POSIX sort and sed can safely work with this stream.

  • The second perl decodes the filenames and escapes them for POSIX xargs.

  • Lastly, xargs calls rm for deleting the files. The sh command is a trick that prevents xargs from running rm when there's no files to delete.

Fravadona
  • 13,917
  • 1
  • 23
  • 35
  • 1
    ++ed for the much more robust solution than the accepted answer (posted in good old days) with a modern practical style. – tshiono Sep 06 '22 at 08:14
  • 1
    @tshiono, the only thing the [previously accepted answer](https://stackoverflow.com/a/34862475/45375) doesn't handle correctly (as stated) is _newlines in filenames_, which is hardly a real-world problem. On the plus side, it is much more concise and conceptually simpler. – mklement0 Sep 06 '22 at 20:33
  • 2
    @mklement0, ...hardly a _common_ real-world problem, maybe, but uncommon real-world problems are still real-world problems. When I'm wearing my red-team hat, creating deliberately unhandled conditions is something attackers _do_; when I'm wearing my defensive-coding hat, having random binary garbage dumped into a filename is something I've seen in my career (due to a 100% unintentional bug), with catastrophic results when that garbage was poorly-handled. – Charles Duffy Jan 23 '23 at 17:19
8

Ignoring newlines is ignoring security and good coding. wnoise had the only good answer. Here is a variation on his that puts the filenames in an array $x

while IFS= read -rd ''; do 
    x+=("${REPLY#* }"); 
done < <(find . -maxdepth 1 -printf '%T@ %p\0' | sort -r -z -n )
Ian Kelling
  • 9,643
  • 9
  • 35
  • 39
5

I realize this is an old thread, but maybe someone will benefit from this. This command will find files in the current directory :

for F in $(find . -maxdepth 1 -type f -name "*_srv_logs_*.tar.gz" -printf '%T@ %p\n' | sort -r -z -n | tail -n+5 | awk '{ print $2; }'); do rm $F; done

This is a little more robust than some of the previous answers as it allows to limit your search domain to files matching expressions. First, find files matching whatever conditions you want. Print those files with the timestamps next to them.

find . -maxdepth 1 -type f -name "*_srv_logs_*.tar.gz" -printf '%T@ %p\n'

Next, sort them by the timestamps:

sort -r -z -n

Then, knock off the 4 most recent files from the list:

tail -n+5

Grab the 2nd column (the filename, not the timestamp):

awk '{ print $2; }'

And then wrap that whole thing up into a for statement:

for F in $(); do rm $F; done

This may be a more verbose command, but I had much better luck being able to target conditional files and execute more complex commands against them.

TopherGopher
  • 655
  • 11
  • 21
4

If the filenames don't have spaces, this will work:

ls -C1 -t| awk 'NR>5'|xargs rm

If the filenames do have spaces, something like

ls -C1 -t | awk 'NR>5' | sed -e "s/^/rm '/" -e "s/$/'/" | sh

Basic logic:

  • get a listing of the files in time order, one column
  • get all but the first 5 (n=5 for this example)
  • first version: send those to rm
  • second version: gen a script that will remove them properly
Eugene Yarmash
  • 142,882
  • 41
  • 325
  • 378
Mark Harrison
  • 297,451
  • 125
  • 333
  • 465
  • Don't forget the `while read` trick for dealing with spaces: `ls -C1 -t | awk 'NR>5' | while read d ; do rm -rvf "$d" ; done` – pinkeen Nov 17 '14 at 13:00
  • 1
    @pinkeen, not quite safe as given there. `while IFS= read -r d` would be a bit better -- the `-r` prevents backslash literals from being consumed by `read`, and the `IFS=` prevents automatic trimming of trailing whitespace. – Charles Duffy Jan 18 '16 at 20:22
  • 4
    BTW, if one is worried about hostile filenames, this is an **extremely** dangerous approach. Consider a file created with `touch $'hello \'$(rm -rf ~)\' world'`; the literal quotes inside the filename would counter the literal quotes you're adding with `sed`, resulting in the code within the filename being executed. – Charles Duffy Jan 18 '16 at 20:23
  • 1
    (to be clear, the "this" above was referring to the `| sh` form, which is the one with the shell injection vulnerability). – Charles Duffy Jan 19 '16 at 21:13
3

With zsh

Assuming you don't care about present directories and you will not have more than 999 files (choose a bigger number if you want, or create a while loop).

[ 6 -le `ls *(.)|wc -l` ] && rm *(.om[6,999])

In *(.om[6,999]), the . means files, the o means sort order up, the m means by date of modification (put a for access time or c for inode change), the [6,999] chooses a range of file, so doesn't rm the 5 first.

lolesque
  • 10,693
  • 5
  • 42
  • 42
  • Intriguing, but for the life of me I couldn't get the sorting glob qualifier (`om`) to work (any sorting I've tried showed no effect - neither on OSX 10.11.2 (tried with zsh 5.0.8 and 5.1.1), nor on Ubuntu 14.04 (zsh 5.0.2)) - what am I missing?. As for the range endpoint: no need to hard-code it, simply use `-1` to refer to the last entry and thus include all remaining files: `[6,-1]`. – mklement0 Jan 19 '16 at 05:42
2

Adaptation of @mklement0's excellent answer with some parameters and without needing to navigate to the folder containing the files to be deleted...

TARGET_FOLDER="/my/folder/path"
FILES_KEEP=5
ls -tp "$TARGET_FOLDER"**/* | grep -v '/$' | tail -n +$((FILES_KEEP+1)) | xargs -d '\n' -r rm --

[Ref(s).: https://stackoverflow.com/a/3572628/3223785 ]

Thanks!

mklement0
  • 382,024
  • 64
  • 607
  • 775
Eduardo Lucio
  • 1,771
  • 2
  • 25
  • 43
1

found interesting cmd in Sed-Onliners - Delete last 3 lines - fnd it perfect for another way to skin the cat (okay not) but idea:

 #!/bin/bash
 # sed cmd chng #2 to value file wish to retain

 cd /opt/depot 

 ls -1 MyMintFiles*.zip > BigList
 sed -n -e :a -e '1,2!{P;N;D;};N;ba' BigList > DeList

 for i in `cat DeList` 
 do 
 echo "Deleted $i" 
 rm -f $i  
 #echo "File(s) gonzo " 
 #read junk 
 done 
 exit 0
tim
  • 11
  • 2
1

Removes all but the 10 latest (most recents) files

ls -t1 | head -n $(echo $(ls -1 | wc -l) - 10 | bc) | xargs rm

If less than 10 files no file is removed and you will have : error head: illegal line count -- 0

To count files with bash

fabrice
  • 11
  • 1
1

I needed an elegant solution for the busybox (router), all xargs or array solutions were useless to me - no such command available there. find and mtime is not the proper answer as we are talking about 10 items and not necessarily 10 days. Espo's answer was the shortest and cleanest and likely the most unversal one.

Error with spaces and when no files are to be deleted are both simply solved the standard way:

rm "$(ls -td *.tar | awk 'NR>7')" 2>&-

Bit more educational version: We can do it all if we use awk differently. Normally, I use this method to pass (return) variables from the awk to the sh. As we read all the time that can not be done, I beg to differ: here is the method.

Example for .tar files with no problem regarding the spaces in the filename. To test, replace "rm" with the "ls".

eval $(ls -td *.tar | awk 'NR>7 { print "rm \"" $0 "\""}')

Explanation:

ls -td *.tar lists all .tar files sorted by the time. To apply to all the files in the current folder, remove the "d *.tar" part

awk 'NR>7... skips the first 7 lines

print "rm \"" $0 "\"" constructs a line: rm "file name"

eval executes it

Since we are using rm, I would not use the above command in a script! Wiser usage is:

(cd /FolderToDeleteWithin && eval $(ls -td *.tar | awk 'NR>7 { print "rm \"" $0 "\""}'))

In the case of using ls -t command will not do any harm on such silly examples as: touch 'foo " bar' and touch 'hello * world'. Not that we ever create files with such names in real life!

Sidenote. If we wanted to pass a variable to the sh this way, we would simply modify the print (simple form, no spaces tolerated):

print "VarName="$1

to set the variable VarName to the value of $1. Multiple variables can be created in one go. This VarName becomes a normal sh variable and can be normally used in a script or shell afterwards. So, to create variables with awk and give them back to the shell:

eval $(ls -td *.tar | awk 'NR>7 { print "VarName=\""$1"\""  }'); echo "$VarName"
Pila
  • 125
  • 1
  • 5
0
leaveCount=5
fileCount=$(ls -1 *.log | wc -l)
tailCount=$((fileCount - leaveCount))

# avoid negative tail argument
[[ $tailCount < 0 ]] && tailCount=0

ls -t *.log | tail -$tailCount | xargs rm -f
  • 2
    `xargs` without `-0` or at bare minimum `-d $'\n'` is unreliable; observe how this behaves with a file with spaces or quote characters in its name. – Charles Duffy Jan 18 '16 at 20:18
0

I made this into a bash shell script. Usage: keep NUM DIR where NUM is the number of files to keep and DIR is the directory to scrub.

#!/bin/bash
# Keep last N files by date.
# Usage: keep NUMBER DIRECTORY
echo ""
if [ $# -lt 2 ]; then
    echo "Usage: $0 NUMFILES DIR"
    echo "Keep last N newest files."
    exit 1
fi
if [ ! -e $2 ]; then
    echo "ERROR: directory '$1' does not exist"
    exit 1
fi
if [ ! -d $2 ]; then
    echo "ERROR: '$1' is not a directory"
    exit 1
fi
pushd $2 > /dev/null
ls -tp | grep -v '/' | tail -n +"$1" | xargs -I {} rm -- {}
popd > /dev/null
echo "Done. Kept $1 most recent files in $2."
ls $2|wc -l
Bulrush
  • 548
  • 2
  • 4
  • 19
0

Modified version of the answer of @Fabien if you want to specify a path. Useful if you're running the script elsewhere.

ls -tr /path/foo/ | head -n -5 | xargs -I% --no-run-if-empty rm /path/foo/%

IMB
  • 15,163
  • 19
  • 82
  • 140
0

Since SC2010 – ShellCheck Wiki warns parsing ls ouput is bad, I propose a simple one-liner with stat without cryptic awk/sed scripting:

stat -c '%y %n' /path/* | sort -r | tail -n +2 | cut -d' ' -f4 | xargs -r rm -v --

tail's +2 parameter controls how many of the newest files are left behind i.e. not deleted. For an input value of N you are left with N-1 of the newest files.

synner
  • 31
  • 3
  • Note that `stat` isn't portable, and your specific command fails on macOS, for instance. When it does work, it is subject to the same - largely hypothetical - limitation as parsing `ls` output: lack of support for filenames with embedded newlines. That said, if there are cases where that is truly a concern, you could do something like `stat --printf='%y %n\0'`, but that too wouldn't be portable. Also note that `-f4` should be `-f4-` in order to support filenames with spaces. – mklement0 Apr 05 '23 at 17:00