Skip to content

Instantly share code, notes, and snippets.

@Alphadelta14
Created February 4, 2023 00:11
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Alphadelta14/0d9175767b406a6d3d402099f134d816 to your computer and use it in GitHub Desktop.
Save Alphadelta14/0d9175767b406a6d3d402099f134d816 to your computer and use it in GitHub Desktop.
Bash Traceback
#!/usr/bin/env bash
# exit on error (errexit)
set -e
# pass ERR trap to subshells (errtrace)
set -E
# Callback function for when set -e (ERREXIT) is triggered)
# This shows a small stacktrace for the current shell along with the failing
# function call.
function _show_traceback() {
# The very first line captures the return code in $?.
local rc=$?
# We pass "${BASH_SOURCE[0]}" and $LINENO (note: not $BASH_LINENO) from
# the trap so they refer to the failing call before this is run.
# Otherwise, they refer to this _show_traceback itself.
local fail_file=$1
local file_line=$2
# traceback locals
local line_idx;
local filename;
# FUNCNAME is an array of functions
local frame=${#FUNCNAME[@]}
# skip "main" frame
frame=$(( frame - 1 ))
_warning "Subcommand failed with code=$rc. Bash traceback:"
# Roughly an enumeration like::
# for frame_id, func in reversed(enumerate(FUNCNAME[1:])):
# filename = BASH_SOURCE[frame_id+1]
# line_idx = BASH_LINENO[frame_id]
while [ "$frame" -gt 1 ]; do
# BASH_SOURCE is +1 from BASH_LINENO and FUNCNAME index
filename="${BASH_SOURCE[$frame]:-"<script>"}"
# decrement after getting source, which is +1 of the rest
frame=$(( frame - 1 ))
line_idx="${BASH_LINENO[$frame]}"
func="${FUNCNAME[$frame]}"
_log "In $filename, line $line_idx:"
_log "[#$frame]\t$func"
done
# frame 0 is _show_traceback, so we instead show $1 and $2 from our trap
_log "In $fail_file, line $file_line:"
_error "\t$BASH_COMMAND"
_error "\t^-- returned $rc"
_log ""
return "$rc"
}
function _register_traceback() {
# Cause shell to exit whenever any command fails.
# Allows the ERR trap to be called before exit. (errexit)
set -e
# Ensure _show_traceback is propagated through functions (errtrace)
set -E
# Invoke _show_traceback whenever a failure occurs (via set -e)
# Pass the current script and lineno (man (1) bash for LINENO usage)
trap '_show_traceback "${BASH_SOURCE[0]}" "$LINENO"' ERR
}
# Writes a message to stderr
# $* is the message to be echo'd.
function _log() {
# >&2 means that the default (stdout) is being redirected (>) to &2 (stderr)
echo -e "$@" >&2
}
# Write a message to stderr with Yellow foreground. WARNING: will be prepended with a red background.
# $* - Message
function _warning() {
_log "\\033[1;33m\\033[41mWARNING\\033[49m: $*\\033[0m"
}
# Write a message to stderr using Red foreground.
# It will be prefixed with ERROR: automatically.
# $* - Message
function _error() {
_log "\\033[1;91mERROR: $*\\033[0m"
}
function _test_something_that_fails() {
false # a failing exit code
}
function _test_subfunction() {
_test_something_that_fails # this is where we fail
echo "success" # we shouldn't get here
}
# Register our error handler
_register_traceback
# Should generate a traceback:
# WARNING: Subcommand failed with code=1. Bash traceback:
# In bash_traceback.sh, line 101:
# [#2] _test_subfunction
# In bash_traceback.sh, line 86:
# [#1] _test_something_that_fails
# In bash_traceback.sh, line 82:
# ERROR: false
# ERROR: ^-- returned 1
_test_subfunction
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment