0

The directory contains files with different extensions. I would like to delete all files with extensions .gz, zx, ext4 except the last 5 files. I can list all those files with

ls -l | grep '\.gz\|xz\|ext4$'

and I could just show the oldest 5 of them:

ls -l | grep '\.gz\|xz\|ext4$' | head -5

Or list the last 5 files:

ls -l | grep '\.gz\|xz\|ext4$' | tail -5

But I don't know how delete all of them except these 5 files that are listed in the above command.

PHA
  • 113
  • What do you mean by "last 5 files"? What do you mean by "oldest"? ls -l doesn't sort files by modification time nor by creation time by default. – Mekeor Melire Apr 04 '17 at 13:09

5 Answers5

3

If the usual sort order is ok (i.e. your files are named by the date), and your shell supports arrays, you can use them for this, avoiding the usual caveats about using ls:

F=(./*.{gz,xz,ext4})          # list files
G=("${F[@]:5}")               # pick all but the first five to another array
G=("${F[@]:0:${#F[@]}-5}")    # or pick all but the last five
rm -- "${G[@]}"               # remove them

zsh can sort by date in itself, so this would grab the matching files to an array, sorted by modification time

F=(./*.(gz|xz|ext4)(Om))

((Om) for newest last, (om) for newest first.) Process the array as above.


Of course, if you need to sort by modification date, can't use zsh and know your filenames are nice, a simple ls -t | tail -n +6 would do. (tail -n +6 starts printing at the sixth line, i.e. skips the first five. I had an off-by-one at this at first.)

With GNU ls, I think we could do ask it to quote the filenames to create suitable input for the shell, and then fill the array with the output:

eval "F=($(ls -t --quoting-style=shell-always ./*.{xz,gz}))"

But that requires trusting that ls and the shell agree on how quotes are interpreted.

ilkkachu
  • 138,973
  • Even better with zsh (without the assign to array step): http://unix.stackexchange.com/a/160228/170373 – ilkkachu Apr 05 '17 at 13:10
1
ls -1 | grep '\.gz$\|xz$\|ext4$' | tail -n +6 | xargs rm

Caveats: Your files should not have spaces/quotes.

set -f; set ./*.[gx]z ./*.ext4; [ "$#" -gt 5] && shift 5; rm -f "$@"

The below will work any sort of filenames:

td="`mktemp`" \
   find . -maxdepth 1 -type f \( -name \*.[gx]z -o -name \*.ext4 \) -exec sh -c '
      [ ! -s "$td" ] && [ "$#" -gt 5 ] && { shift 5; echo >> "$td"; }
      rm -f "$@"
   ' x {} +
rm "$td"
1

You are nearly there.

Use the long option format to specify the number of lines required, and then negate it. (The opposite logic can be used with tail).

head --lines=3 prints from head three lines

head --lines=-3 prints from head except the last three

Here's an example using a head and an eight line file named f

$ cat f
line 1
line 2
line 3
line 4
line 5
line 6
line 7
line 8
$ head -3 f
line 1
line 2
line 3
$ head --lines=3 f
line 1
line 2
line 3
$ head --lines=-3 f
line 1
line 2
line 3
line 4
line 5

Check this part produces what you want before you start deleting files.

The long option isn't strictly required, but it doesn't have the legacy of having to be backward compatible with the evolution of the tail and head commands and I find it easier to remember.

head -n-3 is equivalent with the long option head --lines=-3 f

See below

$ head -3 f #originally
line 1
line 2
line 3
$ head -n3 f #
line 1
line 2
line 3
$ head -n+3 f
line 1
line 2
line 3
$ head -n-3 f
line 1
line 2
line 3
line 4
line 5
$
X Tian
  • 10,463
1

In bash:

ls -1 *.{gz,xz,ext4} | head -n-5 | xargs -r rm

The key insight is, that head -n-5 will print all lines ENDING BEFORE the 5th from the end (i.e. all lines except the last 5). I believe you understand the rest.

The dual for tail would be tail -n+6 which prints all lines STARTING FROM the 6th (i.e. all lines except the first 5).

Robin479
  • 335
0

The following command will list all but the last 5 files:

ls -l | grep '\.gz\|xz\|ext4$' | head -n `expr \`ls | grep '\.gz\|xz\|ext4$' | wc -l\` - 4`

You should be able to convert it into command to delete files easily.