4

I am trying to run a list of commands, stop on the first error (non-zero return code) and run some error handling if this happens.

My current solution:

set -e
set -o pipefail

(
  cmd1
  cmd2
  ...
)

So far so good, the command stops at the first error. But if I add a catcher:

set -e
set -o pipefail
(
  cmd1
  cmd2
) || ( failure_catcher )

Then even if cmd1 fails, cmd2 is still called! It looks like the first subshell is affected by the presence of the catcher.

For a testable example:

set -e
set -o pipefail
(
    false
    echo "After failure"
) || ( echo "Failed" )

Prints "After failure", while I would expect it to print "Failed". Even if I add set -e at the start of the subshell, it still prints "After failure".

I'm clearly missing something fundamental in the way bash handles sub-shells. I'm now looking into alternative solutions, using trap handlers for the failure catcher, but what is wrong with the sub-shell catcher here?

EDIT: I might have found a way:

runit() {
    false
    echo "After failure"
}
export -f runit

(
    bash -e -o pipefail -c runit
) || ( echo "Failed" )

This still lets me embed any valid bash script inside the runit function, preserving the -e option there.

4
  • I think it's more that the || puts the subshell in a context where -e doesn't apply. You'll need to use && instead of newline or ; in the subshell. Commented Feb 25 at 15:57
  • So it's entirely impossible to re-enable -e in there? EDIT: Ah I may have found a way - define and export a function with the commands, then run bash -c the_function_name instead of the commands - in there, set -e will properly work. Commented Feb 25 at 17:06
  • 3
    The shell does not exit if the command that fails is part of the command list immediately following a while or until reserved word, part of the test in an if statement, part of any command executed in a && or || list except the command following the final && or ||... -- Bash Manual (I think more or less the idea is that, it will not exit when the "semantics" is that the failed command is a just "test" in a conditional. When you have || catcher, you made the "subshell" such a "test" -- not so sure if "subshell" or not even matters here) Commented Feb 25 at 18:48
  • Yes @Gyscos, invoking an entirely different interpreter gives a new top-level context, so bash -e -c $my_function is quite different to just forking a subshell. Commented Feb 25 at 21:13

1 Answer 1

1

Regarding the OP's "My current solution:" section.

OP posted the following.

set -e
set -o pipefail
(
  cmd1
  cmd2
) || ( failure_catcher )

Since cmd1 and cmd2 are part of a || list, the errexit option is treated as off when these commands execute. Below is one possible fix, where the errexit option will be treated as on when executing cmd1 and cmd2. Here, set +e temporarily sets the errexit to off so the || list can determine if cmd1 or cmd2 exited with a non-zero status.

set -e
set -o pipefail
( set +e
  ( set -e
    cmd1
    cmd2
  )
  [ $? = 0 ] || ( failure_catcher )
)

Regarding the OP's "For a testable example:" section.

OP posted the following.

set -e
set -o pipefail
(
    false
    echo "After failure"
) || ( echo "Failed" )

Using my possible fix would translate the above to what is shown below.

set -e
set -o pipefail
( set +e
  ( set -e
    false
    echo "After failure"
  )
  [ $? = 0 ] || ( echo "Failed" )
)

Regarding the OP's "I might have found a way:" section.

OP posted the following.

runit() {
    false
    echo "After failure"
}
export -f runit

(
    bash -e -o pipefail -c runit
) || ( echo "Failed" )

Using my possible fix would translate the above to what is shown below.

runit() {
    false
    echo "After failure"
}

( set +e
  ( set -e -o pipefail; runit )
  [ $? = 0 ] || ( echo "Failed" )
)
2
  • Could you perhaps include some explanation of what this is and how it answers the question posed by OP? Commented Feb 26 at 14:28
  • 1
    @music2myear: I added an explanation. However, the comment posted by Tom Yan does provide a more through explanation. Commented Feb 26 at 15:03

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.