diff --git a/bin/miaou-bash b/bin/miaou-bash index e00ceaa..677a1c4 100755 --- a/bin/miaou-bash +++ b/bin/miaou-bash @@ -1,302 +1,428 @@ -#!/usr/bin/env bash +#!/usr/bin/env -S -- bash +#!/usr/bin/env -S -- bash -x -# CONSTANTS +# constants -MIAOU_BASH_ERROR=$(mktemp -t "$(basename "$0").XXXXXXXX" --tmpdir="/run/user/$(id -u)") +MIAOU_BASH_ERROR=$(mktemp -t "$(basename "$0").XXXXXXXX" --tmpdir="/run/user/$UID") MIAOU_DEBUG=${MIAOU_DEBUG:-false} -readonly MIAOU_BASH_ERROR MIAOU_DEBUG -# FUNCTIONS +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$ ]] #${1,,} => Lowercase all chars + [[ "${1:-}" == 'true' ]] } -# overridden function which permits backtracing of original `return` statement -function false { - >&2 echo "${BASH_SOURCE[1]}:${BASH_LINENO[0]} false" - miaou_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" - miaou_debug "overridden exit $code" - builtin exit "$code" -} - -function on_usr1 { - cleanup - # FIXME: get exit code from MIAOU_BASH_SUBERROR - builtin exit 1 +# 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='^&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 { - # 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 + 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 } -function on_exit { - code=${1:-0} - - if [[ $code -gt 0 ]]; then - - stack_lines=("${BASH_LINENO[@]}") - stack_functions=("${FUNCNAME[@]}") - stack_sources=("${BASH_SOURCE[@]}") - - unset 'stack_lines[-1]' - unset 'stack_lines[-1]' - unset 'stack_functions[0]' - unset 'stack_functions[-1]' - - [[ "${#stack_functions[@]}" -gt 0 ]] && stack_functions[-1]='
' - - unset 'stack_sources[0]' - unset 'stack_sources[-1]' +# 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" +} - 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]' +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 +} - stack_lines=("${stack_lines[@]}") - stack_functions=("${stack_functions[@]}") - stack_sources=("${stack_sources[@]}") - - failure_payload='' +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" +} - 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]+) (.*)\$" +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") +} - # FIXME: too much tricky! (delayed stderr) - if [[ -p /dev/stdout ]]; then - failure_type='SUBSHELL_TERM' - failure_payload="$last_error" +function on_error { + trap - RETURN + failure_code="${1:-200}" + >&4 printf '\r' # TODO: force carriage return explicitly, is it really useful? - 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" # useful! - kill -USR1 $$ - else - failure_type='UNKNOWN' - fi + miaou_debug "" - # 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 + 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 - 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' + 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 "" + 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 "" 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 + regex='^(.*): line ([0-9]+): (.*): No such file or directory$' + if [[ $failure_code == 127 && "$last_error" =~ $regex ]]; then + failure_type='FILE_NOT_FOUND' + failure_payload="${BASH_REMATCH[3]}" + miaou_debug "" fi fi fi - fi - else - failure_payload="${last_error}" - regex="╟─⟶" - if [[ "$last_error" =~ $regex ]]; then - last_trim=false # keep the last error for next stacktrace - error_block=false - failure_type='SUBSHELL_ERROR' - # - # >&4 echo "SUBSHELL_ERROR caught up ${BASH_SOURCE[*]} ${BASH_LINENO[*]}" - # - stack_functions[0]="${stack_functions[0]} (hint: prefer source than subshell)" - [[ "${code}" == 128 ]] && failure_type='SUBSHELL_SOURCE_NOT_FOUND' - else - failure_type='COMMAND_ERROR' + miaou_debug '' fi fi - dump_failure "${last_trim:-true}" "${error_block:-true}" + 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 - dump_success + print_miaou_error "${BASH_SOURCE[1]}" "${BASH_LINENO[0]}" "${failure_code}" 'SUBSHELL_ERROR' "${BASH_COMMAND}" + miaou_debug "" + kill -PIPE $$ fi +} - # FIXME: be aware of errors during the on_exit function because it does not pop up! - miaou_debug "--end of on_exit => $code --" +function populate_error_arrays { - builtin exit "$code" + 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[@]}" '
') } -function miaou_debug { - is_true "${MIAOU_DEBUG}" && >/dev/tty builtin echo -e "*********** DEBUG: $*" || true +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]' + + 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 "') + else + failure_code=141 + failure_type="PIPE_ERROR" + fi + miaou_debug "" + builtin exit "$failure_code" } -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 dump_stderr { + miaou_debug "" + tty_ansi FG_LIGHTBLUE + for i in "${tmp_error[@]}"; do echo "$i"; done + tty_ansi RESET + miaou_debug '' } -function ansi_block_error { - FG=BLACK - BG=RED - local content detail exit_code +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 "║" + + if [[ ! -v error_source ]]; then + echo -n '╚' + for ((i = 0; i < length_box; i++)); do echo -n '═'; done + echo '╝' + miaou_debug '' + 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 '' + fi + tty_ansi RESET + miaou_debug '' +} - content=$(style_ansi "$1") - content=$(PADDING=2 style_padding "$content") +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 '' +} - detail="$2" - [[ -n "$detail" ]] && detail=$(BG=BLACK FG=WHITE PADDING=2 style_padding "$2") +function load_tmp_error { + [[ -v tmp_error ]] && return 0 - exit_code=$(FG=CYAN PADDING=2 style_padding "$3") - builtin echo -en "\r" - BLOCK_CONTINUATION_COL=5 style_block "$content$detail$exit_code" - ansi_reset - FG=RED style_ansi " ║" && builtin echo + mapfile -t tmp_error <"$MIAOU_BASH_ERROR" + local count=${#tmp_error[@]} + if [[ $count == 0 ]]; then + miaou_debug "" + else + miaou_debug "" + fi } -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 "${1:-true}" - - local error_block="${2:-true}" - if is_true $error_block; then - miaou_debug "---error box ${failure_type:-} ---" - if [[ ${failure_type} == 'SUBSHELL SOURCE NOT FOUND' ]]; then - : +function on_exit { + trap - RETURN + failure_code="${1:-0}" + local last_error regex + miaou_debug "" + + 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 "" + 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 - >&4 ansi_block_error "$failure_type" "$failure_payload" "$code" + miaou_debug "" fi - miaou_debug '---end of error box---' - fi - miaou_debug '---stacktrace---' - >&4 ansi_traces stack_sources stack_lines stack_functions - miaou_debug '---end of stacktrace---' + 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 '' - cleanup -} + elif [[ -v failure_type && "$failure_type" == 'FILE_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 '' -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---' + elif [[ ! -v failure_type ]]; then + if [[ $failure_code -gt 0 && ${#tmp_error[@]} -gt 0 ]]; then + last_error="${tmp_error[-1]}" + miaou_debug "" + + 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 "" + 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 "" + else + failure_type='UNKNOWN ERROR' + failure_payload="$BASH_COMMAND" + fi + fi + miaou_debug "" + fi fi -} -function dump_success { - dump_stderr - cleanup + 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 "" + else + miaou_debug "" + fi } -function cleanup { - trap - ERR TERM EXIT DEBUG USR1 - exec 3>&- 4>&- - rm "$MIAOU_BASH_ERROR" -} +## main -# MAIN +set -euo pipefail +set -E # HALT on subshell error +set -T # DEBUG RETURN -set -TEue -o pipefail [[ $# -lt 1 ]] && >&2 echo 'ERROR: script expected!' && builtin exit 1 - -# trap 'on_exit $?' ERR TERM INT EXIT -trap 'on_exit $?' ERR 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 +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 "---stdout--$$ $BASHPID $PPID--" +miaou_debug "" # shellcheck source=/dev/null -source "$BASH_ARGV0" 2>&3 -dump_success +source "$BASH_ARGV0" diff --git a/bin/miaou-bash-old b/bin/miaou-bash-old new file mode 100755 index 0000000..e00ceaa --- /dev/null +++ b/bin/miaou-bash-old @@ -0,0 +1,302 @@ +#!/usr/bin/env bash + +# CONSTANTS + +MIAOU_BASH_ERROR=$(mktemp -t "$(basename "$0").XXXXXXXX" --tmpdir="/run/user/$(id -u)") +MIAOU_DEBUG=${MIAOU_DEBUG:-false} +readonly MIAOU_BASH_ERROR MIAOU_DEBUG + +# FUNCTIONS + +function is_true { + [[ "${1,,}" =~ ^true$ ]] #${1,,} => Lowercase all chars +} + +# overridden function which permits backtracing of original `return` statement +function false { + >&2 echo "${BASH_SOURCE[1]}:${BASH_LINENO[0]} false" + miaou_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" + miaou_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} + + if [[ $code -gt 0 ]]; then + + stack_lines=("${BASH_LINENO[@]}") + stack_functions=("${FUNCNAME[@]}") + stack_sources=("${BASH_SOURCE[@]}") + + unset 'stack_lines[-1]' + unset 'stack_lines[-1]' + unset 'stack_functions[0]' + unset 'stack_functions[-1]' + + [[ "${#stack_functions[@]}" -gt 0 ]] && stack_functions[-1]='
' + + 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_payload='' + + 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" # useful! + 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_payload="${last_error}" + regex="╟─⟶" + if [[ "$last_error" =~ $regex ]]; then + last_trim=false # keep the last error for next stacktrace + error_block=false + failure_type='SUBSHELL_ERROR' + # + # >&4 echo "SUBSHELL_ERROR caught up ${BASH_SOURCE[*]} ${BASH_LINENO[*]}" + # + stack_functions[0]="${stack_functions[0]} (hint: prefer source than subshell)" + [[ "${code}" == 128 ]] && failure_type='SUBSHELL_SOURCE_NOT_FOUND' + else + failure_type='COMMAND_ERROR' + fi + fi + + dump_failure "${last_trim:-true}" "${error_block:-true}" + else + dump_success + fi + + # FIXME: be aware of errors during the on_exit function because it does not pop up! + miaou_debug "--end of on_exit => $code --" + + builtin exit "$code" +} + +function miaou_debug { + is_true "${MIAOU_DEBUG}" && >/dev/tty builtin echo -e "*********** 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 { + FG=BLACK + BG=RED + local content detail exit_code + + content=$(style_ansi "$1") + content=$(PADDING=2 style_padding "$content") + + detail="$2" + [[ -n "$detail" ]] && detail=$(BG=BLACK FG=WHITE PADDING=2 style_padding "$2") + + exit_code=$(FG=CYAN PADDING=2 style_padding "$3") + builtin echo -en "\r" + BLOCK_CONTINUATION_COL=5 style_block "$content$detail$exit_code" + ansi_reset + FG=RED style_ansi " ║" && builtin 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 "${1:-true}" + + local error_block="${2:-true}" + if is_true $error_block; then + miaou_debug "---error box ${failure_type:-} ---" + if [[ ${failure_type} == 'SUBSHELL SOURCE NOT FOUND' ]]; then + : + else + >&4 ansi_block_error "$failure_type" "$failure_payload" "$code" + fi + miaou_debug '---end of error box---' + fi + + miaou_debug '---stacktrace---' + >&4 ansi_traces stack_sources stack_lines stack_functions + 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_exit $?' ERR 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--$$ $BASHPID $PPID--" +# shellcheck source=/dev/null +source "$BASH_ARGV0" 2>&3 +dump_success diff --git a/poc b/poc deleted file mode 100755 index 677a1c4..0000000 --- a/poc +++ /dev/null @@ -1,428 +0,0 @@ -#!/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='^&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 "" - 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}" - >&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 "" - - 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 "" - 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 "" - else - regex='^(.*): line ([0-9]+): (.*): No such file or directory$' - if [[ $failure_code == 127 && "$last_error" =~ $regex ]]; then - failure_type='FILE_NOT_FOUND' - failure_payload="${BASH_REMATCH[3]}" - miaou_debug "" - fi - fi - fi - miaou_debug '' - 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]='
' - 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 - print_miaou_error "${BASH_SOURCE[1]}" "${BASH_LINENO[0]}" "${failure_code}" 'SUBSHELL_ERROR' "${BASH_COMMAND}" - miaou_debug "" - 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[@]}" '
') -} - -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]' - - 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 "') - 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 "║" - - if [[ ! -v error_source ]]; then - echo -n '╚' - for ((i = 0; i < length_box; i++)); do echo -n '═'; done - echo '╝' - miaou_debug '' - 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 '' - fi - 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 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 "" - else - miaou_debug "" - fi -} - -function on_exit { - trap - RETURN - failure_code="${1:-0}" - local last_error regex - miaou_debug "" - - 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 "" - 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 - - 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 '' - - elif [[ -v failure_type && "$failure_type" == 'FILE_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 '' - - elif [[ ! -v failure_type ]]; then - if [[ $failure_code -gt 0 && ${#tmp_error[@]} -gt 0 ]]; then - last_error="${tmp_error[-1]}" - miaou_debug "" - - 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 "" - 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 "" - else - failure_type='UNKNOWN ERROR' - failure_payload="$BASH_COMMAND" - fi - fi - miaou_debug "" - 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 "" - 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" diff --git a/test/miaou-bash-old.test.bash b/test/miaou-bash-old.test.bash new file mode 100755 index 0000000..25b060f --- /dev/null +++ b/test/miaou-bash-old.test.bash @@ -0,0 +1,83 @@ +#!/usr/bin/env bash + +# CONSTANTS + +TEST_COUNT=0 +SUCCESS_COUNT=0 +# functions + +function assert { + ((TEST_COUNT++)) + if ./test/stub/scenario.bash "$1" >/dev/null; then + ((SUCCESS_COUNT++)) + else + (echo "error in: $1" && exit 1) + fi + +} + +function strip_ansi { + echo "$1" | sed -E 's/\x1b\[[0-9;]*[a-zA-Z]//g' +} + +function fail_type { + ((TEST_COUNT++)) + local i line first error_start_above regex hint_message + hint_message="${3:-}" + mapfile -t stderr <<<"$(./test/stub/scenario.bash "$1" 2>&1 >/dev/null)" + # echo "stderr countains ${#stderr[@]}" + error_start_above=false + success=false + regex="║[[:blank:]]+([A-Z|_]+)" + for i in "${!stderr[@]}"; do + line=$(strip_ansi "${stderr[$i]}") + first="${line:1:1}" + [[ $first == ╔ ]] && error_start_above=true && continue + if [[ "$error_start_above" == true ]]; then + [[ $line =~ $regex ]] && [[ ${BASH_REMATCH[1]} == "$2" ]] && success=true + break + fi + done + + if [[ $success == true && -n "${hint_message}" ]]; then + success=false + regex="╟─⟶.*(\(.*\))$" + for j in $(seq "$((i + 1))" "$((${#stderr[@]} - 1))"); do + line=$(strip_ansi "${stderr[$j]}") + if [[ $line =~ $regex ]]; then + [[ "${BASH_REMATCH[1]}" == "${hint_message}" ]] && success=true + break + fi + done + fi + if [[ $success == true ]]; then + ((SUCCESS_COUNT++)) + else + echo "error in: $1" && return 1 + fi +} + +# main +set -uo pipefail + +assert success +fail_type subshell_term SUBSHELL_TERM +fail_type unbound_variable UNBOUND_VARIABLE +fail_type false FALSE +fail_type return RETURN +fail_type command_not_found COMMAND_NOT_FOUND +fail_type exit EXIT +fail_type source_not_found SOURCE_NOT_FOUND +fail_type file_not_found FILE_NOT_FOUND +fail_type subshell_error COMMAND_NOT_FOUND '(hint: prefer source than subshell)' +fail_type command_error COMMAND_ERROR + +failed=$((TEST_COUNT - SUCCESS_COUNT)) + +if [[ $failed -gt 0 ]]; then + ratio=$((100 * SUCCESS_COUNT / TEST_COUNT)) + echo "${ratio}% passed, $SUCCESS_COUNT succeeded, $failed failed!" + exit 1 +else + echo 100% passed! +fi diff --git a/test/miaou-bash.test.bash b/test/miaou-bash.test.bash index 25b060f..db510af 100755 --- a/test/miaou-bash.test.bash +++ b/test/miaou-bash.test.bash @@ -2,82 +2,77 @@ # CONSTANTS -TEST_COUNT=0 -SUCCESS_COUNT=0 # functions function assert { - ((TEST_COUNT++)) - if ./test/stub/scenario.bash "$1" >/dev/null; then - ((SUCCESS_COUNT++)) + ((test_count++)) + if miaou-bash ./test/stub/scenario.bash "$1" >/dev/null; then + echo -n . else - (echo "error in: $1" && exit 1) + echo -n F + failures+=("$1") + return 1 fi } -function strip_ansi { - echo "$1" | sed -E 's/\x1b\[[0-9;]*[a-zA-Z]//g' -} - function fail_type { - ((TEST_COUNT++)) - local i line first error_start_above regex hint_message - hint_message="${3:-}" - mapfile -t stderr <<<"$(./test/stub/scenario.bash "$1" 2>&1 >/dev/null)" - # echo "stderr countains ${#stderr[@]}" - error_start_above=false + ((test_count++)) + local success regex hint_message success=false - regex="║[[:blank:]]+([A-Z|_]+)" - for i in "${!stderr[@]}"; do - line=$(strip_ansi "${stderr[$i]}") - first="${line:1:1}" - [[ $first == ╔ ]] && error_start_above=true && continue - if [[ "$error_start_above" == true ]]; then - [[ $line =~ $regex ]] && [[ ${BASH_REMATCH[1]} == "$2" ]] && success=true - break - fi - done + hint_message="${3:-}" + # mapfile -t stderr <<<"$(source ./test/stub/scenario.bash "$1" 2>&1 >/dev/null 3>/dev/null)" + mapfile -t stderr <<<"$(miaou-bash ./test/stub/scenario.bash "$1" 2>&1 >/dev/null 3>/dev/null)" + last_error="${stderr[-1]}" - if [[ $success == true && -n "${hint_message}" ]]; then - success=false - regex="╟─⟶.*(\(.*\))$" - for j in $(seq "$((i + 1))" "$((${#stderr[@]} - 1))"); do - line=$(strip_ansi "${stderr[$j]}") - if [[ $line =~ $regex ]]; then - [[ "${BASH_REMATCH[1]}" == "${hint_message}" ]] && success=true - break - fi - done - fi + regex="^(.*): line ([0-9]+): miaou_error: $2 ([0-9]+)" + [[ "${last_error}" =~ $regex ]] && success=true + + # if [[ $success == true && -n "${hint_message}" ]]; then + # : + # fi if [[ $success == true ]]; then - ((SUCCESS_COUNT++)) + echo -n . else - echo "error in: $1" && return 1 + echo -n F + failures+=("$1") + return 1 fi } # main + set -uo pipefail +test_count=0 +failures=() + assert success -fail_type subshell_term SUBSHELL_TERM +fail_type subshell_term SUBSHELL_ERROR fail_type unbound_variable UNBOUND_VARIABLE +fail_type unbound_associative UNBOUND_VARIABLE fail_type false FALSE -fail_type return RETURN -fail_type command_not_found COMMAND_NOT_FOUND +fail_type last_false COMMAND_ERROR +fail_type command_error COMMAND_ERROR +fail_type subshell_term SUBSHELL_ERROR fail_type exit EXIT +fail_type command_not_found COMMAND_NOT_FOUND fail_type source_not_found SOURCE_NOT_FOUND fail_type file_not_found FILE_NOT_FOUND fail_type subshell_error COMMAND_NOT_FOUND '(hint: prefer source than subshell)' -fail_type command_error COMMAND_ERROR -failed=$((TEST_COUNT - SUCCESS_COUNT)) +failed=${#failures[@]} +success=$((test_count - failed)) +echo if [[ $failed -gt 0 ]]; then - ratio=$((100 * SUCCESS_COUNT / TEST_COUNT)) - echo "${ratio}% passed, $SUCCESS_COUNT succeeded, $failed failed!" + ratio=$((100 * success / test_count)) + echo "${ratio}% passed, ${test_count} tests, ${failed} failed!" + echo failures: + for failure in "${failures[@]}"; do + echo -e "\t- $failure" + done exit 1 else - echo 100% passed! + echo "100% passed, ${test_count} tests" fi diff --git a/test/poc.test.bash b/test/poc.test.bash deleted file mode 100755 index f1bf4ec..0000000 --- a/test/poc.test.bash +++ /dev/null @@ -1,77 +0,0 @@ -#!/usr/bin/env bash - -# CONSTANTS - -# functions - -function assert { - ((test_count++)) - if ./poc ./test/stub/scenario.bash "$1" >/dev/null; then - echo -n . - else - echo -n F - failures+=("$1") - return 1 - fi - -} - -function fail_type { - ((test_count++)) - local success regex hint_message - success=false - hint_message="${3:-}" - mapfile -t stderr <<<"$(./poc ./test/stub/scenario.bash "$1" 2>&1 >/dev/null 3>/dev/null)" - last_error="${stderr[-1]}" - - regex="^(.*): line ([0-9]+): miaou_error: $2 ([0-9]+)" - [[ "${last_error}" =~ $regex ]] && success=true - - # if [[ $success == true && -n "${hint_message}" ]]; then - # : - # fi - if [[ $success == true ]]; then - echo -n . - else - echo -n F - failures+=("$1") - return 1 - fi -} - -# main - -set -uo pipefail - -test_count=0 -failures=() - -assert success -fail_type subshell_term SUBSHELL_ERROR -fail_type unbound_variable UNBOUND_VARIABLE -fail_type unbound_associative UNBOUND_VARIABLE -fail_type false FALSE -fail_type last_false COMMAND_ERROR -fail_type command_error COMMAND_ERROR -fail_type subshell_term SUBSHELL_ERROR -fail_type exit EXIT -fail_type command_not_found COMMAND_NOT_FOUND -fail_type source_not_found SOURCE_NOT_FOUND -fail_type file_not_found FILE_NOT_FOUND -fail_type subshell_error COMMAND_NOT_FOUND '(hint: prefer source than subshell)' - -failed=${#failures[@]} -success=$((test_count - failed)) - -echo -if [[ $failed -gt 0 ]]; then - ratio=$((100 * success / test_count)) - echo "${ratio}% passed, ${test_count} tests, ${failed} failed!" - echo failures: - for failure in "${failures[@]}"; do - echo -e "\t- $failure" - done - exit 1 -else - echo "100% passed, ${test_count} tests" -fi