3 # Copyright (C) 2020 Ian Kelling <ian@iankelling.org>
4 # SPDX-License-Identifier: GPL-3.0-or-later
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 # This is a single file library, just source this file. When an error
21 # happens, we print a stack trace then exit. In an interactive shell, we
22 # return from functions instead of exiting. If err-cleanup is a command,
23 # it runs before the stack trace. Functions are documented inline below
24 # for additional use cases.
26 # Note: occasionally the line numbers are off a bit (at least in Bash
27 # 5.0). This appears to be a bash bug. I plan to report it next time it
30 # Please email me if you use this or have anything to contribute. I'm
31 # not aware of any users yet Ian Kelling <ian@iankelling.org>.
33 # Tested on bash 4.4.20(1)-release (x86_64-pc-linux-gnu) and
34 # 5.0.17(1)-release (x86_64-pc-linux-gnu).
36 # Related: see my bash script template repo at https://iankelling.org/git.
39 # TODO: investigate to see if we can format output betting in case of
40 # subshell failure. Right now, we get independent trace from inside and
41 # outside of the subshell. Note, errexit + inherit_errexit doesn't have
42 # any smarts around this either.
44 if ! test "$BASH_VERSION"; then echo "error: shell is not bash" >&2; exit 1; fi
46 #######################################
47 # err-catch: Setup trap on ERR to print stack trace and exit (or return
48 # if the shell is interactive). This is the most common use case so we
49 # run it after defining it, you can call err-allow to undo that.
51 # This also sets pipefail because it's a good practice to catch more
54 # Note: In interactive shell, stack calling line number is not
55 # available, so we print function definition lines.
57 # Note: This works like set -e, which has one unintuitive feature: If
58 # you use a function as part of a conditional, eg: func && come_cmd, a
59 # failed command within func won't trigger an error.
63 # err_catch_ignore Array containing glob patterns to test against
64 # filenames to ignore errors from in interactive
65 # shell. Initialized to ignore bash-completion
66 # scripts on debian based systems.
68 # err-cleanup If set, this command will run just before exiting.
70 # _err_func_last Used internally in err-bash-trace-interactive
72 #######################################
75 if [[ $
- == *i
* ]]; then
76 if ! test ${err_catch_ignore+defined}; then
78 '/etc/bash_completion.d/*'
82 declare -i _err_func_last
=0
83 if [[ $
- != *c
* ]]; then
86 # shellcheck disable=SC2154
87 trap '_err-bash-trace-interactive $? "${PIPESTATUS[*]}" "$BASH_COMMAND" ${BASH_ARGC[0]} "${BASH_ARGV[@]}" || return $?' ERR
89 # Man bash on exdebug: "If set at shell invocation, arrange to
90 # execute the debugger". We want to avoid that, but I want this file
91 # to be sourceable from bash startup files. noninteractive ssh and
92 # sources .bashrc on invocation. login_shell sources things on
95 # extdebug allows us to print function arguments in our stack trace.
96 if ! shopt login_shell
>/dev
/null
&& [[ ! $SSH_CONNECTION ]]; then
103 # This is the most common use case so run it now.
106 #######################################
107 # Undo err-catch/err-catch-interactive
108 #######################################
115 #######################################
116 # err-exit: Print stack trace and exit
118 # Use this instead of the exit command to be more informative.
120 # usage: err-exit [-EXIT_CODE] [MESSAGE]
122 # EXIT_CODE Default: $? if it is nonzero, otherwise 1.
123 # MESSAGE Print MESSAGE to stderr. Default:
124 # ${BASH_SOURCE[1]}:${BASH_LINENO[0]}: `$BASH_COMMAND' returned $?
128 # err-cleanup If set, this command will run just before exiting.
130 #######################################
132 # vars have _ prefix so that we can inspect existing set vars without
133 # too much overwriting of them.
134 local _err
=$? _pipestatus
="${_pipestatus[*]}"
136 # This has to come before most things or vars get changed
137 local _msg
="${BASH_SOURCE[1]}:${BASH_LINENO[0]}: \`$BASH_COMMAND' returned $_err"
138 local _cmdr
="$BASH_COMMAND" # command right. we chop of the left, keep the right.
140 if [[ $_pipestatus != "$_err" ]]; then
141 _msg
+=", PIPESTATUS: $_pipestatus"
144 if [[ $1 == -* ]]; then
147 elif (( ! _err
)); then
154 ## Begin printing vars from within BASH_COMMAND ##
157 while [[ $_cmdr ]]; do
161 if (( _chars
== ${#_cmdr} )); then
164 _var
="${_cmdr%%[^a-zA-Z0-9_]*}"
165 if [[ ! $_var ||
$_var == [0-9]* ]]; then
170 #echo "iank ${_vars[*]}"
171 #set |& grep ^password
172 # in my small test, this took 50% longer than piping to grep.
173 # That seems a small enough penalty to stay in bash here.
174 if (( ${#_vars[@]} )); then
175 set |
& while read -r _l
; do
176 for _var
in "${!_vars[@]}"; do
178 ${_var}=*) printf "%s\n" "$_l" >&2 ;;
183 ## End printing vars from within BASH_COMMAND ##
185 printf "%s\n" "$_msg" >&2
187 set -e # err trap does not work within an error trap
188 if type -t err-cleanup
>/dev
/null
; then
191 printf "%s: exiting with status %s\n" "$0" "$_err" >&2
195 #######################################
198 # usage: err-bash-trace [FRAME_START]
200 # This function is called by the other functions which print stack
203 # It does not show function args unless you first run:
205 # which err-catch does for you.
207 # FRAME_START Optional variable to set before calling. The frame to
208 # start printing on. default=1. If ${#FUNCNAME[@]} <=
209 # FRAME_START + 1, don't print anything because we are at
210 # the top level of the script and better off printing a
211 # general message, for example see what our callers print.
213 #######################################
215 local -i argc_index
=0 frame i frame_start
=${1:-1}
217 if (( ${#FUNCNAME[@]} <= frame_start
+ 1 )); then
220 for ((frame
=0; frame
< ${#FUNCNAME[@]}; frame
++)); do
221 argc
=${BASH_ARGC[frame]}
223 if ((frame
< frame_start
)); then continue; fi
224 if (( ${#BASH_SOURCE[@]} > 1 )); then
225 source_loc
="${BASH_SOURCE[frame]}:${BASH_LINENO[frame-1]}:"
227 printf " from %sin \`%s" "$source_loc" "${FUNCNAME[frame]}" >&2
228 if shopt extdebug
>/dev
/null
; then
229 for ((i
=argc_index-1
; i
>= argc_index-argc
; i--
)); do
230 printf " %s" "${BASH_ARGV[i]}" >&2
238 #######################################
239 # Internal function for err-catch. Prints stack trace from interactive
242 # Usage: see err-catch-interactive
243 #######################################
244 _err-bash-trace-interactive
() {
245 if (( ${#FUNCNAME[@]} <= 1 )); then
249 for pattern
in "${err_catch_ignore[@]}"; do
250 # shellcheck disable=SC2053
251 if [[ ${BASH_SOURCE[1]} == $pattern ]]; then
256 local ret bash_command argc pattern i last
258 _err_func_last
=${#FUNCNAME[@]}
259 # We have these passed to us because they are lost inside the
267 # The trap returns a nonzero, then gets called again. This condition
268 # tells us if is that has happened by checking if we've gone down a
270 if (( _err_func_last
>= last
)); then
271 printf "ERR: \`%s\' returned %s" "$bash_command" $ret >&2
272 if [[ $pipestatus != "$ret" ]]; then
273 printf ", PIPESTATUS: %s" "$pipestatus" >&2
277 printf " from \`%s" "${FUNCNAME[1]}" >&2
278 if shopt extdebug
>/dev
/null
; then
279 for ((i
=argc
; i
>= 0; i--
)); do
280 printf " %s" "${argv[i]}" >&2
283 printf "\' defined at %s:%s\n" "${BASH_SOURCE[1]}" "$(declare -F "${FUNCNAME[1]}"|awk "{print \$2}")" >&2
287 # Part of an outgoing pipe, avoid getting get us stuck in a weird
288 # subshell if we returned nonzero, which would happen in a situation
291 # tf() { while read -r line; do :; done < <(asdf); };
294 # Note: exit $ret also avoids the stuck subshell problem, and I
295 # can't notice any difference, but this seems more proper.