I have errexit (and pipefail) enabled for my shell script, because that's the behaviour I usually want. However, occasionally I want to capture errors and handle them a specific way.
I know that errexit is disabled for commands that contain boolean operators or are to be used as a condition (if, while etc.)
e.g.
git push && true
echo "Pushed: $?"
will echo "Pushed: 0" on success, or "Pushed: something else" on failure.
However, what if I want a subshell to have errexit enabled, but then I wish to capture the exit code of this subshell?
For example:
#!/usr/bin/env bash
set -o errexit
(
git push
echo "Hai"
) && true
echo "Did it work: $?"
The problem is, bash sees the && boolean operator and disables errexit for the subshell. This means that "Hai" is always echo'd. That's not desirable.
How do enable errexit in this subshell, and capture the status code of the subshell without letting that exit code terminate the outer shell without constantly enabling and disabling errexit all over the place?
I have a strong feeling the solution is to use traps and capture the exit signal. Feel free to provide an answer before I self-answer.
set -o errexit ( equal to set -e) The first option set -o errexit means that if any of the commands in your code fails for any reason, the entire script fails. This is particularly useful when doing Continuous Delivery pipelines.
pipefail. If set, the return value of a pipeline is the value of the last (rightmost) command to exit with a non-zero status, or zero if all commands in the pipeline exit successfully. This option is disabled by default.
set allows you to change the values of shell options and set the positional parameters, or to display the names and values of shell variables.
It appears I stumbled upon a point of contention for many shell aficionados:
http://austingroupbugs.net/view.php?id=537#bugnotes
Basically, the standard said something, interpreters ignored it because the standard seemed illogical, but now interpreters like Bash have really confusing semantics, and no-one wants to fix it.
Unfortunately, trap <blah> EXIT
can't be used to do what I want, because trap
is basically just an interrupt handler for the signal, there is no way to continue execution of the script at a predetermined point (as you would using a try..finally
block in other languages).
So essentially, to my knowledge, there is absolutely no sane way to perform error handling. Your options are:
#!/usr/bin/env bash
set -e
# Some other code
set +e
(
git push || exit $?
echo "Hai"
)
echo "Did it work: $?"
set -e
or:
#!/usr/bin/env bash
set -e
(
git push &&
echo "Hai" ||
exit $?
) && true
echo "Did it work: $?"
Sort of makes you wonder why you bothered with set -e
in the first place!
You could do some hacking with output parsing. Command substitution does not inherit errexit
(except on Bash 4.4 with inherit_errexit
) but it does inherit a ERR
trap with errtrace
. So you can use the trap to exit the subshell on error and use local
or some other means to avoid exiting the parent shell.
handle_error() {
local exit_code=$1 && shift
echo -e "\nHANDLE_ERROR\t$exit_code"
exit $exit_code
}
return_code() {
# need to modify if not GNU head/tail
local output="$(echo "$1" | head -n -1)"
local result="$(echo "$1" | tail -1)"
if [[ $result =~ HANDLE_ERROR\ [0-9]+ ]]; then
echo "$output"
return $(echo "$result" | cut -f2)
else
echo "$1"
return 0
fi
}
set -o errtrace
trap 'handle_error $?' ERR
main() {
local output="$(echo "output before"; echo "running command"; false; echo "Hai")"
return_code "$output" && true
echo "Did it work: $?"
}
main
Unfortunately in my tests using && true
with the command substitution prevents the trap from working (even with command grouping), so you cannot fold this into a single line. If you want to do that, then you can make handle_error
set a global variable instead of return the exit status. You then get:
return_code "$(echo "output before"; echo "running command"; false; echo "Hai")"
echo "Did it work: $global_last_error"
Note also that command substitution swallows trailing newlines, so currently this code will add a newline to the output of the subshell if there wasn't one there originally.
This might not be 100% robust but may be acceptable to unburden you from switching the errexit
flag repeatedly. Maybe there is a way to exploit the same pattern without the parsing?
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With