#!/usr/bin/env bash # constants MIAOU_BASH_ERROR=$(mktemp -t "$(basename "$0").XXXXXXXX" --tmpdir="/run/user/$(id -u)") 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 { is_true "${MIAOU_DEBUG}" && >&4 builtin echo -e "\e[90m <== DEBUG ==> \e[33m$*\e[0m" || true } 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:-}" >&2 echo -n "${source}: line ${line}: miaou_error: code=${code} type=${type}" [[ -n "$payload" ]] && >&2 echo " payload=${payload}" || >&2 echo } function on_error { failure_code="${1:-200}" >&4 printf '\r' # force carriage return explicitly if [[ $$ == "$BASHPID" ]]; then miaou_debug "" failure_type="COMMAND_ERROR" print_miaou_error "${BASH_SOURCE[1]}" "${BASH_LINENO[0]}" "${failure_code}" "${failure_type}" '...' 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[@]}") exit "$failure_code" else miaou_debug "" print_miaou_error "${BASH_SOURCE[1]}" "${BASH_LINENO[0]}" "${failure_code}" 'SUBSHELL_ERROR' kill -PIPE $$ fi } function on_pipe { local tmp_error regex source lineno 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]' mapfile -t tmp_error <"$MIAOU_BASH_ERROR" last_error="${tmp_error[-1]}" regex='(.*): line ([0-9]+): miaou_error: code=([0-9]+) type=SUBSHELL_ERROR' if [[ $last_error =~ $regex ]]; then source=${BASH_REMATCH[1]} lineno=${BASH_REMATCH[2]} failure_code=${BASH_REMATCH[3]} failure_type="SUBSHELL_ERROR" failure_payload='...' 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 "" 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 in $(seq 1 $length_box); 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 in $(seq 1 4); do echo -n '═'; done echo -n '╦' for i in $(seq $((i + 2)) $length_box); 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 { local code code="${1:-0}" mapfile -t tmp_error <"$MIAOU_BASH_ERROR" 2>&3 >&3 dump_stderr if [[ $code -gt 0 && ${#tmp_error[@]} -gt 0 ]]; then local last_error regex last_error="${tmp_error[-1]}" miaou_debug "" if [[ $code -eq 1 ]]; then regex='^(.*): line ([0-9]+): (.*): unbound variable$' if [[ "$last_error" =~ $regex ]]; then local source line payload source="${BASH_REMATCH[1]}" line="${BASH_REMATCH[2]}" payload="${BASH_REMATCH[3]}" miaou_debug "" failure_code=$code failure_type='UNBOUND_VARIABLE' failure_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[@]}" '
') fi fi miaou_debug "" fi if [[ -v failure_type ]]; then 2>&3 >&4 dump_failure 2>&3 >&4 dump_stacktrace fi miaou_debug "" } ## 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 if [[ -e /proc/$$/fd/3 ]]; then exec 4>&3 3>&-; else exec 4>/dev/tty; fi exec 3>/dev/stderr 2>"$MIAOU_BASH_ERROR" miaou_debug "" # shellcheck source=/dev/null source "$BASH_ARGV0" miaou_debug ""