#!/usr/bin/env -S -- bash #!/usr/bin/env -S -- bash -x # constants MIAOU_BASH_ERROR=$(mktemp -t "$(basename "$0").XXXXXXXX" --tmpdir="/run/user/$UID") MIAOU_DEBUG=${MIAOU_DEBUG:-false} declare -A ANSI ANSI[RESET]=0 ANSI[STYLE_ITALIC]=3 ANSI[STYLE_BOLD]=1 ANSI[STYLE_UNDERLINE]=4 ANSI[STYLE_STRIKETHROUGH]=9 ANSI[FG_BLACK]=30 ANSI[FG_RED]=31 ANSI[FG_GREEN]=32 ANSI[FG_YELLOW]=33 ANSI[FG_BLUE]=34 ANSI[FG_PURPLE]=35 ANSI[FG_CYAN]=36 ANSI[FG_WHITE]=37 ANSI[FG_GRAY]=90 ANSI[FG_LIGHTBLUE]=94 ANSI[FG_MAGENTA]=95 ANSI[BG_BLACK]=40 ANSI[BG_RED]=41 ANSI[BG_GREEN]=42 ANSI[BG_YELLOW]=43 ANSI[BG_BLUE]=44 ANSI[BG_PURPLE]=45 ANSI[BG_CYAN]=46 ANSI[BG_WHITE]=47 ANSI[BG_GRAY]=90 ANSI[BG_MAGENTA]=95 readonly MIAOU_BASH_ERROR MIAOU_DEBUG ANSI ## functions function is_true { [[ "${1:-}" == 'true' ]] } # HINT: define an alias to bypass ARG evaluation when unnecessary! # example: alias toto="bash -c \"[[ \\\$MIAOU_DEBUG == 'true' ]] && echo yes \\\$* || echo nope\"" function miaou_debug { if is_true "${MIAOU_DEBUG}"; then local compute_hierarchy=true [[ ! -v miaou_debug_hierarchy ]] && miaou_debug_hierarchy=0 local i message="$*" padding='' regex='^&4 builtin echo -e "\e[90m <== DEBUG ==> ${padding}\e[33m${message}\e[0m" regex='[^/]>$' # does NOT end with /> is_true $compute_hierarchy && [[ "${message}" =~ $regex ]] && ((++miaou_debug_hierarchy)) || true fi } function on_return { local regex='return ([0-9]+)' if [[ "$BASH_COMMAND" =~ $regex ]] && [[ "${BASH_REMATCH[1]}" -gt 0 ]]; then failure_code="${BASH_REMATCH[1]}" failure_type="RETURN" failure_payload="${FUNCNAME[0]}" print_miaou_error "${BASH_SOURCE[1]}" "${BASH_LINENO[0]}" "${failure_code}" "${failure_type}" "${failure_payload}" miaou_debug "" fi true } # overridden function which permits backtracing of the original `exit` statement function exit { error_source=("${BASH_SOURCE[@]}") error_lineno=("${BASH_LINENO[@]}") error_funcname=("${FUNCNAME[@]}") unset 'error_source[-1]' 'error_lineno[-1]' 'error_funcname[-1]' unset 'error_source[0]' 'error_lineno[-1]' 'error_funcname[0]' error_funcname[-1]='
' error_source=("${error_source[@]}") error_lineno=("${error_lineno[@]}") error_funcname=("${error_funcname[@]}") failure_code=${1:-0} failure_type="EXIT" failure_payload="${error_funcname[0]}" miaou_debug "" print_miaou_error "${BASH_SOURCE[1]}" "${BASH_LINENO[0]}" "${failure_code}" "${failure_type}" "${failure_payload}" builtin exit "$failure_code" } function tty_ansi { local code for code in "$@"; do if [[ -v ANSI[$code] ]]; then >&4 echo -ne "\e[${ANSI[$code]}m" else >&2 echo "unknown ansi key: <$code>" break fi done } function print_miaou_error { local source="$1" line="$2" code="$3" type="$4" payload="${5:-}" local message message="${source}: line ${line}: miaou_error: ${type} ${code}" [[ -n "$payload" ]] && message+=" payload: ${payload}" [[ -v tmp_error ]] && miaou_debug "tmp_error=${#tmp_error[@]}" && tmp_error+=("$message") || >&2 echo "$message" } function on_error { trap - RETURN failure_code="${1:-200}" >&4 printf '\r' # TODO: force carriage return explicitly, is it really useful? miaou_debug "" if [[ $$ == "$BASHPID" ]]; then if [[ ! -v failure_type ]]; then if [[ ${BASH_COMMAND} == 'false' ]]; then failure_type="FALSE" else failure_type="COMMAND_ERROR" failure_payload="${BASH_COMMAND}" fi print_miaou_error "${BASH_SOURCE[1]}" "${BASH_LINENO[0]}" "${failure_code}" "${failure_type}" fi error_source=("${BASH_SOURCE[@]}") error_lineno=("${BASH_LINENO[@]}") error_funcname=("${FUNCNAME[@]}") unset 'error_source[-1]' 'error_lineno[-1]' 'error_funcname[-1]' unset 'error_source[0]' 'error_lineno[-1]' 'error_funcname[0]' error_funcname[-1]='
' error_source=("${error_source[@]}") error_lineno=("${error_lineno[@]}") error_funcname=("${error_funcname[@]}") if [[ -v failure_payload ]]; then miaou_debug "" else miaou_debug "" fi builtin exit "$failure_code" else miaou_debug "" print_miaou_error "${BASH_SOURCE[1]}" "${BASH_LINENO[0]}" "${failure_code}" 'SUBSHELL_ERROR' "${BASH_COMMAND}" kill -PIPE $$ fi } function on_pipe { trap - RETURN local tmp_error regex source lineno miaou_debug "" error_source=("${BASH_SOURCE[@]}") error_lineno=("${BASH_LINENO[@]}") error_funcname=("${FUNCNAME[@]}") unset 'error_source[0]' 'error_lineno[0]' 'error_funcname[0]' unset 'error_source[-1]' 'error_lineno[-1]' 'error_funcname[-1]' unset 'error_source[-1]' 'error_lineno[-1]' 'error_funcname[-1]' miaou_debug '' && mapfile -t tmp_error <"$MIAOU_BASH_ERROR" last_error="${tmp_error[-1]}" regex='^(.*): line ([0-9]+): miaou_error: SUBSHELL_ERROR ([0-9]+) payload: (.*)$' if [[ $last_error =~ $regex ]]; then source=${BASH_REMATCH[1]} lineno=${BASH_REMATCH[2]} failure_code=${BASH_REMATCH[3]} failure_payload=${BASH_REMATCH[4]} failure_type="SUBSHELL_ERROR" error_source=("$source" "${error_source[@]}") error_lineno=("$lineno" "${error_lineno[@]}") error_funcname=("${error_funcname[@]}" '
') else failure_code=141 failure_type="PIPE_ERROR" fi miaou_debug "" builtin exit "$failure_code" } function dump_stderr { miaou_debug "" tty_ansi FG_LIGHTBLUE for i in "${tmp_error[@]}"; do echo "$i"; done tty_ansi RESET miaou_debug '' } function dump_failure { local i miaou_debug '' tty_ansi FG_MAGENTA length_box=$((${#failure_type} + ${#failure_code} + (2 * 2) + 2)) [[ -v failure_payload ]] && length_box=$((length_box + ${#failure_payload} + (2 * 2) + 1)) echo -n '╔' for ((i = 0; i < length_box; i++)); do echo -n '═'; done echo '╗' echo -n "║ $failure_type " [[ -v failure_payload ]] && echo -n ' ' && tty_ansi BG_WHITE FG_BLACK && echo -n " $failure_payload " && tty_ansi RESET tty_ansi FG_YELLOW printf "%5s" " $failure_code " tty_ansi FG_MAGENTA echo "║" echo -n '╚' for ((i = 0; i < 4; i++)); do echo -n '═'; done echo -n '╦' for ((i = 5; i < length_box; i++)); do echo -n '═'; done echo '╝' echo " ║" tty_ansi RESET miaou_debug '' } function dump_stacktrace { miaou_debug "" local i source lineno funcname for i in "${!error_source[@]}"; do tty_ansi FG_MAGENTA echo -n " ╟──▷" [[ $i -gt 0 ]] && tty_ansi FG_GRAY tty_ansi STYLE_ITALIC source="${error_source[$i]}" lineno="${error_lineno[$i]}" funcname="${error_funcname[$i]}" printf "%40s:%.4s" "$source" "$lineno" tty_ansi RESET printf " \t%s" "$funcname" echo done miaou_debug '' } function on_exit { trap - RETURN local code="${1:-0}" last_error regex miaou_debug "" [[ ! -v tmp_error ]] && miaou_debug '' && mapfile -t tmp_error <"$MIAOU_BASH_ERROR" if [[ -v failure_type && "$failure_type" == 'RETURN' ]]; then last_error="${tmp_error[-1]}" regex='^(.*): line ([0-9]+): miaou_error: RETURN [0-9]+' if [[ "$last_error" =~ $regex ]]; then miaou_debug "" local source line source="${BASH_REMATCH[1]}" line="${BASH_REMATCH[2]}" error_source=("$source" "${error_source[@]}") error_lineno=("$line" "${error_lineno[@]}") error_funcname=("$failure_payload" "${error_funcname[@]}") else miaou_debug "" fi else if [[ $code -gt 0 && ${#tmp_error[@]} -gt 0 ]]; then last_error="${tmp_error[-1]}" miaou_debug "" regex='^(.*): line ([0-9]+): (.*): unbound variable$' if [[ $code -eq 1 ]] && [[ "$last_error" =~ $regex ]]; then local source line payload source="${BASH_REMATCH[1]}" line="${BASH_REMATCH[2]}" payload="${BASH_REMATCH[3]}" error_source=("${BASH_SOURCE[@]}") error_lineno=("${BASH_LINENO[@]}") error_funcname=("${FUNCNAME[@]}") unset 'error_source[0]' 'error_lineno[0]' 'error_funcname[0]' unset 'error_source[-1]' 'error_lineno[-1]' 'error_funcname[-1]' unset 'error_source[-1]' 'error_lineno[-1]' 'error_funcname[-1]' error_source=("$source" "${error_source[@]}") error_lineno=("$line" "${error_lineno[@]}") error_funcname=("${error_funcname[@]}" '
') failure_code=$code failure_type='UNBOUND_VARIABLE' failure_payload=$payload print_miaou_error "${BASH_SOURCE[1]}" "${BASH_LINENO[0]}" "${failure_code}" "${failure_type}" "${failure_payload}" unset 'tmp_error[-2]' # replace previous last_error miaou_debug "" fi miaou_debug "" fi fi 2>&3 >&3 dump_stderr if [[ -v failure_type ]]; then 2>&3 >&4 dump_failure 2>&3 >&4 dump_stacktrace fi if [[ $code -gt 0 ]]; then miaou_debug "" else miaou_debug "" fi } ## main set -euo pipefail set -E # HALT on subshell error set -T # DEBUG RETURN [[ $# -lt 1 ]] && >&2 echo 'ERROR: script expected!' && builtin exit 1 BASH_ARGV0="$1" && shift # to pretend script runs by itself trap 'on_error $?' ERR trap 'on_exit $?' EXIT trap 'on_pipe' PIPE trap 'on_return' RETURN if [[ -e /proc/$$/fd/3 ]]; then exec 4>&3 3>&-; else exec 4>/dev/tty; fi # swap fd 3 4 when fd 3 sent off exec 3>/dev/stderr 2>"$MIAOU_BASH_ERROR" miaou_debug "" # shellcheck source=/dev/null source "$BASH_ARGV0"