2

This is a backup script that copies files from one directory to another. I use a for loop to check if there are more than five files. If there are, the loop should delete the oldest entries first.

I tried ls -tr | head -n -5 | xargs rm from the command line and it works successfully to delete older files if there are more than 5 in the directory.

However, when I put it into my for loop, I get an error rm: missing operand

Here is the full script. I don't think I am using the for loop correctly in the script, but I'm really not sure how to use the commands ls -tr | head -n -5 | xargs rm in a loop that iterates over the files in the directory.

timestamp=$(date +"%m-%d-%Y")
dest=${HOME}/mybackups
src=${HOME}/safe
fname='bu_'
ffname=${HOME}/mybackups/${fname}${timestamp}.tar.gz

# for loop for deletion of file
for f in ${HOME}/mybackups/*
do
  ls -tr | head -n -5 | xargs rm
done

if [ -e $ffname ];
  then
      echo "The backup for ${timestamp} has failed." | tee ${HOME}/mybackups/Error_${timestamp}
  else
      tar -vczf ${dest}/${fname}${timestamp}.tar.gz ${src}
fi

Edit: I took out the for loop, so it's now just:

[...]
ffname=${HOME}/mybackups/${fname}${timestamp}.tar.gz

ls -tr | head -n -5 | xargs rm

if [ -e $ffname ];
[...]

The script WILL work if it is in the mybackups directory, however, I continue to get the same error if it is not in that directory. The script gets the file names but tries to remove them from the current directory, I think... I tried several modifications but nothing has worked so far.

mklement0
  • 382,024
  • 64
  • 607
  • 775
Michael Queue
  • 1,340
  • 3
  • 23
  • 38

2 Answers2

4

I get an error rm: missing operand

The cause of that error is that there are no files left to be deleted. To avoid that error, use the --no-run-if-empty option:

ls -tr | head -n -5 | xargs --no-run-if-empty rm

In the comments, mklement0 notes that this issue is peculiar to GNU xargs. BSD xargs will not run with an empty argument. Consequently, it does not need and does not support the --no-run-if-empty option.

More

Quoting from a section of code in the question:

for f in ${HOME}/mybackups/*
do
  ls -tr | head -n -5 | xargs rm
done

Note that (1) f is never used for anything and (2) this runs the ls -tr | head -n -5 | xargs rm several times in a row when it needs to be run only once.

Obligatory Warning

Your approach parses the output of ls. This makes for a simple and easily understood command. It can work if all your files are sensibly named. It will not work in general. For more on this, see: Why you shouldn't parse the output of ls(1).

Safer Alternative

The following will work with all manner of file names, whether they contains spaces, tabs, newlines, or whatever:

find . -maxdepth 1 -type f -printf '%T@ %i\n' | sort -n | head -n -5 | while read tstamp inode
do
    find . -inum "$inode" -delete
done
John1024
  • 109,961
  • 14
  • 137
  • 171
  • ok I see what you are saying. So, I removed the for loop and just run the command in the script but now it finds the correct files but says it cannot remove them `cannot remove ‘6.txt’: No such file or directory` I don't think rm is working on the correct directory. – Michael Queue Apr 01 '15 at 21:14
  • [What's the working directory of ls -tr?](http://askubuntu.com/questions/604110/bash-script-to-delete-files-in-a-directory-if-there-are-more-than-5#comment844104_604110) – Cyrus Apr 01 '15 at 21:15
  • @Cyrus `${HOME}/mybackups/` – Michael Queue Apr 01 '15 at 21:17
  • @MichaelJames Is there an actual file in the directory named `6.txt`? If a file in your directory has whitespace in its name, you will get errors like this (this why parsing ls is considered bad). – John1024 Apr 01 '15 at 21:22
  • If there are more than 5 files in the directory, how many of the older files are you wanting to remove? – ILostMySpoon Apr 01 '15 at 21:22
  • @MichaelJames: `ls`'s working directory is the directory from which you execute your script. If you execute your script from /tmp/foobar then `ls -tr` shows content of /tmp/foobar. – Cyrus Apr 01 '15 at 21:25
  • @John1024 yes that file is actually in the directory. This is a contrived exercise, I'm really new to bash scripting. However, the script is not run from that directory. – Michael Queue Apr 01 '15 at 21:25
  • @ILostMySpoon All of them and the command `ls -tr | head -n -5 | xargs rm` does that correctly – Michael Queue Apr 01 '15 at 21:28
  • @MichaelJames "No such file or directory" That is odd. What type of filesystem is this file on? Is it an NFS share? Is it on a windows (NTFS) disk? Also, I added an approach to the answer that does not make direct use of file names. Let me know if that works. – John1024 Apr 01 '15 at 21:52
  • @John1024 Hi John. I tried your solution but it didn't work. I'm running Ubuntu. Still haven't figured it out unfortunately. – Michael Queue Apr 02 '15 at 02:16
  • Nicely done. You probably meant `sort -nr`, not `sort -n`. "Consider:" confused me at first, because I thought it was a _recommendation_ rather than a critical analysis of the OP's code. As a minor point of interest: _BSD_ `xargs` does _not_ run with empty input (and doesn't support `--no-run-if-empty`). – mklement0 Apr 02 '15 at 05:07
  • 1
    @mklement0 Thanks much. I clarified the "consider" part and added your info on BSD `xargs`. It may seem surprising (it was to me), but, when used with `head -n -5` as in the OP's code, `sort -n` is actually correct here. (I verified this with tests.) Alternatively, we could have used `sort -rn | tail -n +6` to the same effect. – John1024 Apr 02 '15 at 05:26
  • Thanks for updating and for clarifying the `sort` issue; I had a misconception about what `head -n -5` does: it actually drops the _last_ 5 lines. (BSD `head`, sadly, doesn't support this; there it can be emulated with `awk`: `awk -v n=5 '{l[NR]=$0; if (NR > n) {print l[NR-n]; delete l[NR-n]}}'`) – mklement0 Apr 02 '15 at 14:14
0

SMH. I ended up coming up to the simplest solution in the world by just cd-ing into the directory before I ran ls -tr | head -n -5 | xargs rm . Thanks for everyone's help!

timestamp=$(date +"%m-%d-%Y")
dest=${HOME}/mybackups
src=${HOME}/safe
fname='bu_'
ffname=${HOME}/mybackups/${fname}${timestamp}.tar.gz

cd ${HOME}/mybackups
ls -tr | head -n -5 | xargs rm

if [ -e $ffname ];
  then
      echo "The backup for ${timestamp} has failed." | tee ${HOME}/mybackups/Error_${timestamp}
  else
      tar -vczf ${dest}/${fname}${timestamp}.tar.gz ${src}
fi

This line ls -tr | head -n -5 | xargs rm came from here

ls -tr displays all the files, oldest first (-t newest first, -r reverse).

head -n -5 displays all but the 5 last lines (ie the 5 newest files).

xargs rm calls rm for each selected file

.

Community
  • 1
  • 1
Michael Queue
  • 1,340
  • 3
  • 23
  • 38