How do I get the effect and usefulness of "set -e" inside a shell function?

ShellError HandlingSh

Shell Problem Overview


set -e (or a script starting with #!/bin/sh -e) is extremely useful to automatically bomb out if there is a problem. It saves me having to error check every single command that might fail.

How do I get the equivalent of this inside a function?

For example, I have the following script that exits immediately on error with an error exit status:

#!/bin/sh -e

echo "the following command could fail:"
false
echo "this is after the command that fails"

The output is as expected:

the following command could fail:

Now I'd like to wrap this into a function:

#!/bin/sh -e

my_function() {
    echo "the following command could fail:"
    false
    echo "this is after the command that fails"
}

if ! my_function; then
    echo "dealing with the problem"
fi

echo "run this all the time regardless of the success of my_function"

Expected output:

the following command could fail:
dealing with the problem
run this all the time regardless of the success of my_function

Actual output:

the following output could fail:
this is after the command that fails
run this all the time regardless of the success of my_function

(ie. the function is ignoring set -e)

This presumably is expected behaviour. My question is: how do I get the effect and usefulness of set -e inside a shell function? I'd like to be able to set something up such that I don't have to individually error check every call, but the script will stop on encountering an error. It should unwind the stack as far as is needed until I do check the result, or exit the script itself if I haven't checked it. This is what set -e does already, except it doesn't nest.

I've found the same question asked outside Stack Overflow but no suitable answer.

Shell Solutions


Solution 1 - Shell

From documentation of set -e:

> When this option is on, if a simple command fails for any of the > reasons listed in Consequences of > Shell Errors or returns an exit status > value > 0, and is not part of the > compound list following a while, > until, or if keyword, and is not a > part of an AND or OR list, and is not > a pipeline preceded by the ! reserved > word, then the shell shall immediately > exit.

In your case, false is a part of a pipeline preceded by ! and a part of if. So the solution is to rewrite your code so that it isn't.

In other words, there's nothing special about functions here. Try:

set -e
! { false; echo hi; }

Solution 2 - Shell

I eventually went with this, which apparently works. I tried the export method at first, but then found that I needed to export every global (constant) variable the script uses.

Disable set -e, then run the function call inside a subshell that has set -e enabled. Save the exit status of the subshell in a variable, re-enable set -e, then test the var.

f() { echo "a"; false;  echo "Should NOT get HERE"; }

# Don't pipe the subshell into anything or we won't be able to see its exit status
set +e ; ( set -e; f ) ; err_status=$?
set -e

## cleaner syntax which POSIX sh doesn't support.  Use bash/zsh/ksh/other fancy shells
if ((err_status)) ; then
    echo "f returned false: $err_status"
fi

## POSIX-sh features only (e.g. dash, /bin/sh)
if test "$err_status" -ne 0 ; then
    echo "f returned false: $err_status"
fi

echo "always print this"

You can't run f as part of a pipeline, or as part of a && of || command list (except as the last command in the pipe or list), or as the condition in an if or while, or other contexts that ignore set -e. This code also can't be in any of those contexts, so if you use this in a function, callers have to use the same subshell / save-exit-status trickery. This use of set -e for semantics similar to throwing/catching exceptions is not really suitable for general use, given the limitations and hard-to-read syntax.

trap err_handler_function ERR has the same limitations as set -e, in that it won't fire for errors in contexts where set -e won't exit on failed commands.

You might think the following would work, but it doesn't:

if ! ( set -e; f );then    ##### doesn't work, f runs ignoring -e
    echo "f returned false: $?"
fi

set -e doesn't take effect inside the subshell because it remembers that it's inside the condition of an if. I thought being a subshell would change that, but only being in a separate file and running a whole separate shell on it would work.

Solution 3 - Shell

You may directly use a subshell as your function definition and set it to exit immediately with set -e. This would limit the scope of set -e to the function subshell only and would later avoid switching between set +e and set -e.

In addition, you can use a variable assignment in the if test and then echo the result in an additional else statement.

# use subshell for function definition
f() (
   set -exo pipefail
   echo a
   false
   echo Should NOT get HERE
   exit 0
)

# next line also works for non-subshell function given by agsamek above
#if ret="$( set -e && f )" ; then 
if ret="$( f )" ; then
   true
else
   echo "$ret"
fi

# prints
# ++ echo a
# ++ false
# a

Solution 4 - Shell

This is a bit of a kludge, but you can do:

export -f f
if sh -ec f; then
...

This will work if your shell supports export -f (bash does).

Note that this will not terminate the script. The echo after the false in f will not execute, nor will the body of the if, but statements after the if will be executed.

If you are using a shell that does not support export -f, you can get the semantics you want by running sh in the function:

f() { sh -ec '
echo This will execute
false
echo This will not
'
}

Solution 5 - Shell

Note/Edit: As a commenter pointed out, this answer uses bash, and not sh like the OP used in his question. I missed that detail when I originaly posted an answer. I will leave this answer up anyway since it might be interested to some passerby.

Y'aaaaaaaaaaaaaaaaaaallll ready for this?

Here's a way to do it with leveraging the DEBUG trap, which runs before each command, and sort of makes errors like the whole exception/try/catch idioms from other languages. Take a look. I've made your example one more 'call' deep.

#!/bin/bash

# Get rid of that disgusting set -e.  We don't need it anymore!
# functrace allows RETURN and DEBUG traps to be inherited by each
# subshell and function.  Plus, it doesn't suffer from that horrible
# erasure problem that -e and -E suffer from when the command 
# is used in a conditional expression.
set -o functrace

# A trap to bubble up the error unless our magic command is encountered 
# ('catch=$?' in this case) at which point it stops.  Also don't try to
# bubble the error if were not in a function.
trap '{ 
    code=$?
    if [[ $code != 0 ]] && [[ $BASH_COMMAND != '\''catch=$?'\'' ]]; then
        # If were in a function, return, else exit.
        [[ $FUNCNAME ]] && return $code || exit $code
    fi
}' DEBUG

my_function() {
    my_function2
}

my_function2() {
    echo "the following command could fail:"
    false
    echo "this is after the command that fails"
}

# the || isn't necessary, but the 'catch=$?' is.
my_function || catch=$?
echo "Dealing with the problem with errcode=$catch (⌐■_■)"

echo "run this all the time regardless of the success of my_function"

and the output:

the following command could fail:
Dealing with the problem with errcode=1 (⌐■_■)
run this all the time regardless of the success of my_function

I haven't tested this in the wild, but off the top of my head, there are a bunch of pros:

  1. It's actually not that slow. I've ran the script in a tight loop with and without the functrace option, and times are very close to each other under 10 000 iterations.

  2. You could expand on this DEBUG trap to print a stack trace without doing that whole looping over $FUNCNAME and $BASH_LINENO nonsense. You kinda get it for free (besides actually doing an echo line).

  3. Don't have to worry about that shopt -s inherit_errexit gotcha.

Solution 6 - Shell

Join all commands in your function with the && operator. It's not too much trouble and will give the result you want.

Solution 7 - Shell

This is by design and POSIX specification. We can read in man bash:

> If a compound command or shell function executes in a context where -e is being ignored, none of the commands executed within the compound command or function body will be affected by the -e setting, even if -e is set and a command returns a failure status. If a compound command or shell function sets -e while executing in a context where -e is ignored, that setting will not have any effect until the compound command or the command containing the function call completes.

therefore you should avoid relying on set -e within functions.

Given the following exampleAustin Group:

set -e
start() {
   some_server
   echo some_server started successfully
}
start || echo >&2 some_server failed

the set -e is ignored within the function, because the function is a command in an AND-OR list other than the last.

The above behaviour is specified and required by POSIX (see: Desired Action):

> The -e setting shall be ignored when executing the compound list following the while, until, if, or elif reserved word, a pipeline beginning with the ! reserved word, or any command of an AND-OR list other than the last.

Solution 8 - Shell

I know this isn't what you asked, but you may or may not be aware that the behavior you seek is built into "make". Any part of a "make" process that fails aborts the run. It's a wholly different way of "programming", though, than shell scripting.

Solution 9 - Shell

You will need to call your function in a sub shell (inside brackets ()) to achieve this.

I think you want to write your script like this:

#!/bin/sh -e

my_function() {
    echo "the following command could fail:"
    false
    echo "this is after the command that fails"
}

(my_function)

if [ $? -ne 0 ] ; then
    echo "dealing with the problem"
fi

echo "run this all the time regardless of the success of my_function"

Then the output is (as desired):

the following command could fail:
dealing with the problem
run this all the time regardless of the success of my_function

Solution 10 - Shell

If a subshell isn't an option (say you need to do something crazy like set a variable) then you can just check every single command that might fail and deal with it by appending || return $?. This causes the function to return the error code on failure.

It's ugly, but it works

#!/bin/sh
set -e

my_function() {
    echo "the following command could fail:"
    false || return $?
    echo "this is after the command that fails"
}

if ! my_function; then
    echo "dealing with the problem"
fi

echo "run this all the time regardless of the success of my_function"

gives

the following command could fail:
dealing with the problem
run this all the time regardless of the success of my_function

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionRobie BasakView Question on Stackoverflow
Solution 1 - ShellRoman CheplyakaView Answer on Stackoverflow
Solution 2 - ShellantakView Answer on Stackoverflow
Solution 3 - ShellphilzView Answer on Stackoverflow
Solution 4 - ShellWilliam PursellView Answer on Stackoverflow
Solution 5 - ShelljmrahView Answer on Stackoverflow
Solution 6 - ShellGintautas MiliauskasView Answer on Stackoverflow
Solution 7 - ShellkenorbView Answer on Stackoverflow
Solution 8 - Shelljcomeau_ictxView Answer on Stackoverflow
Solution 9 - ShelllotharView Answer on Stackoverflow
Solution 10 - ShellcobbalView Answer on Stackoverflow