简体   繁体   中英

Bash subshell errexit semantics

I have errexit (and pipefail) enabled for my shell script, because that's the behaviour I usually want. However, occasionally I want to capture errors and handle them a specific way.

I know that errexit is disabled for commands that contain boolean operators or are to be used as a condition (if, while etc.)

eg

git push && true
echo "Pushed: $?"

will echo "Pushed: 0" on success, or "Pushed: something else " on failure.

However, what if I want a subshell to have errexit enabled, but then I wish to capture the exit code of this subshell?

For example:

#!/usr/bin/env bash
set -o errexit

(
    git push
    echo "Hai"
) && true

echo "Did it work: $?"

The problem is, bash sees the && boolean operator and disables errexit for the subshell. This means that "Hai" is always echo'd. That's not desirable.

How do enable errexit in this subshell, and capture the status code of the subshell without letting that exit code terminate the outer shell without constantly enabling and disabling errexit all over the place ?

Update

I have a strong feeling the solution is to use traps and capture the exit signal. Feel free to provide an answer before I self-answer .

It appears I stumbled upon a point of contention for many shell aficionados:

http://austingroupbugs.net/view.php?id=537#bugnotes

Basically, the standard said something, interpreters ignored it because the standard seemed illogical, but now interpreters like Bash have really confusing semantics, and no-one wants to fix it.

Unfortunately, trap <blah> EXIT can't be used to do what I want, because trap is basically just an interrupt handler for the signal, there is no way to continue execution of the script at a predetermined point (as you would using a try..finally block in other languages).

Everything is awful

So essentially, to my knowledge, there is absolutely no sane way to perform error handling. Your options are:

#!/usr/bin/env bash
set -e

# Some other code

set +e
(
    git push || exit $?
    echo "Hai"
)
echo "Did it work: $?"
set -e

or:

#!/usr/bin/env bash
set -e

(
    git push &&
    echo "Hai" ||
    exit $?
) && true

echo "Did it work: $?"

Sort of makes you wonder why you bothered with set -e in the first place!

You could do some hacking with output parsing. Command substitution does not inherit errexit (except on Bash 4.4 with inherit_errexit ) but it does inherit a ERR trap with errtrace . So you can use the trap to exit the subshell on error and use local or some other means to avoid exiting the parent shell.

handle_error() {
    local exit_code=$1 && shift
    echo -e "\nHANDLE_ERROR\t$exit_code"
    exit $exit_code
}

return_code() {
    # need to modify if not GNU head/tail
    local output="$(echo "$1" | head -n -1)"
    local result="$(echo "$1" | tail -1)"
    if [[ $result =~ HANDLE_ERROR\  [0-9]+ ]]; then
        echo "$output"
        return $(echo "$result" | cut -f2)
    else
        echo "$1"
        return 0
    fi
}

set -o errtrace
trap 'handle_error $?' ERR

main() {
    local output="$(echo "output before"; echo "running command"; false; echo "Hai")"
    return_code "$output" && true
    echo "Did it work: $?"
}

main

Unfortunately in my tests using && true with the command substitution prevents the trap from working (even with command grouping), so you cannot fold this into a single line. If you want to do that, then you can make handle_error set a global variable instead of return the exit status. You then get:

    return_code "$(echo "output before"; echo "running command"; false; echo "Hai")"
    echo "Did it work: $global_last_error"

Note also that command substitution swallows trailing newlines , so currently this code will add a newline to the output of the subshell if there wasn't one there originally.

This might not be 100% robust but may be acceptable to unburden you from switching the errexit flag repeatedly. Maybe there is a way to exploit the same pattern without the parsing?

Using the Bash set command changes options for the current shell. Changes to shell options are NOT inherited by a subshell. The reason being that the script author may want to change the environment options for the subshell!

These examples exit to the parent shell without printing 'hai'

( set -e; git push; echo 'hai' )

Same:

( set -e; git push; printf '\nhai' )

Same:

( set -e
git push
printf '\nhai'
)

Creating a Compond Command with the Operators '&& or ||' following a subshell keeps the subshell open until all commands resolve.

Use these two commands to find bash manual section quoted:

man bash
/errexit

Exit immediately if a pipeline (which may consist of a single simple command), a list, or a compound command (see SHELL GRAMMAR above), exits with a non-zero status. 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 following the if or elif reserved words, 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 value 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. This option applies to the shell environment and each subshell environment separately (see COMMAND EXECUTION ENVIRONMENT above), and may cause subshells to exit before executing all the commands in the subshell.

This part of the bash manual mentions it again:

The ERR trap is not executed if the failed command is part of the command list immediately following a while or until keyword, part of the test in an if statement, part of a 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 value is being inverted using !. These are the same conditions obeyed by the errexit (-e) option.

Finally, this section of the Bash manual indicates set -e is inherited by subshells of POSIX enabled Bash:

Subshells spawned to execute command substitutions inherit the value of the -e option from the parent shell. When not in posix mode, bash clears the -e option in such subshells.

Possibly, a bug: https://groups.google.com/forum/?fromgroups=#!topic/gnu.bash.bug/NCK_0GmIv2M This says, set -e in (subshells) should be independent of surrounding context. The man page says "[set -e] applies to the shell environment and each subshell environment separately", but actually set -e is prevented from working in a (subshell) if it is disabled in the surrounding context.

I use this code

function runWithOwnErrorHandling {

  functionName="$1"
  shift

  set +o errexit
  OLDTRAP="$(trap -p ERR)"
  trap - ERR
  (
    set -o errexit

    $functionName "$@"
  )
  FUNCEXIT=$?
  set -o errexit
  $OLDTRAP
}

Call it with

function someFunc {
  echo "c1"; false
  echo "c2";
}

runWithOwnErrorHandling someFunc
[[ ${FUNCEXIT} -ne 0 ]] && echo someFunc failed

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM