#!/usr/bin/env bash # CONSTANTS MIAOU_BASH_ERROR=$(mktemp -t "$(basename "$0").XXXXXXXX" --tmpdir="/run/user/$(id -u)") MIAOU_DEBUG=${MIAOU_DEBUG:-false} readonly MIAOU_BASH_ERROR MIAOU_DEBUG # FUNCTIONS function is_true { [[ "${1,,}" =~ ^true$ ]] #${1,,} => Lowercase all chars } # overridden function which permits backtracing of original `return` statement function false { >&2 echo "${BASH_SOURCE[1]}:${BASH_LINENO[0]} false" miaou_debug "overridden false" builtin false } # overridden function which permits backtracing of original `exit` statement function exit { code=${1:-0} # >&2 echo "${BASH_SOURCE[2]}: line ${BASH_LINENO[0]}: exit $@" >&2 echo "${BASH_SOURCE[1]}:${BASH_LINENO[0]} exit $code" miaou_debug "overridden exit $code" builtin exit "$code" } function on_usr1 { cleanup # FIXME: get exit code from MIAOU_BASH_SUBERROR builtin exit 1 } function on_return { # regex does not work here unfortunately! if [[ "${BASH_COMMAND}" == 'return '* ]]; then code=$(echo "${BASH_COMMAND}" | cut -d' ' -f2) 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 } function on_exit { code=${1:-0} if [[ $code -gt 0 ]]; then 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[@]}" -gt 0 ]] && stack_functions[-1]='
' unset 'stack_sources[0]' unset 'stack_sources[-1]' if [[ "${#stack_functions[@]}" -gt 1 && "${stack_functions[1]}" =~ exit|false && "${stack_sources[1]}" == "${BASH_SOURCE[0]}" ]]; then # the exit or false 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[@]}") failure_payload='' last_error=$(tail -n1 "$MIAOU_BASH_ERROR") last_source=${stack_sources[0]} last_source=${last_source//./"\."} # replace '.' with '\.' regex1="^$last_source: line ([0-9]+): (.*)\$" regex2="^$last_source:([0-9]+) (.*)\$" # FIXME: too much tricky! (delayed stderr) if [[ -p /dev/stdout ]]; then failure_type='SUBSHELL_TERM' failure_payload="$last_error" if [[ "$last_error" =~ $regex1 || "$last_error" =~ $regex2 ]]; then failure_payload="${BASH_REMATCH[2]}" fi last_error="${BASH_SOURCE[1]}:${BASH_LINENO[0]} subshell term $code" printf "\r" # useful! kill -USR1 $$ else failure_type='UNKNOWN' fi # debug "last_error=[$last_error] <=> ${last_source}" if [[ "$last_error" =~ $regex1 || "$last_error" =~ $regex2 ]]; then line=${BASH_REMATCH[1]} message=${BASH_REMATCH[2]} regex='^(.*): unbound variable$' if [[ $code == 1 ]] && [[ "$message" =~ $regex ]]; then failure_type='UNBOUND_VARIABLE' failure_payload="${BASH_REMATCH[1]}" stack_lines[0]=$line else if [[ "$message" == 'false' ]]; then failure_type='FALSE' else 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[@]}") failure_type='RETURN' else regex='^(.*): command not found$' if [[ "$message" =~ $regex ]]; then failure_type='COMMAND_NOT_FOUND' failure_payload="${BASH_REMATCH[1]}" else regex='^exit' if [[ "$message" =~ $regex ]]; then failure_type='EXIT' else regex='^(.*): No such file or directory$' if [[ "$message" =~ $regex ]]; then failure_payload="${BASH_REMATCH[1]}" if [[ $code == 1 ]]; then failure_type='SOURCE_NOT_FOUND' stack_lines[0]=$line code=128 # change code to 128 to remember this embedded 'source not found' issue! else if [[ $code == 127 ]]; then failure_type='FILE_NOT_FOUND' regex='\.sh$|\.bash$' if [[ "$failure_payload" =~ $regex ]]; then stack_functions[0]="${stack_functions[0]} (hint: prefer source than subshell:1)" code=129 # change code to 128 to remember this embedded 'source not found' issue! # >&2 echo "${last_error}" else : # >&2 echo "${last_error}" fi else failure_type='UNKNOWN_NOT_FOUND' fi fi fi fi fi fi fi fi else failure_payload="${last_error}" regex="╟─⟶" if [[ "$last_error" =~ $regex ]]; then last_trim=false # keep the last error for next stacktrace error_block=false failure_type='SUBSHELL_ERROR' # # >&4 echo "SUBSHELL_ERROR caught up ${BASH_SOURCE[*]} ${BASH_LINENO[*]}" # stack_functions[0]="${stack_functions[0]} (hint: prefer source than subshell)" [[ "${code}" == 128 ]] && failure_type='SUBSHELL_SOURCE_NOT_FOUND' else failure_type='COMMAND_ERROR' fi fi dump_failure "${last_trim:-true}" "${error_block:-true}" else dump_success fi # FIXME: be aware of errors during the on_exit function because it does not pop up! miaou_debug "--end of on_exit => $code --" builtin exit "$code" } function miaou_debug { is_true "${MIAOU_DEBUG}" && >/dev/tty builtin echo -e "*********** DEBUG: $*" || true } function ansi_block_continuation { tput sc tput cuu 1 BG=RED FG=BLACK style_ansi "\t╦" tput rc FG=RED style_ansi "\t║\n" } function ansi_block_error { FG=BLACK BG=RED local content detail exit_code content=$(style_ansi "$1") content=$(PADDING=2 style_padding "$content") detail="$2" [[ -n "$detail" ]] && detail=$(BG=BLACK FG=WHITE PADDING=2 style_padding "$2") exit_code=$(FG=CYAN PADDING=2 style_padding "$3") builtin echo -en "\r" BLOCK_CONTINUATION_COL=5 style_block "$content$detail$exit_code" ansi_reset FG=RED style_ansi " ║" && builtin echo } function ansi_traces { local optional location sources lines functions declare -n sources=$1 declare -n lines=$2 declare -n functions=$3 optional=false for i in "${!sources[@]}"; do FG=RED style_ansi " ╟─⟶ " location=$(printf "%40s" "${sources[$i]}:${lines[$i]}") is_true "$optional" && location=$(FG=GRAY STYLE=ITALIC style_ansi "$location") || location=$(STYLE=ITALIC style_ansi "$location") builtin echo "${location} ${functions[$i]}" optional=true done } function dump_failure { declare -F style_ansi >/dev/null || source "$MIAOU_BASH_DIR/lib/ansi.bash" dump_stderr "${1:-true}" local error_block="${2:-true}" if is_true $error_block; then miaou_debug "---error box ${failure_type:-} ---" if [[ ${failure_type} == 'SUBSHELL SOURCE NOT FOUND' ]]; then : else >&4 ansi_block_error "$failure_type" "$failure_payload" "$code" fi miaou_debug '---end of error box---' fi miaou_debug '---stacktrace---' >&4 ansi_traces stack_sources stack_lines stack_functions miaou_debug '---end of stacktrace---' cleanup } function dump_stderr { last_trim=${1:-false} mapfile -t tmp_error <"$MIAOU_BASH_ERROR" is_true "$last_trim" && unset 'tmp_error[-1]' && tmp_error=("${tmp_error[@]}") if [[ "${#tmp_error[@]}" -gt 0 ]]; then miaou_debug '---stderr---' >&4 printf '\r' for i in "${tmp_error[@]}"; do >&4 echo -e "$i" done else miaou_debug '---no stderr---' fi } function dump_success { dump_stderr cleanup } function cleanup { trap - ERR TERM EXIT DEBUG USR1 exec 3>&- 4>&- rm "$MIAOU_BASH_ERROR" } # MAIN set -TEue -o pipefail [[ $# -lt 1 ]] && >&2 echo 'ERROR: script expected!' && builtin exit 1 # trap 'on_exit $?' ERR TERM INT EXIT trap 'on_exit $?' ERR EXIT trap 'on_return' DEBUG trap 'on_usr1' USR1 BASH_ARGV0="$1" && shift # to pretend script runs by itself # magic FD + delayed tee => /dev/fd/3 means delayed stderr, /dev/fd/4 remains original stderr exec 3> >(tee "$MIAOU_BASH_ERROR" >/dev/null) 4>/dev/stderr miaou_debug "---stdout--$$ $BASHPID $PPID--" # shellcheck source=/dev/null source "$BASH_ARGV0" 2>&3 dump_success