60

I am new to shell scripting, so I need some help here. I have a directory that fills up with backups. If I have more than 10 backup files, I would like to remove the oldest files, so that the 10 newest backup files are the only ones that are left.

So far, I know how to count the files, which seems easy enough, but how do I then remove the oldest files, if the count is over 10?

if [ls /backups | wc -l > 10]
    then
        echo "More than 10"
fi
Nic Hubbard
  • 41,587
  • 63
  • 251
  • 412
  • This condition as written didn't work for me. I had to change the first line to this: `if [ $(ls /backups | wc -l) -gt 10 ]` Basically, the spaces within the brackets are important, the piped commands need to be wrapped in `$()`, and `-gt` is used instead of `>` for comparing numbers in brackets – Chris Apr 01 '18 at 13:16

11 Answers11

103

Try this:

ls -t | sed -e '1,10d' | xargs -d '\n' rm

This should handle all characters (except newlines) in a file name.

What's going on here?

  • ls -t lists all files in the current directory in decreasing order of modification time. Ie, the most recently modified files are first, one file name per line.
  • sed -e '1,10d' deletes the first 10 lines, ie, the 10 newest files. I use this instead of tail because I can never remember whether I need tail -n +10 or tail -n +11.
  • xargs -d '\n' rm collects each input line (without the terminating newline) and passes each line as an argument to rm.

As with anything of this sort, please experiment in a safe place.

Dale Hagglund
  • 16,074
  • 4
  • 30
  • 37
  • `+N` is the `Nth` so it would be `tail -n +11`. – Dennis Williamson Jun 02 '10 at 21:49
  • 1
    Oh, I can always figure it out eventually, I just find something non-intuitive about it every time. `sed -e 1,10d` does exactly what it says: delete the first 10 lines. – Dale Hagglund Jun 02 '10 at 22:24
  • 2
    Perfect, straight-forward answer to this problem. Thank you. – Andrew Ensley Aug 10 '11 at 20:13
  • I'm having a problem with this solution when I try to delete a folder providing an absolute path like this: ... | xargs -d '\n' -n 1 rm -rf $DESDIR/. A space is appended after $DESDIR/ which deletes the whole directory instead of just the oldest folder within that directory – markbaldy Dec 26 '13 at 05:27
  • You'd do better, I think, to pass $DESTDIR as an argument to ls. That said, however, xargs is behaving as intended given how you're using it. I don't have to man page nearby, but read it carefully, looking for examples with {} in them. This is the string to indicate direct argument interpolation. You'll also need the -n1 argument so only one line of input is consumed at a time. – Dale Hagglund Jan 27 '14 at 20:48
  • 2
    wasnt this asking to delete the 10 oldest ... not the 10 newest? Making sed -e '1,10d' .. incorrect? – carl crott Nov 08 '16 at 20:55
  • 5
    @delinquentme The OP asked for the 10 newest files to be left behind, and by deleting them from the output of `ls`, the following parts of the pipeline remove all older files. – Dale Hagglund Nov 15 '16 at 11:06
  • I think you just use command after command while just a simple find command with --delete does exactly the same thing –  Jun 03 '17 at 09:22
  • @PooyaRaki: in fact I think you'll find it is difficult to write a find command that does exactly what the OP describes. See my comments on the answer below that you prefer for a couple issues. I'd be genuinely interested in seeing a find command that does what the OP actually asked for. As regards "using command after command", see the following comment. – Dale Hagglund Jun 07 '17 at 07:57
  • @PooyaRaki: Re "command after command", I think you'll find that each of the commands I used solves exactly one part of the problem: `ls -t` lists the files in the current directory from newest to oldest; `sed` then removes the 10 newest files; and `xargs` removes all the ones that remain. I'd argue that each of these steps is a direct representation of the OP's question. – Dale Hagglund Jun 07 '17 at 08:07
  • Very nice. Just a note that if there are less than 10 files, you get a `rm: missing operand` error. Not a big deal though, since in that case we wouldn't want any files removed anyway – Chris Apr 01 '18 at 12:34
  • @Chris I guess I was assuming that xargs, when given no input lines, wouldn't run the command at all. I checked the man page briefly, and it's certainly not explicit about this. – Dale Hagglund Apr 01 '18 at 12:45
  • 3
    @DaleHagglund, I just learned about the `-r` option, which when used only runs xargs if there are any non-empty lines. So adding that option to `xargs` eliminates the error – Chris Apr 01 '18 at 13:12
  • I know this is not in the users question, but what about doing the same, but having folders instead of mentioned files. (e.g. delete all folder, but keep latest 10 folders). – Toolbox Jan 01 '19 at 14:39
  • @Toolbox For simplicity, I'll assume a directory containing only directories: you should add -r to the end of the rm command given to xargs. CAUTION: Timestamps are a bit more subtle for directories. They only change when the directory itself changes, ie, when a file is removed from it or created in it, but not when, eg, the the contents of a file on the directory is modified. PLEASE PLEASE PLEASE experiment carefully to make sure that `ls -t` finds the directories you want in the order you want. Moving to `rm -r` really increases the chance of having a bad day. – Dale Hagglund Jan 01 '19 at 18:54
  • @Toolbox One more quick comment. You can make xargs a lot more debuggable by starting with the command you want prefixed by `echo`. Now, xargs tells you what it would have done, without actually doing it. – Dale Hagglund Jan 01 '19 at 18:58
  • @Dale Hagglund I use -rf at the end of the command, but for security I make sure the script is really executed in the correct folder by [cd /path/to/folder]. It works fine. I am using this setup to cleanup a build folder. Since I am building several times a time, the folders normally increase with 5-30 folders. – Toolbox Jan 02 '19 at 06:56
  • @Toolbox Sounds like you're on top of things. I just wanted to emphasize caution, since you can remove a lot a files pretty quickly with `xargs rm -rf` – Dale Hagglund Jan 02 '19 at 07:10
  • You should probably add a -1 to your `ls` command, as it may not be aliased as the default. In fact, you could add a backslash before the ls to ensure you don't get any aliasing. ie `\ls -1t`, I needed that anyway - but I am an alias freak. :-) – moodboom Jul 06 '21 at 13:33
  • @moodboom `-1` isn't needed here because `ls` automatically turns it on if the output is not a tty. – Dale Hagglund Jul 14 '21 at 05:51
  • @DaleHagglund I was not aware of that, thanks. It must have been other aliasing that was interfering. – moodboom Jul 14 '21 at 10:17
  • @moodboom Re aliases, I would generally discourage their use in favour of small shell scripts in a personal bin directory. The latter are more portable, generally easier to read and write. (There are a few things that can only be done in aliases, of course.) And, when I do write aliases, I generally avoid shadowing common commands like `ls`. – Dale Hagglund Jul 14 '21 at 16:26
  • @DaleHagglund how can I merge your solution with this command? `find ${VERSIONS_DESTINATION} -name '.zip' -type f -mtime +30 -delete` My desire is: delete files older than 30 days, but keep at least 5 files. – FabianoLothor Jul 26 '21 at 12:56
29

find is the common tool for this kind of task :

find ./my_dir -mtime +10 -type f -delete

EXPLANATIONS

  • ./my_dir your directory (replace with your own)
  • -mtime +10 older than 10 days
  • -type f only files
  • -delete no surprise. Remove it to test your find filter before executing the whole command

And take care that ./my_dir exists to avoid bad surprises !

mahyard
  • 1,230
  • 1
  • 13
  • 34
  • 1
    I think this should be checked as the best answer while the chosen is just a waste of commands ! Good Luck –  Jun 03 '17 at 09:20
  • 9
    A few points about this solution: (1) Using `-mtime 10`, as you say, selects files older than ten days for deletion. However, the OP's question asks for the ten oldest, not all files older than ten days. (2) `find` will traverse the entire directory tree, removing files at any level. Here too, the OP doesn't ask for this behaviour. – Dale Hagglund Jun 07 '17 at 07:51
  • 3
    In the previous comment, I incorrectly said that the OP asked for the ten oldest files to be removed. In fact the OP asks for the ten newest files to be kept, and all older files to be removed. Sorry for the confusion, but in either case, `-mtime 10` doesn't quite do what's asked for. – Dale Hagglund Jun 07 '17 at 08:06
  • It isn't the OP's question, but it did solve what I was looking for however! :D Thanks @MahyarDamavand – Penumbra May 22 '19 at 00:01
14

Make sure your pwd is the correct directory to delete the files then(assuming only regular characters in the filename):

ls -A1t | tail -n +11 | xargs rm

keeps the newest 10 files. I use this with camera program 'motion' to keep the most recent frame grab files. Thanks to all proceeding answers because you showed me how to do it.

Dennis Meng
  • 5,109
  • 14
  • 33
  • 36
mikecolley
  • 149
  • 1
  • 2
10

The proper way to do this type of thing is with logrotate.

Dennis Williamson
  • 346,391
  • 90
  • 374
  • 439
  • 2
    `logrotate` is a good answer, but it might be a bit heavy-weight: it needs a config file and it's at least somewhat biased toward logfiles semi-official places. Also, doesn't it assume that it should first rotate the logs (ie, rename .N to .N+1) and then delete the oldest? At least as written, the OP's question doesn't imply the rotation of a fixed name. – Dale Hagglund Jun 12 '10 at 10:04
3

I like the answers from @Dennis Williamson and @Dale Hagglund. (+1 to each)

Here's another way to do it using find (with the -newer test) that is similar to what you started with.

This was done in bash on cygwin...

if [[ $(ls /backups | wc -l) > 10 ]]
then
  find /backups ! -newer $(ls -t | sed '11!d') -exec rm {} \;
fi
Daniel Haley
  • 51,389
  • 6
  • 69
  • 95
2

Straightforward file counter:

max=12
n=0
ls -1t *.dat |
while read file; do
    n=$((n+1))
    if [[ $n -gt $max ]]; then
        rm -f "$file"
    fi
done
Gustavo Morales
  • 2,614
  • 9
  • 29
  • 37
2

I just found this topic and the solution from mikecolley helped me in a first step. As I needed a solution for a single line homematic (raspberrymatic) script, I ran into a problem that this command only gave me the fileames and not the whole path which is needed for "rm". My used CUxD Exec command can not start in a selected folder.

So here is my solution:

ls -A1t $(find /media/usb0/backup/ -type f -name homematic-raspi*.sbk) | tail -n +11 | xargs rm

Explaining:

  • find /media/usb0/backup/ -type f -name homematic-raspi*.sbk searching only files -type f whiche are named like -name homematic-raspi*.sbk (case sensitive) or use -iname (case insensitive) in folder /media/usb0/backup/
  • ls -A1t $(...) list the files given by find without files starting with "." or ".." -A sorted by mtime -t and with a return of only one column -1
  • tail -n +11 return of only the last 10 -n +11 lines for following rm
  • xargs rm and finally remove the raiming files in the list

Maybe this helps others from longer searching and makes the solution more flexible.

indiana
  • 21
  • 1
1
stat -c "%Y %n" * | sort -rn | head -n +10 | \
        cut -d ' ' -f 1 --complement | xargs -d '\n' rm

Breakdown: Get last-modified times for each file (in the format "time filename"), sort them from oldest to newest, keep all but the last ten entries, and then keep all but the first field (keep only the filename portion).

Edit: Using cut instead of awk since the latter is not always available

Edit 2: Now handles filenames with spaces

bta
  • 43,959
  • 6
  • 69
  • 99
  • I usually use 'cut' for the last step because awk isn't always installed on all machines. – Jay Jun 02 '10 at 18:02
1

On a very limited chroot environment, we had only a couple of programs available to achieve what was initially asked. We solved it that way:

MIN_FILES=5
FILE_COUNT=$(ls -l | grep -c ^d )


if [ $MIN_FILES -lt $FILE_COUNT  ]; then
  while [ $MIN_FILES -lt $FILE_COUNT ]; do
    FILE_COUNT=$[$FILE_COUNT-1]
    FILE_TO_DEL=$(ls -t | tail -n1)
    # be careful with this one
    rm -rf "$FILE_TO_DEL"
  done
fi

Explanation:

  • FILE_COUNT=$(ls -l | grep -c ^d ) counts all files in the current folder. Instead of grep we could use also wc -l but wc was not installed on that host.
  • FILE_COUNT=$[$FILE_COUNT-1] update the current $FILE_COUNT
  • FILE_TO_DEL=$(ls -t | tail -n1) Save the oldest file name in the $FILE_TO_DEL variable. tail -n1 returns the last element in the list.
miron
  • 1,361
  • 1
  • 11
  • 24
0

Based on others suggestions and some awk foo, I got this to work. I know this an old thread, but I didn't find a decent answer here and this sorted it for me. This just deletes the oldest file, but you can change the head -n 1 to 10 and get the oldest 10.

find $DIR -type f -printf '%T+ %p\n' | sort | head -n 1 | awk '{first =$1; $1 =""; print $0}' | xargs -d '\n' rm

Morgan
  • 14
  • 3
-2

Using inode numbers via stat & find command (to avoid pesky-chars-in-file-name issues):

stat -f "%m %i" * | sort -rn -k 1,1 | tail -n +11 | cut -d " " -f 2 | \
   xargs -n 1 -I '{}' find "$(pwd)" -type f -inum '{}' -print

#stat -f "%m %i" * | sort -rn -k 1,1 | tail -n +11 | cut -d " " -f 2 | \
#   xargs -n 1 -I '{}' find "$(pwd)" -type f -inum '{}' -delete 
bashfu
  • 1