This is all perfectly understandable if we step through slowly.
Some more logging is required,
so run bash with the -x
parameter,
which will echo commands just before bash executes them,
prefixed by +
.
First run
$ bash -x sandbox.sh; echo $?
+ set -eu -o pipefail -E
++ func1
++ echo FUNC1
++ exit 1
+ var=FUNC1
1
-e
says this shell will exit immediately a command returns non-zero.
Crucially though, you run func1
in a subshell (using $(
)
).
The trace above shows this fact by using two +
s as the prefix (++
).
- The subshell spits out
FUNC1
on stdout, and then exits with return code 1.
- Note:
-e
is off inside this subshell. The reason the subshell quit was due to the exit
command, not -e
. You can't really tell this due to the way func1
is written.
- Back in the first shell, we assign
FUNC1
to the variable var. However, the exit code of this assignment command is the exit code of the last command substitution. Bash sees this failure (i.e., non-zero exit code), and quits.
To quote the manual's SIMPLE COMMAND EXPANSION section:
If one of the expansions contained a command substitution, the exit status of the command is the exit status of the last command substitution performed.
Second run
Exactly the same explanation as the first run.
We note again that the -e
is not in effect inside the subshell.
This time however,
there is a material difference — we get a clearer view of what is happening.
- The exit code of
func2
is the exit code of its last command
- That
echo
always succeeds.
func2
always succeeds
- The assignment always succeeds.
-e
has no effect.
shopt -s inherit_errexit
?
This will turn on -e
in subshells.
It is however a difficult bedfellow.
It does not guarantee we assert when a command fails.
Consider this:
set -e
shopt -s inherit_errexit
f() { echo a; (exit 22); echo b; }
echo "f says [$(f)] $?"
echo byee
This time the command substitution is part of an echo
, rather than an assignment, and we get
+ set -e
+ shopt -s inherit_errexit
++ f
++ echo a
++ exit 22
+ echo 'f says [a] 22'
f says [a] 22
+ echo byee
byee
- The subshell sees a command that fails with exit code 22. Since
-e
is in effect, the shell exits with code 22 (echo b
does not execute).
- Back in the first shell,
echo
gets a
as the output of f
, and 22
as the exit code of the subshell
- Thing is, unlike an assignment, the exit code of the
echo
is zero.
Version
$ bash --version
GNU bash, version 5.0.17(1)-release (x86_64-redhat-linux-gnu)
Copyright (C) 2019 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software; you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
$(...)
command substitutions).set -e / -o errexit
is not inherited in subshells. Simpler example:bash -c 'set -e; echo $(false; echo survived)'
. If you had usedvar=$(set -e; func2)
in "The Line", the "shouldn't be reached" lines wouldn't have been reached.shopt -s inherit_errexit
at the beginning of your script (also enabled in posix mode). Notice thatset -e / errexit
is independent of theERR
trap and is not affected byset -E / errtrace
set -e
made my script work as intended. But it also made puzzled me a bit more. Why don't I need to addset -e
to the line that performs command substitution infunc2
?inherit_errexit
is on whenbash
is running assh
. I wouldn't say the idea is right, I'm not sure it could ever be implemented with a clear and consistent API. To me, it's better avoided, reserved for the most basic of scripts (i.e. with the real-life definition of script, when you just put in a file a plain sequence of commands run one after the other).