Search code examples
bashshellexit-code

Bash set -e not exiting immediately with pipefail


set -eo pipefail

commandThatFails || exitFunction

exitFunction

So this script is running the exitMethod twice... I thought set -e exited immediately on any non zero exit code and set -o pipefail made sure that during the pipeline any failure is the final exit status code not the most recent command?

Therefore I thought :

  1. commandThatFails
  2. Executes exitFunction
  3. set -o pipefail returns non zero exit code as the first command fails
  4. Non zero exit code is detected by set -e and exits immediately

In the docs it states:

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 !. If a compound command other than a subshell returns a non-zero status because a command failed while -e was being ignored, the shell does not exit. A trap on ERR, if set, is executed before the shell exits.

I thought that the exitfunction is the command following the final || so therefore would be counted and picked up and exited immediately.

I can resolve the issue with:

commandThatFails || { exitFunction; exit 1; }

but it doesn't seem like the more elegant way to handle this, any thoughts appreciated!


Solution

  • || is a flow control operator, not a pipeline component. pipefail has no effect on it.

    If set -e caused flow control operators to exit, then you could never have an else branch of your script run with it active; it would be completely useless.

    For that reason, && and || suppress set -e behavior for their left-hand sides, just like if condition; then success; else fail; fi suppresses that behavior for condition.


    General practice is to make your exitFunction actually call exit, so you can write:

    die() {
      rc=$?
      (( $# )) && printf '%s\n' "$*" >&2
      exit "$(( rc == 0 ? 1 : rc ))"
    }
    
    commandThatFails || die "commandThatFails failed"
    

    ...but if you want to call something else first, yes, you need to use a grouping, just as you did in the question.