I have a simple shell script with the following preamble:
#!/usr/bin/env bash
set -eu
set -o pipefail
I also have the following function:
foo() {
printf "Foo working... "
echo "Failed!"
false # point of interest #1
true # point of interest #2
}
Executing foo()
as a regular command works as expected:
The script exits at #1
, because the return code of false
is non-zero and we use set -e
.
My goal is to capture the output of the function foo()
in a variable, and only print it in case an error occurs during the execution of foo()
.
This is what I've come up with:
printf "Doing something that could fail... "
if a="$(foo 2>&1)"; then
echo "Success!"
else
code=$?
echo "Error:"
printf "${a}"
exit $code
fi
The script doesn't exit at #1
and the "Success!"
path of the if
statement is executed.
Commenting out the true
at #2
causes the "Error:"
path of the if
statement to be executed.
It seems like bash just ignores set -e
inside the substitution and the if
statement is simply checking the return code of the last command in foo()
.
Q: What causes this weird behaviour?
A: This is just how bash works, it's normal behaviour
Q: Is there any way to make bash respect set -e
inside a command substitution and make this work correctly?
A: You shouldn't use set -e
for this purpose
Q: How would you go about implementing this without set -e
(i.e. print the output of a function only if something went wrong while executing it)?
A: See accepted answer and my "final thoughts" section.
I am using:
GNU bash, version 5.0.11(1)-release (x86_64-apple-darwin18.6.0)
Final thoughts / takeaway (might be useful for someone else):
Beware that using if ...; then
, or even && ... || ...
will disable most kinds of "traditional" bash error handling methods (this includes set -e
and trap ... ERR
+ set -o errtrace
) by design.
If you want to do something like I did, you probably should check the return codes inside your function manually and return a non-null exit code by hand (dangerous_command || return 1
) to avoid continuing execution on errors (you can do this whether you use set -e
or not).
As answered, set -e
does not propagate inside command substitutions by design.
If you wish to implement error handling logic which does, you can use trap ... ERR
in combination with set -o errtrace
, which will work with functions running inside command substitutions (that is unless you put them inside an if
statement, which will disable trap ... ERR
as well, so in this case manual return code checking is your only option if you wish to stop your function on errors).
If you think about it, this whole behaviour kind of makes sense: you wouldn't expect your script to terminate on a command "guarded" by an if
statement, as the whole point of your if
statement is checking whether the command succeeds or not.
Personally I still wouldn't go as far as avoiding set -e
and trap ... ERR
entirely as they can be really useful, but understanding how they behave in different circumstances is important, because they are no silver bullet either.
Q: How would you go about implementing this without
set -e
(i.e. print the output of a function only if something went wrong while executing it)?
You may use this way by checking return value of the function:
#!/usr/bin/env bash
foo() {
local n=$RANDOM
echo "Foo working with random=$n ..."
(($n % 2))
}
echo "Doing something that could fail..."
a="$(foo 2>&1)"
code=$?
if (($code == 0)); then
echo "Success!"
else
printf '{"ErrorCode": %d, "ErrorMessage": "%s"}\n' $code "$a"
exit $code
fi
Now run it as:
$> ./errScript.sh
Doing something that could fail...
Success!
$> ./errScript.sh
Doing something that could fail...
{"ErrorCode": 1, "ErrorMessage": "Foo working with random=27662 ..."}
$> ./errScript.sh
Doing something that could fail...
Success!
$> ./errScript.sh
Doing something that could fail...
{"ErrorCode": 1, "ErrorMessage": "Foo working with random=31864 ..."}
This dummy function code returns failure if $RANDOM
is even number and success for $RANDOM
being odd number.
You need to enable set -e
in command substitution as well:
#!/usr/bin/env bash
set -eu
set -o pipefail
foo() {
printf "Foo working... "
echo "Failed!"
false # point of interest #1
true # point of interest #2
}
printf "Doing something that could fail... "
a="$(set -e; foo)"
code=$?
if (($code == 0)); then
echo "Success!"
else
echo "Error:"
printf "${a}"
exit $code
fi
Then use it as:
./errScript.sh; echo $?
Doing something that could fail... 1
However do note that using set -e
is not ideal in shell scripts and it may fail to exit script in many scenarios.