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.