Last active
May 12, 2017 04:36
-
-
Save ben0x539/b3356519b949633292a73271f5f386f0 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env bash | |
################################################################################ | |
# possibly slightly more reasonable error handling in bash. | |
# ideally you'd like set -e to just abort your script whenever something | |
# goes unexpectedly wrong. there's a couple snags, tho. | |
# 1) set -e doesn't persist through command substitutions. | |
# you can use set -E ; trap exit ERR to mitigate that | |
# 2) echo "foo: $(foo)" succeeds even if foo fails. | |
# you can write x=$(foo); echo "foo: $x" to mitigate that. | |
# 3) local x=$(foo) always succeeds even if foo fails. | |
# that's why people write local x; x=$(foo) | |
# ok, I guess we could use trap 'kill $$' EXIT instead of hoping for | |
# propagation and just do without RETURN traps to clean up things, but... | |
# 4) if you are (dynamically if you're inside a function, not only | |
# lexically) inside the first operand to || or &&, or the control | |
# bits of a if/while/... kinda control structure, you never exit | |
# early on errors and the ERR handler never fires, and set -eE don't | |
# affect that anymore. | |
# That means error checking + error handling really don't compose very well. | |
# If you ever want to check whether something succeeds without exiting if it | |
# doesn't, you lose the ability to react to unexpected errors inside that | |
# thing. | |
# So, let's do the natural, obvious, intuitive thing, disable -e, and implement | |
# our own early-return logic on top of trap ... ERR, so that we never have to | |
# have problem 4) because we never call functions in that sorta position! | |
# The trap handler runs in a context that lets it return from the function that | |
# caused the error, and then the caller has to deal with our exit status, which | |
# just calls our trap handler again in the caller's context, so we get to | |
# return again, etc. | |
# To permit testing for expected errors, the ERR trap handler checks for a | |
# magical variable to conditionally skip error propagation. The variable gets | |
# set in a special wrapper function if it is about to exit unhappily, via | |
# RETURN trap handler). We will call other functions through that wrapper if | |
# we want to allow them to fail without taking down the whole script. | |
# So, given a function that might not always succeed, | |
# | |
# foo() { | |
# false | |
# true | |
# } | |
# instead of calling it in such a way that disables immediate exit on errors | |
# and consequently masks the exit status of false because it is followed by | |
# true, | |
# if ! foo; then | |
# echo >&2 "it went wrong!" | |
# fi | |
# we call it protected by our wrapper and explicitly check the exit status: | |
# try foo | |
# if [[ $? -ne 0 ]]; then | |
# echo >&2 "it went wrong (but in a good way)!" | |
# fi | |
# Hooray! | |
# Then we just gotta be careful to not use process subsitution except in lines | |
# that are only variable assignments to avoid those other pitfalls and I think | |
# we're good. | |
# Commands that aren't functions but other executables or shell builtins can | |
# still be used in front of && or || or as conditions etc because they aren't | |
# gonna execute more of our own shell functions that care about whether the | |
# ERR handler is respected. | |
################################################################################ | |
# The scaffolding | |
set -E | |
set -e # only for syntax errors while we're defining functions | |
trap ' | |
__exit_status="$?" | |
if [[ -v __STOP_UNWINDING ]]; then | |
unset __STOP_UNWINDING | |
unset __UNWINDING | |
elif [[ -v FUNCNAME ]]; then | |
__UNWINDING=1 | |
return "$__exit_status" | |
else | |
__UNWINDING=1 | |
exit "$__exit_status" | |
fi | |
' ERR | |
trap ' | |
if [[ -v __UNWINDING ]]; then | |
echo >&2 "*** exiting after unwinding" | |
fi | |
' EXIT # optional I guess | |
# "public interface" | |
try() { | |
trap ' | |
if [[ "$?" -ne 0 ]]; then | |
__STOP_UNWINDING=1 | |
fi | |
trap - RETURN | |
' RETURN | |
"$@" | |
} | |
# examples | |
counter=42 | |
fail() { | |
counter=$(( counter + 1 )) | |
echo >&2 "failing with status $counter" | |
return $counter | |
} | |
foo() { | |
try fail | |
echo "status after try fail: $?" | |
fail | |
# plain fail causes us to return early | |
echo "this won't print" | |
} | |
main() { | |
try foo | |
# we get foo's exit status but don't unwind further | |
echo "status after try foo: $?" | |
try true | |
# nothing to see here | |
echo "status after try true: $?" | |
try try try try fail | |
echo "status after try try try try fail: $?" | |
} | |
# disable -e so we can trap all errors instead of exiting. ideally we're done | |
# with potential syntax errors now. | |
set +e | |
main | |
echo "successful exit" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment