2 # Copyright (C) 2019 Ian Kelling
3 # SPDX-License-Identifier: GPL-3.0-or-later
5 # Commentary: Print stack trace and exit/return on errors, or use
6 # functions below for for more details and manual error handling. See
7 # end of file for credits etc.
9 #######################################
10 # err-catch: Setup trap on ERR to print stack trace and exit (or return
11 # if the shell is interactive). This is the most common use case so we
12 # run it after defining it, you can call err-allow to undo that.
14 # This also sets pipefail because it's a good practice to catch more
17 # Note: In interactive shell, stack calling line number is not
18 # available, so we print function definition lines.
22 # err_catch_ignore Array containing glob patterns to test against
23 # filenames to ignore errors from in interactive
24 # shell. Initialized to ignore bash-completion
25 # scripts on debian based systems.
27 # err-cleanup If set, this command will run just before exiting.
29 # _err_func_last Used internally in err-bash-trace-interactive
31 #######################################
34 if [[ $
- == *i
* ]]; then
35 if ! test ${err_catch_ignore+defined}; then
37 '/etc/bash_completion.d/*'
41 declare -i _err_func_last
=0
43 # shellcheck disable=SC2154
44 trap '_err-bash-trace-interactive $? "$BASH_COMMAND" ${BASH_ARGC[0]} "${BASH_ARGV[@]}" || return $?' ERR
46 # Man bash on exdebug: "If set at shell invocation, arrange to
47 # execute the debugger". We want to avoid that, but I want this file
48 # to be sourceable from bash startup files. noninteractive ssh and
49 # sources .bashrc on invocation. login_shell sources things on
52 # extdebug allows us to print function arguments in our stack trace.
53 if ! shopt login_shell
>/dev
/null
&& [[ ! $SSH_CONNECTION ]]; then
60 # This is the most common use case so run it now.
63 #######################################
64 # Undo err-catch/err-catch-interactive
65 #######################################
72 #######################################
73 # err-exit: Print stack trace and exit
75 # Use this instead of the exit command to be more informative.
77 # usage: err-exit [-EXIT_CODE] [MESSAGE]
79 # EXIT_CODE Default: $? if it is nonzero, otherwise 1.
80 # MESSAGE Print MESSAGE to stderr. Default:
81 # ${BASH_SOURCE[1]}:${BASH_LINENO[0]}: `$BASH_COMMAND' returned $?
85 # err-cleanup If set, this command will run just before exiting.
87 #######################################
90 # This has to come before most things or vars get changed
91 local msg
="${BASH_SOURCE[1]}:${BASH_LINENO[0]}: \`$BASH_COMMAND' returned $err"
93 if [[ $1 == -* ]]; then
96 elif (( ! err
)); then
102 printf "%s\n" "$msg" >&2
104 set -e # err trap does not work within an error trap
105 if type -t err-cleanup
>/dev
/null
; then
108 printf "%s: exiting with status %s\n" "$0" "$err" >&2
112 #######################################
115 # usage: err-bash-trace [FRAME_START]
117 # This function is called by the other functions which print stack
120 # It does not show function args unless you first run:
122 # which err-catch does for you.
124 # FRAME_START Optional variable to set before calling. The frame to
125 # start printing on. default=1. If ${#FUNCNAME[@]} <=
126 # FRAME_START + 1, don't print anything because we are at
127 # the top level of the script and better off printing a
128 # general message, for example see what our callers print.
130 #######################################
132 local -i argc_index
=0 frame i frame_start
=${1:-1}
134 if (( ${#FUNCNAME[@]} <= frame_start
+ 1 )); then
137 for ((frame
=0; frame
< ${#FUNCNAME[@]}; frame
++)); do
138 argc
=${BASH_ARGC[frame]}
140 if ((frame
< frame_start
)); then continue; fi
141 if (( ${#BASH_SOURCE[@]} > 1 )); then
142 source_loc
="${BASH_SOURCE[frame]}:${BASH_LINENO[frame-1]}:"
144 printf " from %sin \`%s" "$source_loc" "${FUNCNAME[frame]}" >&2
145 if shopt extdebug
>/dev
/null
; then
146 for ((i
=argc_index-1
; i
>= argc_index-argc
; i--
)); do
147 printf " %s" "${BASH_ARGV[i]}" >&2
155 #######################################
156 # Internal function for err-catch. Prints stack trace from interactive
159 # Usage: see err-catch-interactive
160 #######################################
161 _err-bash-trace-interactive
() {
162 if (( ${#FUNCNAME[@]} <= 1 )); then
166 for pattern
in "${err_catch_ignore[@]}"; do
167 # shellcheck disable=SC2053
168 if [[ ${BASH_SOURCE[1]} == $pattern ]]; then
173 local ret bash_command argc pattern i last
175 _err_func_last
=${#FUNCNAME[@]}
176 # We have these passed to us because they are lost inside the
183 # The trap returns a nonzero, then gets called again. This condition
184 # tells us if we are the first.
185 if (( _err_func_last
> last
)); then
186 printf "ERR: \`%s\' returned %s\n" "$bash_command" $ret >&2
188 printf " from \`%s" "${FUNCNAME[1]}" >&2
189 if shopt extdebug
>/dev
/null
; then
190 for ((i
=argc
; i
>= 0; i--
)); do
191 printf " %s" "${argv[i]}" >&2
194 printf "\' defined at %s:%s\n" "${BASH_SOURCE[1]}" "$(declare -F "${FUNCNAME[1]}"|awk "{print \$2}")" >&2
198 # Part of an outgoing pipe, avoid getting get us stuck in a weird
199 # subshell if we returned nonzero, which would happen in a situation
202 # tf() { while read -r line; do :; done < <(asdf); };
205 # Note: exit $ret also avoids the stuck subshell problem, and I
206 # can't notice any difference, but this seems more proper.
213 # Related: see my bash script template repo at https://iankelling.org/git.
216 # Please email me if you have a patches, bugs, feedback, or if you use
217 # it or republish it since I'm not aware of any users yet
218 # Ian Kelling <ian@iankelling.org>.
220 # Tested on bash 4.4.20(1)-release (x86_64-pc-linux-gnu). If you test