I`m trying to delete old files from directory and leave only 3 newest files.

cd /home/user1/test

while [ `ls -lAR | grep ^- | wc -l` < 3 ] ; do

    rm `ls -t1 /home/user/test | tail -1`
    echo " - - - "


something is wrong with conditional statement.


If you want to loop over files, never use ls*. tl;dr There are lots of situations where you'd end up deleting the wrong file, or even all files.

That said, unfortunately this is a tricky thing to do right in Bash. There's a working answer over at a duplicate question my even older find_date_sorted which you can use with small modifications:

while IFS= read -r -d '' -u 9
    let ++counter
    if [[ counter -gt 3 ]]
        path="${REPLY#* }" # Remove the modification time
        echo -e "$path" # Test
        # rm -v -- "$path" # Uncomment when you're sure it works
done 9< <(find . -mindepth 1 -type f -printf '%TY-%Tm-%TdT%TH:%TM:%TS %p\0' | sort -rz) # Find and sort by date, newest first

* No offense guys - I also used ls before. But it really isn't safe.

Edit: New find_date_sorted with unit tests.

  • I second the part about not parsing ls. Also the script is neat but I think it could be done in a one liner, let me check ... – rahmu Apr 5 '12 at 12:10
  • Well, you can always compact Bash code into a one-liner, but the issue is whether it'll be readable :) – l0b0 Apr 5 '12 at 12:59
  • 2
    If I may mercilessly steal an idea from @Peter.O, try ((++counter>3)) as your test. It's nicely succinct. As for oneliners: If brevity is a concern wrap the code in a function, then don't worry about it. – Sorpigal Apr 5 '12 at 13:38
  • @Sorpigal Neat shortcut – l0b0 Apr 5 '12 at 13:39

To delete all but the 3 newest files using a zsh glob, you can use Om (capital O) to sort the files from oldest to newest and a subscript to grab the files you want.

rm ./*(Om[1,-4])
#    | ||||  ` stop at the 4th to the last file (leaving out the 3 newest)
#    | |||` start with first file (oldest in this case)
#    | ||` subscript to pick one or a range of files
#    | |` look at modified time
#    | ` sort in descending order
#    ` start by looking at all files

Other examples:

# delete oldest file (both do the same thing)
rm ./*(Om[1])
rm ./*(om[-1])

# delete oldest two files
rm ./*(Om[1,2])

# delete everything but the oldest file
rm ./*(om[1,-2])

By far the easiest method is to use zsh and its glob qualifiers: Om to sort by decreasing age (i.e. oldest first) and [1,3] to retain only the first three matches.

rm ./*(Om[1,3])

See also How do I filter a glob in zsh for more examples.

And do heed l0b0's advice: your code will break horribly if you have file names that contain shell special characters.

  • Woa woa hold the train, is this really a complete solution in 1 line? – Ryan May 15 '12 at 16:34
  • 2
    @Ryan That's zsh for you. – Gilles 'SO- stop being evil' May 15 '12 at 18:28

You can use the following function to get the newest file in a directory:

    local newest=$1

    for f;do [[ $f -nt $newest ]] && newest="$f"; done;

    printf '%s\n' "$newest"

Give it a different set of files by exculding the newest file after each iteration.

Tip: If you hold the initial set of files in an array called "${files[@]}", then save the index of the newest file found, and unset 'files[index]' before the next iteration.

Usage: newest_in [set of files/directories]

Sample output:

[rany$] newest_in ./*

First, the -R option is for recursion, which is probably not what you want - that will search in all subdirectories as well. Second, the < operator (when not being seen as redirection) is for string comparison. You probably want -lt. Try:

while [ `ls -1A | grep ^- | wc -l` -lt 3 ]

But I would use find here:

while [ `find . -maxdepth 1 -type f -print | wc -l` -lt 3 ]
  • Both will fail if one or more of the file names contain a '$\n'. – Rany Albeg Wein Apr 28 '13 at 8:15

I know it's a sin to parse ls, but what about this:

find . -type f -maxdepth 1 | xargs ls -ltr | sed '1,3d' | awk '{print $9}' | xargs rm

A quick test with 5 empty files:

$ >1
$ >2
$ >3
$ >4
$ >5
$ find . -type f -maxdepth 1 | xargs ls -lt | sed '1,3d' | awk '{print $9}' | xargs rm
$ ls -1
  • This one will fail for two reasons - Parsing ls output and using find and xargs together in the wrong way. If you combine find and xargs I recommend you to use finds -print0 and accordingly xargss -O options. Read about Using Find for more information about the subject. You might also want to take a look and read about why Parsing ls is very bad. – Rany Albeg Wein Apr 28 '13 at 8:32

This one-liner does it all I think:

ls -t1 /home/user/test | tail -n +4 | xargs -I{} rm -- "{}"
  • ls is a tool for interactively looking at file information. Its output is formatted for humans and will cause bugs in scripts. Use globs or find instead. Understand why: mywiki.wooledge.org/ParsingLs – Rany Albeg Wein Apr 28 '13 at 8:34

Here's one solution:

rm /appserver/webapplogs/$(ls -t1 /appserver/webapplogs/|tail -1)
  • 1
    @bakbak ls is a tool for interactively looking at file information. Its output is formatted for humans and will cause bugs in scripts. Look here to understand why. Specificaly speaking about your answer - this one will break if one or more of the files contain a $'\n' character. Also, the Command Substitution ( i.e. $(...) ) lacks double-quotes which brings up another important subject - Word Splitting! – Rany Albeg Wein Apr 28 '13 at 8:23

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.