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.
- shell options: https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html
errexit
(-e
): exit on non-zero resulterrtrace
(-E
): inheritERR
trap
- additional shell option: https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html
inherit_errexit
: inheriterrexit
setting during command substitution
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
-
The
-e
/errexit
shell option and theERR
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:
-
Neither the
-e
/errexit
shell option nor theERR
trap is inherited if used inside a command substitution!- To inherit the
-e
/errexit
shell option use theinherit_errexit
shell option. - To inherit the
ERR
trap use the-E
/errtrace
shell option.
- To inherit the
-
Neither the
-e
/errexit shell option nor theERR
trap will trigger if- the failed command is part of the command list immediately following an
until
orwhile
keyword,
- part of the test following the
if
orelif
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
!
.
- the failed command is part of the command list immediately following an
-
To make the
-e
/errexit
shell option and theERR
trap work reliably you will likely have to change you code style in order to avoid all the pitfalls described in 3).
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
- The code sample below will output something like this: