Skip to content

Instantly share code, notes, and snippets.

@ben0x539
Last active May 12, 2017 04:36
Show Gist options
  • Save ben0x539/b3356519b949633292a73271f5f386f0 to your computer and use it in GitHub Desktop.
Save ben0x539/b3356519b949633292a73271f5f386f0 to your computer and use it in GitHub Desktop.
#!/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