cleanup documentation
[errhandle.git] / err
1 #!/bin/bash
2 # Bash Error Handler
3 # Copyright (C) 2020 Ian Kelling <ian@iankelling.org>
4 # SPDX-License-Identifier: GPL-3.0-or-later
5 #
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.
10 #
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.
15 #
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/>.
18
19
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.
25 #
26 # Please email me if you use this or have anything to contribute. I'm
27 # not aware of any users yet Ian Kelling <ian@iankelling.org>.
28 #
29 # Tested on bash 4.4.20(1)-release (x86_64-pc-linux-gnu).
30 #
31 # Related: see my bash script template repo at https://iankelling.org/git.
32
33
34 # TODO: investigate to see if we can format output betting in case of
35 # subshell failure. Right now, we get independent trace from inside and
36 # outside of the subshell. Note, errexit + inherit_errexit doesn't have
37 # any smarts around this either.
38
39 #######################################
40 # err-catch: Setup trap on ERR to print stack trace and exit (or return
41 # if the shell is interactive). This is the most common use case so we
42 # run it after defining it, you can call err-allow to undo that.
43 #
44 # This also sets pipefail because it's a good practice to catch more
45 # errors.
46 #
47 # Note: In interactive shell, stack calling line number is not
48 # available, so we print function definition lines.
49 #
50 # Globals
51 #
52 # err_catch_ignore Array containing glob patterns to test against
53 # filenames to ignore errors from in interactive
54 # shell. Initialized to ignore bash-completion
55 # scripts on debian based systems.
56 #
57 # err-cleanup If set, this command will run just before exiting.
58 #
59 # _err_func_last Used internally in err-bash-trace-interactive
60 #
61 #######################################
62 err-catch() {
63 set -E;
64 if [[ $- == *i* ]]; then
65 if ! test ${err_catch_ignore+defined}; then
66 err_catch_ignore=(
67 '/etc/bash_completion.d/*'
68 '*/bash-completion/*'
69 )
70 fi
71 declare -i _err_func_last=0
72 if [[ $- != *c* ]]; then
73 shopt -s extdebug
74 fi
75 # shellcheck disable=SC2154
76 trap '_err-bash-trace-interactive $? "${PIPESTATUS[*]}" "$BASH_COMMAND" ${BASH_ARGC[0]} "${BASH_ARGV[@]}" || return $?' ERR
77 else
78 # Man bash on exdebug: "If set at shell invocation, arrange to
79 # execute the debugger". We want to avoid that, but I want this file
80 # to be sourceable from bash startup files. noninteractive ssh and
81 # sources .bashrc on invocation. login_shell sources things on
82 # invocation.
83 #
84 # extdebug allows us to print function arguments in our stack trace.
85 if ! shopt login_shell >/dev/null && [[ ! $SSH_CONNECTION ]]; then
86 shopt -s extdebug
87 fi
88 trap err-exit ERR
89 fi
90 set -o pipefail
91 }
92 # This is the most common use case so run it now.
93 err-catch
94
95 #######################################
96 # Undo err-catch/err-catch-interactive
97 #######################################
98 err-allow() {
99 shopt -u extdebug
100 set +E +o pipefail
101 trap ERR
102 }
103
104 #######################################
105 # err-exit: Print stack trace and exit
106 #
107 # Use this instead of the exit command to be more informative.
108 #
109 # usage: err-exit [-EXIT_CODE] [MESSAGE]
110 #
111 # EXIT_CODE Default: $? if it is nonzero, otherwise 1.
112 # MESSAGE Print MESSAGE to stderr. Default:
113 # ${BASH_SOURCE[1]}:${BASH_LINENO[0]}: `$BASH_COMMAND' returned $?
114 #
115 # Globals
116 #
117 # err-cleanup If set, this command will run just before exiting.
118 #
119 #######################################
120 err-exit() {
121 local err=$? pipestatus="${PIPESTATUS[*]}"
122
123 # This has to come before most things or vars get changed
124 local msg="${BASH_SOURCE[1]}:${BASH_LINENO[0]}: \`$BASH_COMMAND' returned $err"
125 if [[ $pipestatus != "$err" ]]; then
126 msg+=", PIPESTATUS: $pipestatus"
127 fi
128 set +x
129 if [[ $1 == -* ]]; then
130 err=${1#-}
131 shift
132 elif (( ! err )); then
133 err=1
134 fi
135 if [[ $1 ]]; then
136 msg="$1"
137 fi
138 printf "%s\n" "$msg" >&2
139 err-bash-trace 2
140 set -e # err trap does not work within an error trap
141 if type -t err-cleanup >/dev/null; then
142 err-cleanup
143 fi
144 printf "%s: exiting with status %s\n" "$0" "$err" >&2
145 exit $err
146 }
147
148 #######################################
149 # Print stack trace
150 #
151 # usage: err-bash-trace [FRAME_START]
152 #
153 # This function is called by the other functions which print stack
154 # traces.
155 #
156 # It does not show function args unless you first run:
157 # shopt -s extdebug
158 # which err-catch does for you.
159 #
160 # FRAME_START Optional variable to set before calling. The frame to
161 # start printing on. default=1. If ${#FUNCNAME[@]} <=
162 # FRAME_START + 1, don't print anything because we are at
163 # the top level of the script and better off printing a
164 # general message, for example see what our callers print.
165 #
166 #######################################
167 err-bash-trace() {
168 local -i argc_index=0 frame i frame_start=${1:-1}
169 local source_loc
170 if (( ${#FUNCNAME[@]} <= frame_start + 1 )); then
171 return 0
172 fi
173 for ((frame=0; frame < ${#FUNCNAME[@]}; frame++)); do
174 argc=${BASH_ARGC[frame]}
175 argc_index+=$argc
176 if ((frame < frame_start)); then continue; fi
177 if (( ${#BASH_SOURCE[@]} > 1 )); then
178 source_loc="${BASH_SOURCE[frame]}:${BASH_LINENO[frame-1]}:"
179 fi
180 printf " from %sin \`%s" "$source_loc" "${FUNCNAME[frame]}" >&2
181 if shopt extdebug >/dev/null; then
182 for ((i=argc_index-1; i >= argc_index-argc; i--)); do
183 printf " %s" "${BASH_ARGV[i]}" >&2
184 done
185 fi
186 echo \' >&2
187 done
188 return 0
189 }
190
191 #######################################
192 # Internal function for err-catch. Prints stack trace from interactive
193 # shell trap.
194 #
195 # Usage: see err-catch-interactive
196 #######################################
197 _err-bash-trace-interactive() {
198 if (( ${#FUNCNAME[@]} <= 1 )); then
199 return 0
200 fi
201
202 for pattern in "${err_catch_ignore[@]}"; do
203 # shellcheck disable=SC2053
204 if [[ ${BASH_SOURCE[1]} == $pattern ]]; then
205 return 0
206 fi
207 done
208
209 local ret bash_command argc pattern i last
210 last=$_err_func_last
211 _err_func_last=${#FUNCNAME[@]}
212 # We have these passed to us because they are lost inside the
213 # function.
214 ret=$1
215 pipestatus="$2"
216 bash_command="$3"
217 argc=$(( $4 - 1 ))
218 shift 4
219 argv=("$@")
220 # The trap returns a nonzero, then gets called again. This condition
221 # tells us if is that has happened by checking if we've gone down a
222 # stack level.
223 if (( _err_func_last >= last )); then
224 printf "ERR: \`%s\' returned %s" "$bash_command" $ret >&2
225 if [[ $pipestatus != "$ret" ]]; then
226 printf ", PIPESTATUS: %s" "$pipestatus" >&2
227 fi
228 echo >&2
229 fi
230 printf " from \`%s" "${FUNCNAME[1]}" >&2
231 if shopt extdebug >/dev/null; then
232 for ((i=argc; i >= 0; i--)); do
233 printf " %s" "${argv[i]}" >&2
234 done
235 fi
236 printf "\' defined at %s:%s\n" "${BASH_SOURCE[1]}" "$(declare -F "${FUNCNAME[1]}"|awk "{print \$2}")" >&2
237 if [[ -t 1 ]]; then
238 return $ret
239 else
240 # Part of an outgoing pipe, avoid getting get us stuck in a weird
241 # subshell if we returned nonzero, which would happen in a situation
242 # like this:
243 #
244 # tf() { while read -r line; do :; done < <(asdf); };
245 # tf
246 #
247 # Note: exit $ret also avoids the stuck subshell problem, and I
248 # can't notice any difference, but this seems more proper.
249 return 0
250 fi
251 }
252