MIAOU-BASH is a collection of settings and helpers for leveraging BASH. Developer-friendly, it may be used as solo package with or without the miaou project.
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.

415 lines
13 KiB

#!/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 i message="$*" padding=''
local compute_hierarchy=true
[[ ! -v miaou_debug_hierarchy ]] && miaou_debug_hierarchy=0
regex='^</' # starts with </
[[ $miaou_debug_hierarchy -gt 0 ]] &&
[[ "${message}" =~ $regex ]] &&
miaou_debug_hierarchy=$((miaou_debug_hierarchy - 1)) &&
compute_hierarchy=false
for ((i = 0; i < miaou_debug_hierarchy; i++)); do padding+=' '; done
>&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=$((miaou_debug_hierarchy + 1)) ||
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 "<on_return code=$failure_code payload=$failure_payload/>"
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]='<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:-}"
local message
message="${source}: line ${line}: miaou_error: ${type} ${code}"
[[ -n "$payload" ]] && message+=" payload: ${payload}"
>&2 echo "$message"
}
function add_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}"
tmp_error+=("$message")
}
function on_error {
trap - RETURN
failure_code="${1:-200}"
>&4 printf '\r' # TODO: force carriage return explicitly, is it really useful?
miaou_debug "<on_error code=${failure_code} command={${BASH_COMMAND}}>"
if [[ $$ == "$BASHPID" ]]; then
if [[ ! -v failure_type ]]; then
if [[ ${BASH_COMMAND} == 'false' ]]; then
failure_type="FALSE"
print_miaou_error "${BASH_SOURCE[1]}" "${BASH_LINENO[0]}" "${failure_code}" "${failure_type}"
else
failure_type="COMMAND_ERROR"
failure_payload="${BASH_COMMAND}"
print_miaou_error "${BASH_SOURCE[1]}" "${BASH_LINENO[0]}" "${failure_code}" "${failure_type}" "${failure_payload}"
miaou_debug "<investigate_tmp_error pid=${BASHPID}>"
load_tmp_error # cannot print_miaou_error after investigation
if [[ ${#tmp_error[@]} -gt 1 ]]; then
local last_error regex
last_error="${tmp_error[-2]}" # the second latest cause previous print_miaou_error above
regex='^(.*): line ([0-9]+): (.*): command not found$'
if [[ $failure_code == 127 && "$last_error" =~ $regex ]]; then
failure_type='COMMAND_NOT_FOUND'
failure_payload="${BASH_REMATCH[3]}"
miaou_debug "<command_not_found payload=${failure_payload}/>"
fi
fi
miaou_debug '</investigate_tmp_error>'
fi
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[@]}")
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
builtin exit "$failure_code"
else
print_miaou_error "${BASH_SOURCE[1]}" "${BASH_LINENO[0]}" "${failure_code}" 'SUBSHELL_ERROR' "${BASH_COMMAND}"
miaou_debug "</on_error pipe=$failure_code sub_pid=$BASHPID pid=$$ command={${BASH_COMMAND}}>"
kill -PIPE $$
fi
}
function populate_error_arrays {
local source=$1 line=$2 payload=$3
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>')
}
function on_pipe {
trap - RETURN
local tmp_error regex source lineno
miaou_debug "<on_pipe sub_pid=$BASHPID pid=$$>"
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]'
load_tmp_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"
miaou_debug "<subshell_error payload={${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>"
builtin exit "$failure_code"
}
function dump_stderr {
miaou_debug "<stderr count=${#tmp_error[@]}>"
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 = 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 "║"
if [[ ! -v error_source ]]; then
echo -n '╚'
for ((i = 0; i < length_box; i++)); do echo -n '═'; done
echo '╝'
miaou_debug '<continuation disabled/>'
else
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 " ║"
miaou_debug '<continuation enabled/>'
fi
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 load_tmp_error {
[[ -v tmp_error ]] && return 0
mapfile -t tmp_error <"$MIAOU_BASH_ERROR"
local count=${#tmp_error[@]}
if [[ $count == 0 ]]; then
miaou_debug "<tmp_error empty/>"
else
miaou_debug "<tmp_error count=${count}/>"
fi
}
function on_exit {
trap - RETURN
failure_code="${1:-0}"
local last_error regex
miaou_debug "<on_exit code=$failure_code>"
load_tmp_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 "<return code=$failure_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
elif [[ -v failure_type && "$failure_type" == 'COMMAND_NOT_FOUND' ]]; then
unset 'tmp_error[-1]' 'tmp_error[-1]'
add_miaou_error "${BASH_SOURCE[1]}" "${BASH_LINENO[0]}" "${failure_code}" "${failure_type}" "${failure_payload}"
miaou_debug '<command_not_found change_stderr />'
elif [[ ! -v failure_type ]]; then
if [[ $failure_code -gt 0 && ${#tmp_error[@]} -gt 0 ]]; then
last_error="${tmp_error[-1]}"
miaou_debug "<investigate_tmp_error last={$last_error}>"
regex='^(.*): line ([0-9]+): (.*): unbound variable$'
if [[ $failure_code -eq 1 ]] && [[ "$last_error" =~ $regex ]]; then
failure_type='UNBOUND_VARIABLE'
failure_payload="${BASH_REMATCH[3]}"
failure_code=2
local source="${BASH_REMATCH[1]}" line="${BASH_REMATCH[2]}"
error_source=("${BASH_SOURCE[@]}")
error_lineno=("${BASH_LINENO[@]}")
error_funcname=("${FUNCNAME[@]}")
populate_error_arrays "$source" "$line" "$failure_payload"
unset 'tmp_error[-1]'
add_miaou_error "$source" "$line" "${failure_code}" "${failure_type}" "${failure_payload}"
miaou_debug "<unbound_variable source=${source} line=${line} payload=${failure_payload}/>"
else
regex='^(.*): line ([0-9]+): (.*): No such file or directory$'
if [[ $failure_code -eq 1 ]] && [[ "$last_error" =~ $regex ]]; then
failure_type='SOURCE_NOT_FOUND'
failure_payload="${BASH_REMATCH[3]}"
failure_code=3
local source="${BASH_REMATCH[1]}" line="${BASH_REMATCH[2]}"
error_source=("${BASH_SOURCE[@]}")
error_lineno=("${BASH_LINENO[@]}")
error_funcname=("${FUNCNAME[@]}")
populate_error_arrays "$source" "$line" "$failure_payload"
unset 'tmp_error[-1]'
add_miaou_error "$source" "$line" "${failure_code}" "${failure_type}" "${failure_payload}"
miaou_debug "<source_not_found source=${source} line=${line} payload=${failure_payload}/>"
else
failure_type='UNKNOWN ERROR'
failure_payload="$BASH_COMMAND"
fi
fi
miaou_debug "</investigate_tmp_error>"
fi
fi
2>&3 >&3 dump_stderr
if [[ -v failure_type ]]; then
2>&3 >&4 dump_failure
if [[ -v error_source ]]; then
2>&3 >&4 dump_stacktrace
fi
fi
if [[ $failure_code -gt 0 ]]; then
miaou_debug "</on_exit code=$failure_code>"
else
miaou_debug "</on_exit success>"
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 "<source file=$BASH_ARGV0 pid=${BASHPID}/>"
# shellcheck source=/dev/null
source "$BASH_ARGV0"