X-Git-Url: https://iankelling.org/git/?a=blobdiff_plain;f=fai%2Fconfig%2Ffiles%2Fboot%2Fbash-trace%2FDEFAULT;h=2a4077f851c1c6ef468ac58db2cee12eac3626ff;hb=d9993568d38dd7d2d18ced6b5007e9cc07d1e576;hp=61f8ae52edb9958c799001e43a1f91b8f8994f74;hpb=3d9cc96092cdc8aa05bc95cf83c07bb1af692013;p=automated-distro-installer diff --git a/fai/config/files/boot/bash-trace/DEFAULT b/fai/config/files/boot/bash-trace/DEFAULT index 61f8ae5..2a4077f 100644 --- a/fai/config/files/boot/bash-trace/DEFAULT +++ b/fai/config/files/boot/bash-trace/DEFAULT @@ -1,48 +1,298 @@ -# meant to be sourced. copy/pasted from https://iankelling.org/git/?p=errhandle;a=summary - -bash-trace() { - local -i argc_index=0 arg frame i start=${1:-1} max_indent=8 indent - local source - local extdebug=false - if [[ $(shopt -p extdebug) == *-s* ]]; then - extdebug=true - fi - - for ((frame=0; frame < ${#FUNCNAME[@]}-1; frame++)); do - argc=${BASH_ARGC[frame]} - argc_index+=$argc - ((frame < start)) && continue - if (( ${#BASH_SOURCE[@]} > 1 )); then - source="${BASH_SOURCE[frame+1]}:${BASH_LINENO[frame]}:" - fi - indent=$((frame-start+1)) - indent=$((indent < max_indent ? indent : max_indent)) - printf "%${indent}s↳%sin \`%s" '' "$source" "${FUNCNAME[frame]}" - if $extdebug; then - for ((i=argc_index-1; i >= argc_index-argc; i--)); do - printf " %s" "${BASH_ARGV[i]}" - done - fi - echo \' - done +#!/bin/bash +# Bash Error Handler +# Copyright (C) 2020 Ian Kelling +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +# This is a single file library, just source this file. When an error +# happens, we print a stack trace then exit. In an interactive shell, we +# return from functions instead of exiting. If err-cleanup is a command, +# it runs before the stack trace. Functions are documented inline below +# for additional use cases. +# +# Note: occasionally the line numbers are off a bit (at least in Bash +# 5.0). This appears to be a bash bug. I plan to report it next time it +# happens to me. +# +# Please email me if you use this or have anything to contribute. I'm +# not aware of any users yet Ian Kelling . +# +# Tested on bash 4.4.20(1)-release (x86_64-pc-linux-gnu) and +# 5.0.17(1)-release (x86_64-pc-linux-gnu). +# +# Related: see my bash script template repo at https://iankelling.org/git. + + +# TODO: investigate to see if we can format output betting in case of +# subshell failure. Right now, we get independent trace from inside and +# outside of the subshell. Note, errexit + inherit_errexit doesn't have +# any smarts around this either. + +if ! test "$BASH_VERSION"; then echo "error: shell is not bash" >&2; exit 1; fi + +####################################### +# err-catch: Setup trap on ERR to print stack trace and exit (or return +# if the shell is interactive). This is the most common use case so we +# run it after defining it, you can call err-allow to undo that. +# +# This also sets pipefail because it's a good practice to catch more +# errors. +# +# Note: In interactive shell, stack calling line number is not +# available, so we print function definition lines. +# +# Note: This works like set -e, which has one unintuitive feature: If +# you use a function as part of a conditional, eg: func && come_cmd, a +# failed command within func won't trigger an error. +# +# Globals +# +# err_catch_ignore Array containing glob patterns to test against +# filenames to ignore errors from in interactive +# shell. Initialized to ignore bash-completion +# scripts on debian based systems. +# +# err-cleanup If set, this command will run just before exiting. +# +# _err_func_last Used internally in err-bash-trace-interactive +# +####################################### +err-catch() { + set -E; + if [[ $- == *i* ]]; then + if ! test ${err_catch_ignore+defined}; then + err_catch_ignore=( + '/etc/bash_completion.d/*' + '*/bash-completion/*' + ) + fi + declare -i _err_func_last=0 + if [[ $- != *c* ]]; then + shopt -s extdebug + fi + # shellcheck disable=SC2154 + trap '_err-bash-trace-interactive $? "${PIPESTATUS[*]}" "$BASH_COMMAND" ${BASH_ARGC[0]} "${BASH_ARGV[@]}" || return $?' ERR + else + # Man bash on exdebug: "If set at shell invocation, arrange to + # execute the debugger". We want to avoid that, but I want this file + # to be sourceable from bash startup files. noninteractive ssh and + # sources .bashrc on invocation. login_shell sources things on + # invocation. + # + # extdebug allows us to print function arguments in our stack trace. + if ! shopt login_shell >/dev/null && [[ ! $SSH_CONNECTION ]]; then + shopt -s extdebug + fi + trap err-exit ERR + fi + set -o pipefail } +# This is the most common use case so run it now. +err-catch + +####################################### +# Undo err-catch/err-catch-interactive +####################################### +err-allow() { + shopt -u extdebug + set +E +o pipefail + trap ERR +} + +####################################### +# err-exit: Print stack trace and exit +# +# Use this instead of the exit command to be more informative. +# +# usage: err-exit [-EXIT_CODE] [MESSAGE] +# +# EXIT_CODE Default: $? if it is nonzero, otherwise 1. +# MESSAGE Print MESSAGE to stderr. Default: +# ${BASH_SOURCE[1]}:${BASH_LINENO[0]}: `$BASH_COMMAND' returned $? +# +# Globals +# +# err-cleanup If set, this command will run just before exiting. +# +####################################### +err-exit() { + # vars have _ prefix so that we can inspect existing set vars without + # too much overwriting of them. + local _err=$? _pipestatus="${_pipestatus[*]}" + + # This has to come before most things or vars get changed + local _msg="${BASH_SOURCE[1]}:${BASH_LINENO[0]}: \`$BASH_COMMAND' returned $_err" + local _cmdr="$BASH_COMMAND" # command right. we chop of the left, keep the right. + if [[ $_pipestatus != "$_err" ]]; then + _msg+=", PIPESTATUS: $_pipestatus" + fi + set +x + if [[ $1 == -* ]]; then + _err=${1#-} + shift + elif (( ! _err )); then + _err=1 + fi + if [[ $1 ]]; then + _msg="$1" + fi -errcatch() { - set -E; shopt -s extdebug - _err-trap() { - err=$? - exec >&2 - set +x - echo "${BASH_SOURCE[1]}:${BASH_LINENO[0]}:in \`$BASH_COMMAND' returned $err" - bash-trace 2 - set -e - "${_errcatch_cleanup[@]}" - echo "$0: exiting with code $err" - exit $err - } - trap _err-trap ERR - set -o pipefail + ## Begin printing vars from within BASH_COMMAND ## + local _var _chars _l + local -A _vars + while [[ $_cmdr ]]; do + _chars="${#_cmdr}" + _cmdr="${_cmdr#*$}" + _cmdr="${_cmdr#{}" + if (( _chars == ${#_cmdr} )); then + break + fi + _var="${_cmdr%%[^a-zA-Z0-9_]*}" + if [[ ! $_var || $_var == [0-9]* ]]; then + continue + fi + _vars[${_var}]=t + done + #echo "iank ${_vars[*]}" + #set |& grep ^password + # in my small test, this took 50% longer than piping to grep. + # That seems a small enough penalty to stay in bash here. + if (( ${#_vars[@]} )); then + set |& while read -r _l; do + for _var in "${!_vars[@]}"; do + case $_l in + ${_var}=*) printf "%s\n" "$_l" >&2 ;; + esac + done + done + fi + ## End printing vars from within BASH_COMMAND ## + + printf "%s\n" "$_msg" >&2 + err-bash-trace 2 + set -e # err trap does not work within an error trap + if type -t err-cleanup >/dev/null; then + err-cleanup + fi + printf "%s: exiting with status %s\n" "$0" "$_err" >&2 + exit $_err +} + +####################################### +# Print stack trace +# +# usage: err-bash-trace [FRAME_START] +# +# This function is called by the other functions which print stack +# traces. +# +# It does not show function args unless you first run: +# shopt -s extdebug +# which err-catch does for you. +# +# FRAME_START Optional variable to set before calling. The frame to +# start printing on. default=1. If ${#FUNCNAME[@]} <= +# FRAME_START + 1, don't print anything because we are at +# the top level of the script and better off printing a +# general message, for example see what our callers print. +# +####################################### +err-bash-trace() { + local -i argc_index=0 frame i frame_start=${1:-1} + local source_loc + if (( ${#FUNCNAME[@]} <= frame_start + 1 )); then + return 0 + fi + for ((frame=0; frame < ${#FUNCNAME[@]}; frame++)); do + argc=${BASH_ARGC[frame]} + argc_index+=$argc + if ((frame < frame_start)); then continue; fi + if (( ${#BASH_SOURCE[@]} > 1 )); then + source_loc="${BASH_SOURCE[frame]}:${BASH_LINENO[frame-1]}:" + fi + printf " from %sin \`%s" "$source_loc" "${FUNCNAME[frame]}" >&2 + if shopt extdebug >/dev/null; then + for ((i=argc_index-1; i >= argc_index-argc; i--)); do + printf " %s" "${BASH_ARGV[i]}" >&2 + done + fi + echo \' >&2 + done + return 0 } -errcatch +####################################### +# Internal function for err-catch. Prints stack trace from interactive +# shell trap. +# +# Usage: see err-catch-interactive +####################################### +_err-bash-trace-interactive() { + if (( ${#FUNCNAME[@]} <= 1 )); then + return 0 + fi + + for pattern in "${err_catch_ignore[@]}"; do + # shellcheck disable=SC2053 + if [[ ${BASH_SOURCE[1]} == $pattern ]]; then + return 0 + fi + done + + local ret bash_command argc pattern i last + last=$_err_func_last + _err_func_last=${#FUNCNAME[@]} + # We have these passed to us because they are lost inside the + # function. + ret=$1 + pipestatus="$2" + bash_command="$3" + argc=$(( $4 - 1 )) + shift 4 + argv=("$@") + # The trap returns a nonzero, then gets called again. This condition + # tells us if is that has happened by checking if we've gone down a + # stack level. + if (( _err_func_last >= last )); then + printf "ERR: \`%s\' returned %s" "$bash_command" $ret >&2 + if [[ $pipestatus != "$ret" ]]; then + printf ", PIPESTATUS: %s" "$pipestatus" >&2 + fi + echo >&2 + fi + printf " from \`%s" "${FUNCNAME[1]}" >&2 + if shopt extdebug >/dev/null; then + for ((i=argc; i >= 0; i--)); do + printf " %s" "${argv[i]}" >&2 + done + fi + printf "\' defined at %s:%s\n" "${BASH_SOURCE[1]}" "$(declare -F "${FUNCNAME[1]}"|awk "{print \$2}")" >&2 + if [[ -t 1 ]]; then + return $ret + else + # Part of an outgoing pipe, avoid getting get us stuck in a weird + # subshell if we returned nonzero, which would happen in a situation + # like this: + # + # tf() { while read -r line; do :; done < <(asdf); }; + # tf + # + # Note: exit $ret also avoids the stuck subshell problem, and I + # can't notice any difference, but this seems more proper. + return 0 + fi +}