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.
 
 

313 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_BASH_DEBUG:-false}
readonly MIAOU_BASH_ERROR MIAOU_DEBUG
# FUNCTIONS
function is_true {
[[ "${1,,}" =~ ^(true|yes|1)$ ]] #${1,,} => Lowercase all chars
}
# overridden function which permits backtracing of original `return` statement
function false {
>&2 echo "${BASH_SOURCE[1]}:${BASH_LINENO[0]} false"
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"
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}
stack_lines=("${BASH_LINENO[@]}")
stack_functions=("${FUNCNAME[@]}")
stack_sources=("${BASH_SOURCE[@]}")
# >/dev/tty echo "stack_functionsA ${#stack_functions[@]} ${stack_functions[@]}"
unset 'stack_lines[-1]'
unset 'stack_lines[-1]'
unset 'stack_functions[0]'
unset 'stack_functions[-1]'
# >/dev/tty echo "stack_functionsB ${#stack_functions[@]} ${stack_functions[@]}"
[[ "${#stack_functions[@]}" -gt 0 ]] && stack_functions[-1]='<main>'
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_traces=()
failure_payload=''
if [[ $code -gt 0 ]]; then
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"
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_type='UNDEFINED'
debug "last error=${last_error}"
failure_payload="${last_error}"
regex="^ .*:[0-9]* .*\$"
if [[ "$last_error" =~ $regex ]]; then
if [[ "${code}" == 128 ]]; then
failure_type='SUBSHELL SOURCE NOT FOUND'
stack_functions[0]="${stack_functions[0]} (hint: prefer source than subshell:2)"
else
failure_type='SUBSHELL ERROR'
stack_functions[0]="${stack_functions[0]} (hint: prefer source than subshell:3)"
fi
else
regex='^subshell term:'
if [[ "$last_error" =~ $regex ]]; then
failure_type='SUBSHELL_UNKNOWN'
fi
fi
fi
for i in "${!stack_functions[@]}"; do
stack_detail=$(printf "\t%40s\t%s\n" "${stack_sources[$i]}:${stack_lines[$i]}" "${stack_functions[$i]}")
failure_traces+=("$stack_detail")
done
dump_failure
else
dump_success
fi
builtin exit "$code"
}
function miaou_debug {
is_true "${MIAOU_DEBUG}" && >/dev/tty echo "*********** 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 {
BG=RED
FG=BLACK
PADDING=2
local content detail exit_code
content=$(style_ansi "$1")
content=$(style_padding "$content")
detail=$(BG=BLACK FG=WHITE style_padding "$2")
exit_code=$(FG=CYAN style_ansi " $3")
printf "\r"
BLOCK_CONTINUATION_COL=5 style_block "$content$detail$exit_code"
# echo "$content$detail$exit_code"
ansi_reset
FG=RED style_ansi " ║"
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 true
if [[ ${failure_type} == 'SUBSHELL SOURCE NOT FOUND' ]]; then
:
else
miaou_debug '---error---'
if [[ $failure_type == 'SUBSHELL ERROR' ]]; then
>&4 echo -e "${failure_payload}"
else
>&4 ansi_block_error "$failure_type" "$failure_payload" "$code"
fi
miaou_debug '---end of error---'
fi
miaou_debug '---stacktrace---'
>&4 ansi_traces stack_sources stack_lines stack_functions
# for i in "${failure_traces[@]}"; do
# >&4 echo -e "$i"
# done
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_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---'
# shellcheck source=/dev/null
source "$BASH_ARGV0" 2>&3
dump_success