5

I have a shell script that looks for a file, /tmp/bbsnode1, and if the existence of that file is true, it deletes it. What I'd like to do is if multiple files exist (/tmp/bbsnode2, /tmp/bbsnode3, and /tmp/bbsnode4), delete all of them. But only delete them if all of them exist.

Here's what I have so far:

if [ -f /tmp/bbsnode1 ]
then
/usr/bin/rm /tmp/bbsnode1
fi
don_crissti
  • 82,805
ignatius
  • 401

5 Answers5

11

I would use a shell function for this, rather than a script:

rm-all-or-none() {
  for f; do
    [ -f "$f" ] ||
      { printf '%s is not an existing file, no files removed\n' "$f" >&2
        return 1;}
  done
  rm -fv -- "$@"
}

Then I would call it using brace expansion, rather than a glob. Globs only expand to files that exist, but in this case we want to specify the files and only remove them if all of them exist:

rm-all-or-none /tmp/bbsnode{1..4}

Longer equivalent version:

rm-all-or-none() {
  for f in "$@"; do
    if [ -f "$f" ]; then
      :
    else
      printf '%s is not an existing file, no files removed\n' "$f" >&2
      return 1
    fi
  done
  rm -fv -- "$@"
}

Also see:

Wildcard
  • 36,499
  • 1
    That is an question for (a) Wildcard !! – Archemar Nov 04 '16 at 06:18
  • 1
    I hadn't noticed that detail. :D – muru Nov 04 '16 at 06:22
  • I don't know if "golfing" the code is appropriate here... why not write: for f in "$@"; do instead? – Olivier Dulac Nov 04 '16 at 14:08
  • 2
    I would agree with @Oliver here, I am pretty knowledgeable in bash, but it took a few glances myself to catch everything going on. Very few people are aware that you could omit in "$@" in this case, IMHO. I'd even change the use of || to a regular if statement to make it easier to pick up. I do like the use of -- though on rm. I often omit that, but it can be very important and makes the script much more rugged when used long-term. – penguin359 Nov 04 '16 at 16:54
  • 1
    @penguin359, the purpose here is to increase the number of people who are aware that you can omit in "$@". ;) I wasn't trying to golf, I just tend to prefer || { ...;} for negative conditions with an error-exit action associated. But I've now added a longer form of the function as well. – Wildcard Nov 04 '16 at 18:00
  • 2
    @Wildcard part of my concern was that there was no explanation for the for f; and not everyone would be able to pick up on it, but seeing it next to the longer version makes it very clear indeed! Thanks! – penguin359 Nov 04 '16 at 18:19
8

You could use an arbitrary command like ls to check for the files and delete them in one line

ls /tmp/bbsnode{1,2,3,4} &>/dev/null && rm /tmp/bbsnode{1,2,3,4}

Note that in general it's unsafe to do such things in /tmp because any other user could create conflicting files with the same names.

A short explanation:

The return value of ls is non-zero if one of the files does not exist. The {1,2,3,4} is brace expansion: it expands to a string for each number: so /tmp/bbsnode{1,2,3,4} is the same as /tmp/bbsnode1 /tmp/bbsnode2 /tmp/bbsnode3 /tmp/bbsnode4. The && executes the next command only if the previous command has a zero return value, and so here rm is only executed if all 4 files exist. Finally, the &> /dev/null suppresses the output of ls (&> redirected both stdout and stderr, /dev/null gets rid of it).

Below another solution with shell builtins only. It's similar to what others have answered but without an extra function or script:

set -- /tmp/bbsnode{1,2,3,4}
(for f; do test -f "$f" || exit; done) && rm -- "$@"
rudimeier
  • 10,315
3

In this particular case you could do:

set -- file[1-4]
[[ $# -eq 4 ]] && rm -f -- "$@"

This sets the argument list to the file names that match any of file1, file2, file3 or file41 then rms those files only if the number of arguments equals 4, that is, if all files exist.


1: for simplicity, I use file instead of /tmp/bbsnode

don_crissti
  • 82,805
3

The simplest way is via two commands: set -e and stat

#!/bin/bash
set -e # make script exit if there's errors

main()
{
    stat "$@"
    rm "$@"
}

main "$@"

How this works

The key here is set -e. It makes the script exit immediately if errors are encountered. stat takes on the list of all the files you give it. If there is a file missing, it will return exit status 1, which signifies error. That way, your script will get to rm part if and only if there's no errors encountered with stat

Side-notes

  • you can use stat "$@" > /dev/null to suppress output to screen
  • Using main() function isn't required, this is just stylistic preference of the author.
  • as rudimeier pointed out in the comments, you can also use a one-liner with these two commands: stat "$@" && rm "$@".

Test Run:

bash-4.3$ chmod +x remove_all_if_exist.sh 
bash-4.3$ touch /tmp/{file1,file2}
bash-4.3$ ls /tmp/file*
/tmp/file1  /tmp/file2
bash-4.3$ ./remove_all_if_exist.sh  /tmp/file1 /tmp/file2 /tmp/file3
  File: '/tmp/file1'
  Size: 0           Blocks: 0          IO Block: 4096   regular empty file
Device: 801h/2049d  Inode: 3423307     Links: 1
Access: (0664/-rw-rw-r--)  Uid: ( 1000/ xieerqi)   Gid: ( 1000/ xieerqi)
Access: 2016-11-04 17:44:06.587438784 -0600
Modify: 2016-11-04 17:44:06.587438784 -0600
Change: 2016-11-04 17:44:06.587438784 -0600
 Birth: -
  File: '/tmp/file2'
  Size: 0           Blocks: 0          IO Block: 4096   regular empty file
Device: 801h/2049d  Inode: 3423308     Links: 1
Access: (0664/-rw-rw-r--)  Uid: ( 1000/ xieerqi)   Gid: ( 1000/ xieerqi)
Access: 2016-11-04 17:44:06.587438784 -0600
Modify: 2016-11-04 17:44:06.587438784 -0600
Change: 2016-11-04 17:44:06.587438784 -0600
 Birth: -
stat: cannot stat '/tmp/file3': No such file or directory
bash-4.3$ ls /tmp/file*
/tmp/file1  /tmp/file2
bash-4.3$ ./remove_all_if_exist.sh  /tmp/file1 /tmp/file2
  File: '/tmp/file1'
  Size: 0           Blocks: 0          IO Block: 4096   regular empty file
Device: 801h/2049d  Inode: 3423307     Links: 1
Access: (0664/-rw-rw-r--)  Uid: ( 1000/ xieerqi)   Gid: ( 1000/ xieerqi)
Access: 2016-11-04 17:44:06.587438784 -0600
Modify: 2016-11-04 17:44:06.587438784 -0600
Change: 2016-11-04 17:44:06.587438784 -0600
 Birth: -
  File: '/tmp/file2'
  Size: 0           Blocks: 0          IO Block: 4096   regular empty file
Device: 801h/2049d  Inode: 3423308     Links: 1
Access: (0664/-rw-rw-r--)  Uid: ( 1000/ xieerqi)   Gid: ( 1000/ xieerqi)
Access: 2016-11-04 17:44:06.587438784 -0600
Modify: 2016-11-04 17:44:06.587438784 -0600
Change: 2016-11-04 17:44:06.587438784 -0600
 Birth: -
bash-4.3$ ls /tmp/file*
ls: cannot access '/tmp/file*': No such file or directory
bash-4.3$ 

Alternative with python:

python -c 'import sys,os;f=sys.argv[1:];c=[os.path.exists(a) for a in f]; l = len(c)*[True]; result = [os.unlink(a) for a in f] if l == c else False; print result' /tmp/file1 /tmp/file2
  • get list of command line-arguments with sys.argv[1:]
  • build a list of true-false values if files exist
  • if all files return True, unlink(remove) them
  • Very very noisy. Why not suppress all output from the stat command? Also, regarding your preference for main() in a shell script: See the hacker section of Evolution of a Programmer. ;) – Wildcard Nov 04 '16 at 23:58
  • @Wildcard yep, very noisy, that's why I've added "Side Notes" section ( which you probably missed ) which does mention redirection to /dev/null. I just prefer giving the OP full-test, so to speak, so that they know it works. Also, about main() , see this: http://unix.stackexchange.com/q/313256/85039 ;) – Sergiy Kolodyazhnyy Nov 05 '16 at 00:01
  • 1
    I think more simple would be stat "$@" && rm "$@" without set -e, which would be the same like in my answer. – rudimeier Nov 05 '16 at 06:47
  • @rudimeier sure, that can be done as well. – Sergiy Kolodyazhnyy Nov 05 '16 at 06:53
  • Thank you all, for your suggestions. They're greatly appreciated. But I'll stick with the first answer, it does what it's supposed to do, regardless of how many characters the command(s) are. So, again, thank you. – ignatius Nov 05 '16 at 08:24
0

You could use this script, named removing_group

#!/usr/bin/bash

function check {
    while (( "$#" )) ; do
        if [ -f "$1" ] ; then
            # echo "The file $1" ;
            shift ;
        else
            # echo "The's no file $1";
            return 1
        fi
    done
    return 0
}

if check $@; then
    while (( "$#" )) ; do
        # echo "Remove $1" ;
        rm "$1" ;
        shift ;
    done
fi

The check function checks all of it arguments to be regular files. If the next file isn't exist, the function returns 1 and nothing happens. If all of the files exists, then returned 0 will process if check $@, which will remove files one-by-one. The files for checking and removing described as parameters in the command line, for example

./removing_group /tmp/bbsnode1 /tmp/bbsnode2 /tmp/bbsnode3 /tmp/bbsnode4

or

./removing_group /tmp/bbsnode{1,2,3,4}