Home > database >  How to make errexit behaviour work in bash functions
How to make errexit behaviour work in bash functions

Time:02-06

How do I get -e / errexit to work in bash functions, so that the first failed command* within a function causes the function to return with an error code (just as -e works at top-level).

* not part of boolean expression, if/elif/while/etc etc etc

I ran the following test-script, and I expect any function containing f in its name (i.e. a false line in its source) to return error-code, but they don't if there's another command after. Even when I put the function body in a subshell with set -e specified again, it just blindly steamrolls on through the function after a command fails instead of exiting the subshell/function with the nonzero status code.

Environments / interpreters tested:

I get the same result in all of these envs/shells, which is to be expected I guess unless one was buggy.

Arch:

  • bash test.sh (bash 5.1)
  • zsh test.sh (zsh 5.8)
  • sh test.sh (just a symlink to bash)

Alpine:

  • docker run -v /test.sh:/test.sh alpine:latest sh /test.sh (BusyBox 1.34)

Ubuntu:

  • docker run -v /test.sh:/test.sh ubuntu:21.04 sh /test.sh
  • docker run -v /test.sh:/test.sh ubuntu:21.04 bash /test.sh (bash 5.1)
  • docker run -v /test.sh:/test.sh ubuntu:21.04 dash /test.sh (bash 5.1)

Test script

set -e

t() {
    true
}

f() {
    false
}

tf() {
    true
    false
}

ft() {
    false
    true
}

et() {
    set -e
    true
}

ef() {
    set -e
    false
}

etf() {
    set -e
    true
    false
}

eft() {
    set -e
    false
    true
}

st() {( set -e
    true
)}

sf() {( set -e
    false
)}

stf() {( set -e
    true
    false
)}

sft() {( set -e
    false
    true
)}

for test in t f tf ft _ et ef etf eft _ st sf stf sft; do
    if [ "$test" = '_' ]; then
        echo ""
    elif "$test"; then
        echo "$test: pass"
    else
        echo "$test: fail"
    fi
done

Output on my machine

t: pass
f: fail
tf: fail
ft: pass

et: pass
ef: fail
etf: fail
eft: pass

st: pass
sf: fail
stf: fail
sft: pass

Desired output

Without significantly changing source in functions themselves, i.e. not adding an if/then or || return to every line in the function.

t: pass
f: fail
tf: fail
ft: fail

et: pass
ef: fail
etf: fail
eft: fail

st: pass
sf: fail
stf: fail
sft: fail

Or at least pass/fail/fail/fail in one group, so I can use that approach for writing robust functions.

With accepted solution

With the accepted solution, the first four tests give the desired result. The other two groups don't, but are irrelevant since those were my own attempts to work around the issue. The root cause is that the "don't errexit the script when if/while-condition fails" behaviour propagates into any functions called in the condition, rather than just applying to the end-result.

CodePudding user response:

How to make errexit behaviour work in bash functions

Call the function not inside if.

I expect any function containing f in its name (i.e. a false line in its source) to return error-code

Weeeeeell, your expectancy does not match reality. It is not how it is implemented. And flag errexit will exit your script, not return error-code.

Desired output

You can:

  • Download Bash or other shell sources or write your own and implement the behavior that you want to have.

    • consider extending the behavior with like shopt -s errexit_but_return_nonzero_in_if_contexts or something shorter
  • Run it in a separate shell.

    elif bash -ec "$(declare -f "$test"); $test"; then
    
  • Write a custom loadable to "clear" CMD_IGNORE_RETURN flag when Bash enters contexts in which set -e should be ignored. I think for that static COMMAND *currently_executing_command; needs to be extern and then just currently_executing_command.flags &= ~CMD_IGNORE_RETURN;.

  • You can do a wrapper where you temporarily disable errexit and get return status, but you have to call it outside of if:

errreturn() {
    declare -g ERRRETURN
    local flags=$(set  o)
    set  e
    (
        set -e
        "$@"
    )
    ERRRETURN=$?
    eval "$flags"
}

....
        echo ""
    else
        errreturn "$test"
        if (( ERRRETURN == 0 )); then
            echo "$test: pass"
        else
            echo "$test: fail"
        fi
    fi
  •  Tags:  
  • Related