#!/usr/bin/env bash #!/usr/bin/env -S -- bash -x # constants MIAOU_DEBUG=${MIAOU_DEBUG:-false} MIAOU_ERROR_REGEX='^(.*): line ([0-9]+): miaou_error:' 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_DEBUG MIAOU_ERROR_REGEX 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 saved_bash_rematch=("${BASH_REMATCH[@]}") # important when regex are used before calling miaou_debug 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 BASH_REMATCH=("${saved_bash_rematch[@]}") # important when regex are used before calling miaou_debug 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 } # overridden function which permits backtracing of the original `exit` statement function exit { code=${1:-0} if [[ $code -gt 0 ]]; then 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}" fi builtin exit "$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}" 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%%$'\n'*}" # first line only print_miaou_error "${BASH_SOURCE[1]}" "${BASH_LINENO[0]}" "${failure_code}" "${failure_type}" "${failure_payload}" load_tmp_error # cannot print_miaou_error after investigation miaou_debug "" 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 "" elif ! is_true "$CHILD_PROCESS"; then # in main process only, not from children! local regex2 source lineno funcname last_error="${tmp_error[-1]}" regex2='^[[:space:]]+miaou_trace: source=(.*) lineno=(.*) funcname=(.*)$' if [[ "$last_error" =~ $regex2 ]]; then miaou_debug "" source="${BASH_REMATCH[1]}" lineno="${BASH_REMATCH[2]}" funcname="${BASH_REMATCH[3]}" extra_source=("$source") extra_lineno=("$lineno") extra_funcname=("$funcname") # miaou_debug "" # go backward for ((i = ${#tmp_error[@]} - 2; i >= 0; i--)); do content=${tmp_error[$i]} # miaou_debug "" if [[ "$content" =~ $regex2 ]]; then source="${BASH_REMATCH[1]}" lineno="${BASH_REMATCH[2]}" funcname="${BASH_REMATCH[3]}" extra_source=("$source" "${extra_source[@]}") extra_lineno=("$lineno" "${extra_lineno[@]}") extra_funcname=("$funcname" "${extra_funcname[@]}") # miaou_debug '' else regex2='^(.*): line ([0-9]+): miaou_error: (.*) [0-9]+(.*)$' if [[ "$content" =~ $regex2 ]]; then failure_type='CHILD_ERROR' failure_payload="${BASH_REMATCH[3]}" local optional_payload optional_payload="${BASH_REMATCH[4]}" [[ -n "$optional_payload" ]] && optional_payload="${optional_payload# payload: }" && failure_payload="${failure_payload} ${optional_payload}" miaou_debug "" else miaou_debug '' fi break fi # miaou_debug '' done miaou_debug "" fi 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]='
' if [[ $failure_type == 'CHILD_ERROR' ]]; then error_source=("${extra_source[@]}" "${error_source[@]}") error_lineno=("${extra_lineno[@]}" "${error_lineno[@]}") error_funcname=("${extra_funcname[@]}" "${error_funcname[@]}") # add the funcname hint message from backward local funcname hint_for_next=false for ((i = 0; i < ${#error_funcname[@]}; i++)); do is_true "$hint_for_next" && error_funcname[i]+=' (hint: prefer source than child process)' && hint_for_next=false funcname="${error_funcname[$i]}" [[ "$funcname" == '
' ]] && hint_for_next=true || true done else error_source=("${error_source[@]}") error_lineno=("${error_lineno[@]}") error_funcname=("${error_funcname[@]}") fi 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 self_upgrade { sudo miaou-bash "$MIAOU_BASH_DIR"/lib/self-upgrade.bash } function uninstall { sudo -E miaou-bash "$MIAOU_BASH_DIR"/lib/uninstall.bash } 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 { local tmp_error_count=${#tmp_error[@]} miaou_debug "" if [[ -v MAIN_FD_MIAOU && $tmp_error_count -gt 0 && "$MAIN_FD_STDERR" == false ]]; then miaou_debug "" local last_error="${tmp_error[-1]}" if [[ "$last_error" =~ $MIAOU_ERROR_REGEX ]]; then tmp_error_count=$((tmp_error_count - 1)) miaou_debug "" unset 'tmp_error[-1]' if [[ $tmp_error_count == 0 ]]; then # IDEA: grab last_stdout and test no carriage_return, cf: ../test/stub/tee_last_line.bash miaou_debug "" printf "\r" fi fi fi tty_ansi FG_LIGHTBLUE for i in "${tmp_error[@]}"; do is_true "$CHILD_PROCESS" && miaou_debug "" 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 save_err_trace_to_main { miaou_debug "" local i source lineno funcname for i in "${tmp_error[@]}"; do >>"$MIAOU_BASH_ERROR_CHILDREN" echo "$i"; done for i in "${!error_source[@]}"; do source="${error_source[$i]}" lineno="${error_lineno[$i]}" funcname="${error_funcname[$i]}" # miaou_debug "" >>"$MIAOU_BASH_ERROR_CHILDREN" echo -e "\tmiaou_trace: source=${source} lineno=${lineno} funcname=${funcname}" done miaou_debug "" } function dump_stacktrace { miaou_debug "" local i source lineno funcname for i in "${!error_source[@]}"; do tty_ansi FG_MAGENTA echo -n " ╟──▷" tty_ansi STYLE_ITALIC source="${error_source[$i]}" lineno="${error_lineno[$i]}" funcname="${error_funcname[$i]}" local hint regex='(^.*) \(hint: (.*)\)$' if [[ "$funcname" =~ $regex ]]; then hint="${BASH_REMATCH[2]}" funcname="${BASH_REMATCH[1]} (hint: " else unset hint [[ $i -gt 0 ]] && tty_ansi FG_GRAY fi printf "%40s:%.4s" "$source" "$lineno" tty_ansi RESET printf " \t%s" "$funcname" if [[ -v hint ]]; then tty_ansi FG_YELLOW builtin echo -n "$hint" tty_ansi RESET builtin echo -n ')' fi echo done miaou_debug '' } function load_tmp_error { miaou_debug "" if [[ ! -v tmp_error ]]; then local count count_children tmp_error_children if [[ -f "$MIAOU_BASH_ERROR_CURRENT_CHILDREN" ]]; then miaou_debug "" mapfile -t tmp_error_children <"$MIAOU_BASH_ERROR_CURRENT_CHILDREN" local count_children=${#tmp_error_children[@]} if [[ $count_children == 0 ]]; then miaou_debug "" else miaou_debug "" fi miaou_debug "" rm "$MIAOU_BASH_ERROR_CURRENT_CHILDREN" fi mapfile -t tmp_error <"$MIAOU_BASH_ERROR" if [[ -v count_children && $count_children -gt 0 ]]; then tmp_error=("${tmp_error[@]}" "${tmp_error_children[@]}") fi local count=${#tmp_error[@]} if [[ $count == 0 ]]; then miaou_debug "" else miaou_debug "" local i last_miaou_error_index=-1 content local multiple_errors=() for i in "${!tmp_error[@]}"; do content="${tmp_error[i]}" # miaou_debug "" if [[ "$content" =~ $MIAOU_ERROR_REGEX ]]; then if [[ $last_miaou_error_index -ge 0 ]]; then multiple_errors+=("$last_miaou_error_index") fi last_miaou_error_index=$i fi done if [[ "${#multiple_errors[@]}" -gt 0 ]]; then miaou_debug "" for i in "${multiple_errors[@]}"; do unset "tmp_error[$i]"; done tmp_error=("${tmp_error[@]}") miaou_debug "" fi miaou_debug "" fi else local count=${#tmp_error[@]} miaou_debug "" fi miaou_debug '' } 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[2]}" "${BASH_LINENO[1]}" "${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[2]}" "${BASH_LINENO[1]}" "${failure_code}" "${failure_type}" "${failure_payload}" miaou_debug "" elif [[ -v failure_type && "$failure_type" == 'CHILD_ERROR' ]]; then for ((i = 0; i < ${#extra_source[@]}; i++)); do unset 'tmp_error[-1]'; done add_miaou_error "${BASH_SOURCE[2]}" "${BASH_LINENO[1]}" "${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 if ! is_true $CHILD_PROCESS; then 2>&3 >&3 dump_stderr if [[ -v failure_type && $failure_code -gt 0 ]]; then 2>&3 >&4 dump_failure if [[ -v error_source ]]; then 2>&3 >&4 dump_stacktrace fi fi elif [[ -v failure_type ]]; then 2>&3 >&4 save_err_trace_to_main fi if [[ $failure_code -gt 0 ]]; then miaou_debug "" else miaou_debug "" fi cleanup } function build_miaou_bash_error { local directory directory="/run/user/$UID/$(basename "$0")" CHILD_PROCESS=false [[ -f "${directory}/${PPID}.tmp" ]] && CHILD_PROCESS=true mkdir -p "$directory" MIAOU_BASH_ERROR_CURRENT_CHILDREN="${directory}/$$.children.tmp" MIAOU_BASH_ERROR_CHILDREN="${directory}/${PPID}.children.tmp" MIAOU_BASH_ERROR="${directory}/$$.tmp" rm -f "$MIAOU_BASH_ERROR" readonly MIAOU_BASH_ERROR_CURRENT_CHILDREN MIAOU_BASH_ERROR_CHILDREN MIAOU_BASH_ERROR CHILD_PROCESS } function cleanup { miaou_debug '' trap - ERR EXIT PIPE RETURN rm -f "$MIAOU_BASH_ERROR" } function usage { echo "$(basename "$0")