Bash: return failure from subshell when any command fails

I have a complex script where I generally test each command individually for success. However, there are cases where I have to carry out a sequence of simple operations, and I’d prefer to get an exit status from the entire sequence, without bothering to check each command individually.

This is my first attempt, with some arbitrary commands:

#!/bin/bash
if ! (
    set -e
    cd test
    touch foo
    chown root:root foo)
then
    echo "subshell failed"
else
    echo "subshell completed"
fi

The set -e was meant to ensure that the subshell exited with failure when the first failure was encountered, but this doesn’t work. In this case, if directory test doesn’t exist, the script simply creates foo in the current directory, and then fails on the chown.

What’s the right way to do this? In other words, echo ‘subshell completed’ only if all 3 commands completed without error?

Asked By: QF0

||

Re: https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html

The shell does not exit if the command that fails is part of the
command list immediately following a while or until keyword, part of
the test in an if statement, part of any command executed in a && or
|| list except the command following the final && or ||, any command
in a pipeline but the last, or if the command’s return status is being
inverted with !.

Emphasis mine.

Answered By: clvrmnky

The right way is to check the exit status of each command and do whatever cleanup is appropriate at that point. (Along with printing an appropriate error message, but the tools themselves likely do that already.) For example, if the file gets created, but the permissions can’t be set properly, do you want to leave the file there or should you (try to) remove it?

(Granted, this particular case would be a bit unexpected, but anyway.)

If you don’t want to be that fancy, then just say you want to run each command only if the previous one succeeds, i.e. join them with &&:

file=foo
if  cd test &&
    touch "$file" &&
    chown root:root "$file"
then
    echo ok.
else
    echo something failed.
fi

Note that that would leave the script running in the newly-created directory. Which may or may not be what you want to do, depending on what the script does next. Of course you could still use a subshell to isolate the effects of the cd:

if  ( cd test &&
      touch "$file" &&
      chown root:root "$file" )
then ...

or just do away with the cd entirely:

filepath="test/$file"
if  touch "$filepath" &&
    chown root:root "$filepath" )
then ...

(don’t use path as a variable name if you think you’ll ever use zsh; it’ll blow up hilariously.)

In any case, forget about set -e. It doesn’t do what you want and is confusing enough to likely be worse than useless. See e.g. BashFAQ 105

Answered By: ilkkachu
Categories: Answers Tags:
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.