You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
301 lines
9.1 KiB
301 lines
9.1 KiB
#!/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 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 "<on_return code=$failure_code payload=$failure_payload />"
|
|
fi
|
|
}
|
|
|
|
# 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]='<main>'
|
|
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 "<exit code=$failure_code overridden/>"
|
|
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:-}"
|
|
>&2 echo -n "${source}: line ${line}: miaou_error: code=${code} type=${type}"
|
|
[[ -n "$payload" ]] && >&2 echo " payload=${payload}" || >&2 echo
|
|
}
|
|
|
|
function on_error {
|
|
trap - RETURN
|
|
failure_code="${1:-200}"
|
|
>&4 printf '\r' # TODO: force carriage return explicitly, is it really useful?
|
|
|
|
if [[ $$ == "$BASHPID" ]]; then
|
|
if [[ ! -v failure_type ]]; then
|
|
failure_type="COMMAND_ERROR"
|
|
failure_payload="${BASH_COMMAND}"
|
|
print_miaou_error "${BASH_SOURCE[1]}" "${BASH_LINENO[0]}" "${failure_code}" "${failure_type}"
|
|
fi
|
|
|
|
if [[ -v failure_payload ]]; then
|
|
miaou_debug "<on_error exit=$failure_code type=${failure_type} payload=${failure_payload} />"
|
|
else
|
|
miaou_debug "<on_error exit=$failure_code type=${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]='<main>'
|
|
error_source=("${error_source[@]}")
|
|
error_lineno=("${error_lineno[@]}")
|
|
error_funcname=("${error_funcname[@]}")
|
|
|
|
builtin exit "$failure_code"
|
|
else
|
|
miaou_debug "<on_error pipe=$failure_code sub_pid=$BASHPID pid=$$/>"
|
|
print_miaou_error "${BASH_SOURCE[1]}" "${BASH_LINENO[0]}" "${failure_code}" 'SUBSHELL_ERROR'
|
|
kill -PIPE $$
|
|
fi
|
|
}
|
|
|
|
function on_pipe {
|
|
trap - RETURN
|
|
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[@]}" '<main>')
|
|
else
|
|
failure_code=141
|
|
failure_type="PIPE_ERROR"
|
|
fi
|
|
miaou_debug "<on_pipe exit=$failure_code sub_pid=$BASHPID pid=$$/>"
|
|
builtin exit "$failure_code"
|
|
}
|
|
|
|
function dump_stderr {
|
|
miaou_debug '<stderr>'
|
|
tty_ansi FG_LIGHTBLUE
|
|
for i in "${tmp_error[@]}"; do echo "$i"; done
|
|
tty_ansi RESET
|
|
miaou_debug '</stderr>'
|
|
}
|
|
|
|
function dump_failure {
|
|
local i
|
|
miaou_debug '<error_box>'
|
|
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 '</error_box>'
|
|
}
|
|
|
|
function dump_stacktrace {
|
|
miaou_debug "<stacktrace count=${#error_source[@]}>"
|
|
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 '</stacktrace>'
|
|
}
|
|
|
|
function on_exit {
|
|
trap - RETURN
|
|
local code="${1:-0}" last_error regex
|
|
|
|
mapfile -t tmp_error <"$MIAOU_BASH_ERROR"
|
|
2>&3 >&3 dump_stderr
|
|
if [[ -v failure_type && "$failure_type" == 'RETURN' ]]; then
|
|
last_error="${tmp_error[-1]}"
|
|
regex='^(.*): line ([0-9]+): miaou_error: code=[0-9]+ type=RETURN'
|
|
if [[ "$last_error" =~ $regex ]]; then
|
|
miaou_debug "<return exit=$code />"
|
|
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 "<return type=unexpected />"
|
|
fi
|
|
|
|
else
|
|
if [[ $code -gt 0 && ${#tmp_error[@]} -gt 0 ]]; then
|
|
last_error="${tmp_error[-1]}"
|
|
miaou_debug "<on_exit exit=$code last_error='$last_error'>"
|
|
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 "<unbound_variable source=${source} line=${line} payload=${payload}>"
|
|
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[@]}" '<main>')
|
|
fi
|
|
fi
|
|
fi
|
|
miaou_debug "</on_exit>"
|
|
fi
|
|
|
|
if [[ -v failure_type ]]; then
|
|
2>&3 >&4 dump_failure
|
|
2>&3 >&4 dump_stacktrace
|
|
fi
|
|
|
|
miaou_debug "</source type=failure exit=$code pid=${BASHPID}>"
|
|
}
|
|
|
|
## 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 "<source from=$BASH_ARGV0 pid=${BASHPID}>"
|
|
# shellcheck source=/dev/null
|
|
source "$BASH_ARGV0"
|
|
miaou_debug "</source type=success pid=${BASHPID}>"
|