9

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 " - - - "

done

something is wrong with conditional statement.

8 Answers 8

9

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:

counter=0
while IFS= read -r -d '' -u 9
do
    let ++counter
    if [[ counter -gt 3 ]]
    then
        path="${REPLY#* }" # Remove the modification time
        echo -e "$path" # Test
        # rm -v -- "$path" # Uncomment when you're sure it works
    fi
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.

4
  • 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
    Commented Apr 5, 2012 at 12:10
  • Well, you can always compact Bash code into a one-liner, but the issue is whether it'll be readable :)
    – l0b0
    Commented Apr 5, 2012 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
    Commented Apr 5, 2012 at 13:38
  • @Sorpigal Neat shortcut
    – l0b0
    Commented Apr 5, 2012 at 13:39
5

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])
5

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.

2
  • Woa woa hold the train, is this really a complete solution in 1 line?
    – MetaGuru
    Commented May 15, 2012 at 16:34
  • 2
    @Ryan That's zsh for you. Commented May 15, 2012 at 18:28
1

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

newest_in() 
{ 
    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 ./*
foo.txt
[rany$]
-1

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 ]
1
  • Both will fail if one or more of the file names contain a '$\n'. Commented Apr 28, 2013 at 8:15
-1

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
3
4
5
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. Commented Apr 28, 2013 at 8:32
-2

This one-liner does it all I think:

ls -t1 /home/user/test | tail -n +4 | xargs -I{} rm -- "{}"
1
  • 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 Commented Apr 28, 2013 at 8:34
-2

Here's one solution:

rm /appserver/webapplogs/$(ls -t1 /appserver/webapplogs/|tail -1)
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! Commented Apr 28, 2013 at 8:23

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .