10

I'm writing a shell script that needs to do a bunch of commands and every command depends on every previous command. If any command fails the entire script should fail and I call an exit function. I could check the exit code of each command but I am wondering if there is mode I can enable or a way to get bash to do that automatically.

For example, take these commands:

cd foo || myfunc
rm a || myfunc
cd bar || myfunc
rm b || myfunc


Is there a way where I can somehow signal to the shell before executing these commands that it should call myfunc if any of them fail, so that instead I can write something cleaner like:

cd foo
rm a
cd bar
rm b
12

You can use bash trap ERR to make your script quit if any command returns status greater than zero and execute your function on exiting.

Something like:

myfunc() {
  echo 'Error raised. Exiting!'
}

trap 'myfunc' ERR

# file does not exist, causing error
ls asd
echo 123

Note that bash trap ERR does implicit set -o errexit or set -e and is not POSIX.

And the ERR trap is not executed if the failed command is part of the command list immediately following until or while keyword, part of the test following the if or elif reserved words, part of a command executed in && or || list, or if the command’s return status is being inverted using !.

1

A (perhaps) simpler variation on the accepted answer:

  1. Use set -e to cause the failure of one command to abort the execution of a list.
  2. Simply list your commands.
  3. Use an if-then-else statement to execute your error-handling command(s).  This last piece is a bit tricky.  Watch:
set -e
if
    cmd1                        # e.g.,     cd foo
    cmd2                        # e.g.,     rm a
    cmd3                        # e.g.,     cd bar
    cmd4                        # e.g.,     rm b
then
    set +e
    commands to do upon success (if any)
else
    set +e
    myfunc
    other commands to do upon failure (if any)
fi

The tricky part is that you put your commands into the if part of the if-then-else, not the then part or the else part.  Recall that the syntax of the if statement is

if list; then list; [ elif list; then list; ] ... [ else list; ] fi
   ↑↑↑↑
The set -e tells the shell that, if cmd1 (cd foo) fails, it should not go on and execute cmd2 (rm a), and so on down the line.  If this happened to a command at the outermost level of a shell script, the shell would exit.  However, since the cmd1 · cmd2 · cmd3 · cmd4 is a (compound) list following an if, the failure of any of those four commands simply causes the entire list to fail — which causes the else clause to be executed.  If all four commands succeed, the then clause is executed.

In either case, the first thing you should do is probably to disable (turn off) the e option by doing set +e. Otherwise, the script might get blown out of the water if a command in myfunc fails.

set -e is specified and described in the POSIX specification.

0

Taken your word "every command depends on every previous command. If any command fails the entire script should fail" literally, i think you don't need any special function to treat the errors .

All you need is to chain your commands with && operator and || operator, which does exactly what you wrote.

For example this chain will broke and will print "something went wrong" if any of the previous commands broke (bash reads from left to right)

cd foo && rm a && cd bar && rm b || echo "something went wrong"

Real example (i created dir foo, file a, dir bar and file b just for a real demo):

gv@debian:/home/gv/Desktop/PythonTests$ cd foo && rm a && cd bar && rm bb || echo "something is wrong"
rm: cannot remove 'bb': No such file or directory
something is wrong #mind the error in the last command

gv@debian:/home/gv/Desktop/PythonTests$ cd foo && rm aa && cd bar && rm b || echo "something is wrong"
rm: cannot remove 'aa': No such file or directory
something is wrong #mind the error in second command in the row

And finally if all commands have been executed successfully (exit code 0), script just goes on :

gv@debian:/home/gv/Desktop/PythonTests$ cd foo && rm a && cd bar && rm b || echo "something is wrong"
gv@debian:/home/gv/Desktop/PythonTests/foo/bar$ 
# mind that the error message is not printed since all commands were successful.

What is important to remember is that with the use of && next command is executed if previous command exited with code 0 which for bash means success.

If any command goes wrong in the chain then the command / script / whatever follows || will be executed.

And just for the record, If you need to perform different actions depending on the command that broke , you could also do it with classic script by monitoring the value of $? which reports the exit code of the exactly previous command (returns zero if command executed successfully or other positive number if command failed)

Example:

for comm in {"cd foo","rm a","cd bbar","rm b"};do  #mind the error in third command
eval $comm
    if [[ $? -ne 0 ]];then 
        echo "something is wrong in command $comm"
        break
    else 
    echo "command $comm executed succesful"
    fi
done

Output:

command cd foo executed succesfull
command rm a executed succesfull
bash: cd: bbar: No such file or directory
something is wrong in command cd bbar

Tip : You can suppress the message "bash: cd: bbar: No such file..." by applying eval $comm 2>/dev/null

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.