Commit | Line | Data |
---|---|---|
78a1a75c | 1 | #!/bin/bash |
fc2d9041 IK |
2 | # Copyright (C) 2019 Ian Kelling |
3 | # SPDX-License-Identifier: GPL-3.0-or-later | |
acd03e1e | 4 | |
e58bbdf6 IK |
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. | |
acd03e1e | 8 | |
e58bbdf6 IK |
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. | |
13 | # | |
14 | # This also sets pipefail because it's a good practice to catch more | |
15 | # errors. | |
16 | # | |
17 | # Note: In interactive shell, stack calling line number is not | |
18 | # available, so we print function definition lines. | |
19 | # | |
20 | # Globals | |
21 | # | |
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. | |
26 | # | |
27 | # err-cleanup If set, this command will run just before exiting. | |
28 | # | |
29 | # _err_func_last Used internally in err-bash-trace-interactive | |
30 | # | |
31 | ####################################### | |
32 | err-catch() { | |
33 | set -E; | |
34 | if [[ $- == *i* ]]; then | |
35 | if ! test ${err_catch_ignore+defined}; then | |
36 | err_catch_ignore=( | |
37 | '/etc/bash_completion.d/*' | |
38 | '*/bash-completion/*' | |
39 | ) | |
40 | fi | |
41 | declare -i _err_func_last=0 | |
42 | shopt -s extdebug | |
43 | # shellcheck disable=SC2154 | |
44 | trap '_err-bash-trace-interactive $? "$BASH_COMMAND" ${BASH_ARGC[0]} "${BASH_ARGV[@]}" || return $?' ERR | |
45 | else | |
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 | |
50 | # invocation. | |
51 | # | |
52 | # extdebug allows us to print function arguments in our stack trace. | |
53 | if ! shopt login_shell >/dev/null && [[ ! $SSH_CONNECTION ]]; then | |
54 | shopt -s extdebug | |
55 | fi | |
56 | trap err-exit ERR | |
57 | fi | |
58 | set -o pipefail | |
59 | } | |
60 | # This is the most common use case so run it now. | |
61 | err-catch | |
62 | ||
63 | ####################################### | |
64 | # Undo err-catch/err-catch-interactive | |
65 | ####################################### | |
66 | err-allow() { | |
67 | shopt -u extdebug | |
68 | set +E +o pipefail | |
69 | trap ERR | |
70 | } | |
71 | ||
72 | ####################################### | |
73 | # err-exit: Print stack trace and exit | |
74 | # | |
75 | # Use this instead of the exit command to be more informative. | |
76 | # | |
77 | # usage: err-exit [-EXIT_CODE] [MESSAGE] | |
78 | # | |
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 $? | |
82 | # | |
83 | # Globals | |
84 | # | |
85 | # err-cleanup If set, this command will run just before exiting. | |
86 | # | |
87 | ####################################### | |
88 | err-exit() { | |
89 | local err=$? | |
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" | |
92 | set +x | |
93 | if [[ $1 == -* ]]; then | |
94 | err=${1#-} | |
95 | shift | |
96 | elif (( ! err )); then | |
97 | err=1 | |
98 | fi | |
99 | if [[ $1 ]]; then | |
100 | msg="$1" | |
101 | fi | |
102 | printf "%s\n" "$msg" >&2 | |
103 | err-bash-trace 2 | |
104 | set -e # err trap does not work within an error trap | |
105 | if type -t err-cleanup >/dev/null; then | |
106 | err-cleanup | |
107 | fi | |
108 | printf "%s: exiting with status %s\n" "$0" "$err" >&2 | |
109 | exit $err | |
110 | } | |
a5c2ac43 | 111 | |
a5c2ac43 IK |
112 | ####################################### |
113 | # Print stack trace | |
114 | # | |
e58bbdf6 | 115 | # usage: err-bash-trace [FRAME_START] |
a5c2ac43 | 116 | # |
62aed8de IK |
117 | # This function is called by the other functions which print stack |
118 | # traces. | |
119 | # | |
a5c2ac43 IK |
120 | # It does not show function args unless you first run: |
121 | # shopt -s extdebug | |
e58bbdf6 | 122 | # which err-catch does for you. |
a5c2ac43 | 123 | # |
e58bbdf6 IK |
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. | |
5a600375 | 129 | # |
a5c2ac43 IK |
130 | ####################################### |
131 | err-bash-trace() { | |
e58bbdf6 IK |
132 | local -i argc_index=0 frame i frame_start=${1:-1} |
133 | local source_loc | |
134 | if (( ${#FUNCNAME[@]} <= frame_start + 1 )); then | |
135 | return 0 | |
acd03e1e | 136 | fi |
5a600375 | 137 | for ((frame=0; frame < ${#FUNCNAME[@]}; frame++)); do |
acd03e1e IK |
138 | argc=${BASH_ARGC[frame]} |
139 | argc_index+=$argc | |
e58bbdf6 | 140 | if ((frame < frame_start)); then continue; fi |
acd03e1e | 141 | if (( ${#BASH_SOURCE[@]} > 1 )); then |
e58bbdf6 | 142 | source_loc="${BASH_SOURCE[frame]}:${BASH_LINENO[frame-1]}:" |
acd03e1e | 143 | fi |
e58bbdf6 | 144 | printf " from %sin \`%s" "$source_loc" "${FUNCNAME[frame]}" >&2 |
5a600375 | 145 | if shopt extdebug >/dev/null; then |
acd03e1e | 146 | for ((i=argc_index-1; i >= argc_index-argc; i--)); do |
e58bbdf6 | 147 | printf " %s" "${BASH_ARGV[i]}" >&2 |
acd03e1e | 148 | done |
78a1a75c | 149 | fi |
e58bbdf6 | 150 | echo \' >&2 |
acd03e1e | 151 | done |
a5c2ac43 | 152 | return 0 |
78a1a75c | 153 | } |
364203b2 | 154 | |
a5c2ac43 | 155 | ####################################### |
e58bbdf6 IK |
156 | # Internal function for err-catch. Prints stack trace from interactive |
157 | # shell trap. | |
a5c2ac43 | 158 | # |
5f3350ec IK |
159 | # Usage: see err-catch-interactive |
160 | ####################################### | |
5f3350ec | 161 | _err-bash-trace-interactive() { |
55f98600 IK |
162 | if (( ${#FUNCNAME[@]} <= 1 )); then |
163 | return 0 | |
164 | fi | |
165 | ||
166 | for pattern in "${err_catch_ignore[@]}"; do | |
167 | # shellcheck disable=SC2053 | |
168 | if [[ ${BASH_SOURCE[1]} == $pattern ]]; then | |
169 | return 0 | |
170 | fi | |
171 | done | |
172 | ||
3fb24829 IK |
173 | local ret bash_command argc pattern i last |
174 | last=$_err_func_last | |
175 | _err_func_last=${#FUNCNAME[@]} | |
5f3350ec IK |
176 | # We have these passed to us because they are lost inside the |
177 | # function. | |
178 | ret=$1 | |
179 | bash_command="$2" | |
180 | argc=$(( $3 - 1 )) | |
181 | shift 3 | |
182 | argv=("$@") | |
3fb24829 IK |
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 | |
e58bbdf6 | 186 | printf "ERR: \`%s\' returned %s\n" "$bash_command" $ret >&2 |
5f3350ec | 187 | fi |
e58bbdf6 | 188 | printf " from \`%s" "${FUNCNAME[1]}" >&2 |
3fb24829 IK |
189 | if shopt extdebug >/dev/null; then |
190 | for ((i=argc; i >= 0; i--)); do | |
e58bbdf6 | 191 | printf " %s" "${argv[i]}" >&2 |
3fb24829 | 192 | done |
5f3350ec | 193 | fi |
e58bbdf6 IK |
194 | printf "\' defined at %s:%s\n" "${BASH_SOURCE[1]}" "$(declare -F "${FUNCNAME[1]}"|awk "{print \$2}")" >&2 |
195 | if [[ -t 1 ]]; then | |
196 | return $ret | |
197 | else | |
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 | |
200 | # like this: | |
201 | # | |
202 | # tf() { while read -r line; do :; done < <(asdf); }; | |
203 | # tf | |
204 | # | |
205 | # Note: exit $ret also avoids the stuck subshell problem, and I | |
206 | # can't notice any difference, but this seems more proper. | |
207 | return 0 | |
5f3350ec | 208 | fi |
9ce3dca1 IK |
209 | } |
210 | ||
e58bbdf6 | 211 | # Credits etc: |
a5c2ac43 | 212 | # |
e58bbdf6 | 213 | # Related: see my bash script template repo at https://iankelling.org/git. |
a5c2ac43 | 214 | # |
a5c2ac43 | 215 | # |
e58bbdf6 IK |
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>. | |
219 | # | |
220 | # Tested on bash 4.4.20(1)-release (x86_64-pc-linux-gnu). If you test |