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 - perlgets the modification time and outputs it along the encoded filename (each- newlineand- backslashcharacters are replaced with the literals- \012and- \134respectively).
 
- Now each - time filenameis guaranteed to be single-line, so POSIX- sortand- sedcan safely work with this stream.
 
- The second - perldecodes the filenames and escapes them for POSIX- xargs.
 
- Lastly, - xargscalls- rmfor deleting the files. The- shcommand is a trick that prevents- xargsfrom running- rmwhen there's no files to delete.