Skip to content

Instantly share code, notes, and snippets.

@bkahlert
Last active August 10, 2023 17:52
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bkahlert/08f9ec3b8453db5824a0aa3df6a24cb4 to your computer and use it in GitHub Desktop.
Save bkahlert/08f9ec3b8453db5824a0aa3df6a24cb4 to your computer and use it in GitHub Desktop.
Bash Error Handling — shell options -e/errexit, inherit_errexit, -E/errtrace and the ERR trap

Bash Error Handling — shell options -e/errexit, inherit_errexit, -E/errtrace and the ERR trap

This Markdown file is 90% the output of error_handling.bash.

I wrote the script / this document to describe the shell options errexit, inherit_errexit, and errtrace as well as the ERR trap and their conditions and interactions in more detail as this is what I always failed to understand with the pieces of information I found so far.

References

Shell options and trap in action

The remainder of what you will see is the execution of the command group
{ false; return 0; } in different variations and with different shell options enabled.

You can think of this group as function foo. It's basically the same but was less suitable for reasons of presentation.

Each of the following lines shows:

  • optional list of enabled shell options during invocation
  • invoked shell script
  • exit status/code of the invocation
                                                               {         false; return 0; }            ↩ 0
                                                               {         false; return 0; }  && true   ↩ 0
                                                               { set -e; false; return 0; }            ↩ 1
                                                               { set -e; false; return 0; }  && true   ↩ 0
                 -errexit                                      {         false; return 0; }            ↩ 1
                 -errexit                                      {         false; return 0; }  && true   ↩ 0
                 -errexit                                      { set -e; false; return 0; }            ↩ 1
                 -errexit                                      { set -e; false; return 0; }  && true   ↩ 0

The same invocations but this time command substituted:
The only difference: out="$(fail)" no longer fails which can be "healed" using the inherit_errexit option.

                                                 out=$(        {         false; return 0; })           ↩ 0
                                                 out=$(        {         false; return 0; }) && true   ↩ 0
                                                 out=$(set -e; { set -e; false; return 0; })           ↩ 1
                                                 out=$(set -e; { set -e; false; return 0; }) && true   ↩ 0
                 -errexit                        out=$(        {         false; return 0; })           ↩ 0
                 -errexit                        out=$(        {         false; return 0; }) && true   ↩ 0
                 -errexit                        out=$(set -e; { set -e; false; return 0; })           ↩ 1
                 -errexit                        out=$(set -e; { set -e; false; return 0; }) && true   ↩ 0
-inherit_errexit -errexit                        out=$(        {         false; return 0; })           ↩ 1
-inherit_errexit -errexit                        out=$(        {         false; return 0; }) && true   ↩ 0
-inherit_errexit -errexit                        out=$(set -e; { set -e; false; return 0; })           ↩ 1
-inherit_errexit -errexit                        out=$(set -e; { set -e; false; return 0; }) && true   ↩ 0

The same restrictions apply to the ERR trap/signal, which

[...] is not executed if the failed command is part of the command list immediately [...]
These are the same conditions obeyed by the errexit (-e) option."
-- https://www.gnu.org/software/bash/manual/html_node/Bourne-Shell-Builtins.html#Bourne-Shell-Builtins

                                                               {         false; return 0; }            ↩ 0
                                                               {         false; return 0; }  && true   ↩ 0
                            trap "exit \$?" ERR;               {         false; return 0; }            ↩ 1
                            trap "exit \$?" ERR;               {         false; return 0; }  && true   ↩ 0

The ERR trap is not inherited by default. This can be changed with the errtrace option:

                                                 out=$(        {         false; return 0; })           ↩ 0
                                                 out=$(        {         false; return 0; }) && true   ↩ 0
                            trap "exit \$?" ERR; out=$(        {         false; return 0; })           ↩ 0
                            trap "exit \$?" ERR; out=$(        {         false; return 0; }) && true   ↩ 0
                -errtrace                        out=$(        {         false; return 0; })           ↩ 0
                -errtrace                        out=$(        {         false; return 0; }) && true   ↩ 0
                -errtrace   trap "exit \$?" ERR; out=$(        {         false; return 0; })           ↩ 1
                -errtrace   trap "exit \$?" ERR; out=$(        {         false; return 0; }) && true   ↩ 0

Summary

  1. The -e/errexit shell option and the ERR trap will trigger whenever

    • a pipeline (which may consist of a single simple command),
    • a list, or
    • a compound command returns a non-zero exit status, BUT:
  2. Neither the -e/errexit shell option nor the ERR trap is inherited if used inside a command substitution!

    • To inherit the -e/errexit shell option use the inherit_errexit shell option.
    • To inherit the ERR trap use the -E/errtrace shell option.
  3. Neither the -e/errexit shell option nor the ERR trap will trigger if

    • the failed command is part of the command list immediately following an
      • until or
      • while keyword,
    • part of the test following the
      • if or
      • elif reserved words,
    • part of a command executed in a
      • && or
      • || list (as demonstrated in the examples)
      • 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 using !.
  4. To make the -e/errexit shell option and the ERR trap work reliably you will likely have to change you code style in order to avoid all the pitfalls described in 3).

My Advice

Use the -e/errexit shell option only to improve your error handling (preferably together with inherit_errexit).

  • Do not rely on it.
  • It easily fails in unintuitive situations where you would expect it to work (true && call works; call && true does not).
  • Your script has to work without. The moment someone uses your script you have little control on the effective -e/errexit setting.

Use the ERR trap for more fine-grained error handling (preferably together with -E/errtrace).

  • Do not rely on it.
  • Improve the default logging to track down sources of errors more easily.
    • The code sample below will output something like this:
       ✘ error-handling.bash: false ↩ 1
           at main(/home/bkahlert/error-handling.bash:165)
      
      handle_error() {
        local esc_red esc_reset; esc_red=$(tput setaf 1 || true); esc_reset=$(tput sgr0 || true)
        printf " %s %s: %s %s\n     at %s\n" "${esc_red-}${esc_reset-}" "${0##*/}" "$2" "${esc_red-}$1${esc_reset-}" "$3" >&2
        exit "$1"
      }
      
      trap 'handle_error "$?" "${BASH_COMMAND:-?}" "${FUNCNAME[0]:-main}(${BASH_SOURCE[0]:-?}:${LINENO:-?})"' ERR
#!/usr/bin/env bash
set -uo pipefail
# Enables the specified options. Prefix an option with + to disable.
# Prefixing options with `-` is optional.
set_opt() {
local opt set_op shopt_op
while (($#)); do
opt=$1 set_op=- shopt_op=s && shift
[ ! "${opt:0:1}" = "-" ] || opt=${opt:1}
[ ! "${opt:0:1}" = "+" ] || opt=${opt:1} set_op=+ shopt_op=u
set "${set_op}o" "$opt" 2>/dev/null || shopt "-q${shopt_op}" "$opt" 2>/dev/null || exit 99
done
}
# Runs the contents of STDIN with the specified shell options and prints a summary.
execute() {
local script status color=2
script=$(cat -) || exit 100
(
set_opt +errexit +inherit_errexit "$@"
eval "$script"
) >&2
status=$?
[ "$status" -eq 0 ] || color=1
printf "%s%*s%s %s %s↩ %d%s\n" \
"$(tput setaf 8)" 25 "$*" "$(tput sgr0)" \
"${script//$'\n'/; }" \
"$(tput setaf "$color")" "$status" "$(tput sgr0)"
}
# shellcheck disable=SC2059
execute_pattern() {
[ $# -ge 3 ] || exit 102
local invocation
printf -v invocation '{ %sfalse; return 0; }' "$1" && shift
while [[ "${1-}" == *%s* ]]; do
printf -v invocation "$1" "$invocation" && shift
done
printf "%s " "$invocation" | execute "$@"
printf "%s && true" "$invocation" | execute "$@"
}
echo 'Reference:
shell options: https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html
errexit (-e): exit on non-zero result
errtrace (-E): inherit ERR trap
additional shell option: https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html
inherit_errexit: inherit errexit setting during command substitution
'
echo 'The remainder of what you will see is the execution of the command group
{ false; return 0; } in different variations and with different shell options enabled.
Each of the following lines shows:
- optional list of enabled shell options during invocation
- invoked shell script
- exit status/code of the invocation
'
execute_pattern ' ' ' %s ' ' %s'
execute_pattern 'set -e; ' ' %s ' ' %s'
execute_pattern ' ' ' %s ' ' %s' -errexit
execute_pattern 'set -e; ' ' %s ' ' %s' -errexit
echo '
The same invocations but this time command substituted:
The only difference: out="$(fail)" no longer fails
which can be "healed" using the inherit_errexit option.
'
execute_pattern ' ' 'out=$( %s)' ' %s'
execute_pattern 'set -e; ' 'out=$(set -e; %s)' ' %s'
execute_pattern ' ' 'out=$( %s)' ' %s' -errexit
execute_pattern 'set -e; ' 'out=$(set -e; %s)' ' %s' -errexit
execute_pattern ' ' 'out=$( %s)' ' %s' -inherit_errexit -errexit
execute_pattern 'set -e; ' 'out=$(set -e; %s)' ' %s' -inherit_errexit -errexit
echo '
The same restrictions apply to the ERR trap/signal, which
"[...] is not executed if the failed command is part of the command list immediately [...]
These are the same conditions obeyed by the errexit (-e) option."
- https://www.gnu.org/software/bash/manual/html_node/Bourne-Shell-Builtins.html#Bourne-Shell-Builtins
'
execute_pattern ' ' ' %s ' ' %s'
execute_pattern ' ' ' %s ' 'trap "exit \$?" ERR; %s'
echo '
The ERR trap is not inherited by default. This can be changed with the errtrace option:
'
execute_pattern ' ' 'out=$( %s)' ' %s'
execute_pattern ' ' 'out=$( %s)' 'trap "exit \$?" ERR; %s'
execute_pattern ' ' 'out=$( %s)' ' %s' -errtrace
execute_pattern ' ' 'out=$( %s)' 'trap "exit \$?" ERR; %s' -errtrace
echo '
To wrap it up:
1) The -e/errexit shell option and
the ERR trap will trigger whenever
- a pipeline (which may consist of a single simple command),
- a list, or
- a compound command
returns a non-zero exit status, BUT:
2) Neither the -e/errexit shell option
nor the ERR trap is inherited if used inside a command substitution!
- To inherit the -e/errexit shell option use the inherit_errexit shell option.
- To inherit the ERR trap use the -E/errtrace shell option.
3) Neither the -e/errexit shell option
nor the ERR trap will trigger if
- the failed command is part of the command list immediately following an
- until or
- while keyword,
- part of the test following the
- if or
- elif reserved words,
- part of a command executed in a
- && or
- || list (as demonstrated in the examples)
- 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 using !.
4) To make the -e/errexit shell option
and the ERR trap work reliably
you will likely have to change you code style
in order to avoid all of the pitfalls described in 3).
'
echo '
My advice:
Use the -e/errexit shell option only to improve your error handling
(preferably together with inherit_errexit).
- Do not rely on it.
- It easily fails in unintuitive situations where you would expect it to work
("true && call" works; "call && true" does not).
- Your script has to work without. The moment someone uses your script
you have little control on the effective -e/errexit setting.
Use the ERR trap for more fine-grained error handling
(preferably together with -E/errtrace).
- Do not rely on it.
- Improve the default logging to track down sources of errors more easily.
- The code sample below will output something like this:
✘ error-handling.bash: false ↩ 1
at main(/home/bkahlert/error-handling.bash:165)
'
handle_error() {
local esc_red esc_reset; esc_red=$(tput setaf 1 || true); esc_reset=$(tput sgr0 || true);
printf " %s %s: %s %s\n at %s\n" "${esc_red-}✘${esc_reset-}" "${0##*/}" "$2" "${esc_red-}↩ $1${esc_reset-}" "$3" >&2
exit "$1"
}
trap 'handle_error "$?" "${BASH_COMMAND:-?}" "${FUNCNAME[0]:-main}(${BASH_SOURCE[0]:-?}:${LINENO:-?})"' ERR
false
exit 0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment