#!/usr/bin/env bash # CONSTANTS DEBUG=${BASH_BACK_DEBUG:-false} TMP_ERROR=$(mktemp -t "$(basename $0).XXXXXXXX" --tmpdir=/run/user/$(id -u)) SCRIPT=$1 # FUNCTIONS function on_return { if [[ "${BASH_COMMAND}" == 'return '* ]]; then code=$(echo "${BASH_COMMAND}" | cut -d' ' -f2) # regex does not work here unfortunately! thus use of basic cut if [[ -n "$code" ]] && [[ "$code" -ne 0 ]]; then trap - DEBUG >&2 echo "${BASH_SOURCE[2]}: line ${BASH_LINENO[0]}: return $code from ${FUNCNAME[1]}" fi fi } # overridden function which permits stacktracing of original `return` statement function exit { code=${1:-0} >&2 echo "${BASH_SOURCE[2]}:${BASH_LINENO[0]} exit $@" # >&2 echo "${BASH_SOURCE[2]}: line ${BASH_LINENO[0]}: exit $@" builtin exit $code } function on_exit { code=${1:-0} stack_lines=("${BASH_LINENO[@]}") stack_functions=("${FUNCNAME[@]}") stack_sources=("${BASH_SOURCE[@]}") unset stack_lines[-1] unset stack_lines[-1] unset stack_functions[0] unset stack_functions[-1] stack_functions[-1]='
' unset stack_sources[0] unset stack_sources[-1] if [[ "${stack_functions[1]}" == exit && "${stack_sources[1]}" == "${BASH_SOURCE[0]}" ]]; then # the exit case scenario unset stack_functions[1] unset stack_sources[1] unset stack_lines[0] fi stack_lines=("${stack_lines[@]}") stack_functions=("${stack_functions[@]}") stack_sources=("${stack_sources[@]}") stack_summary=() if [[ $code -gt 0 ]]; then last_error=$(tail -n1 $TMP_ERROR) last_source=${stack_sources[0]} last_source=${last_source//./"\."} # replace . by \. regex1="^$last_source: line ([0-9]+): (.*)\$" regex2="^$last_source:([0-9]+) (.*)\$" if [[ "$last_error" =~ $regex1 || "$last_error" =~ $regex2 ]]; then line=${BASH_REMATCH[1]} message=${BASH_REMATCH[2]} regex=': unbound variable$' [[ $code == 1 ]] && [[ "$message" =~ $regex ]] && stack_lines[0]=$line regex="^return ([0-9]*) from (.*)\$" if [[ "$message" =~ $regex ]]; then return_code="${BASH_REMATCH[1]}" funcname="${BASH_REMATCH[2]}" # >&4 echo "RETURN DETECTED line ${stack_sources[0]} $line $funcname" stack_sources=("${stack_sources[0]}" "${stack_sources[@]}") stack_lines=("$line" "${stack_lines[@]}") stack_functions=("$funcname" "${stack_functions[@]}") stack_summary+=("ERROR $code: [return $return_code]") else stack_summary+=("ERROR $code: [$message]") fi else regex="^ .*:[0-9]* .*\$" [[ "$last_error" =~ $regex ]] || stack_summary+=("ERROR $code: ") fi for i in "${!stack_functions[@]}"; do stack_detail=$(printf "\t%30s\t%s\n" "${stack_sources[$i]}:${stack_lines[$i]}" "${stack_functions[$i]}") stack_summary+=("$stack_detail") done >&4 dump_stderr true dump_summary else >&4 dump_stderr false fi cleanup builtin exit $code } function dump_summary { $DEBUG && >/dev/tty echo '---stacktrace---' for i in "${stack_summary[@]}"; do >&4 echo -e "$i" done } function dump_stderr { last_trim=${1:-false} mapfile -t tmp_error <$TMP_ERROR if $last_trim; then unset tmp_error[-1] tmp_error=("${tmp_error[@]}") fi if [[ "${#tmp_error[@]}" -gt 0 ]]; then $DEBUG && >/dev/tty echo '---stderr---' for i in "${tmp_error[@]}"; do echo -e "$i" done fi } function success { >&2 dump_stderr cleanup } function cleanup { trap - ERR TERM INT EXIT exec 3>&- 4>&- rm $TMP_ERROR } # MAIN set -TEue -o pipefail trap 'on_exit $?' ERR TERM INT EXIT trap 'on_return' DEBUG # magic FD exec 3>$TMP_ERROR 4>/dev/stderr $DEBUG && >/dev/tty echo '---stdout---' source $SCRIPT 2>&3 success