3be16713f9b27546cef83e7edbd2802dc13cd0e3
[errhandle.git] / err
1 #!/bin/bash
2 # Copyright (C) 2019 Ian Kelling
3 # SPDX-License-Identifier: GPL-3.0-or-later
4
5 # Commentary: Bash stack trace and error handling functions. This file
6 # is meant to be sourced. It loads some functions which you may want to
7 # call manually (see the comments at the start of each one), and then
8 # runs err-catch. See the README file for a slightly longer explanation.
9
10
11 #######################################
12 # Print stack trace
13 #
14 # usage: err-bash-trace [MESSAGE]
15 #
16 # This function is called by the other functions which print stack
17 # traces.
18 #
19 # It does not show function args unless you first run:
20 # shopt -s extdebug
21 # which err-catch & err-print do for you.
22 #
23 # MESSAGE Message to print just before the stack trace.
24 #
25 # _frame_start Optional variable to set before calling. The frame to
26 # start printing on. default=1. Useful when printing from
27 # an ERR trap function to avoid printing that function.
28 #######################################
29 err-bash-trace() {
30 local -i argc_index=0 frame i start=${_frame_start:-1}
31 local source
32 if [[ $1 ]]; then
33 printf "%s\n" "$1"
34 fi
35 for ((frame=0; frame < ${#FUNCNAME[@]}; frame++)); do
36 argc=${BASH_ARGC[frame]}
37 argc_index+=$argc
38 ((frame < start)) && continue
39 if (( ${#BASH_SOURCE[@]} > 1 )); then
40 source="${BASH_SOURCE[frame]}:${BASH_LINENO[frame-1]}:"
41 fi
42 printf " from %sin \`%s" "$source" "${FUNCNAME[frame]}"
43 if shopt extdebug >/dev/null; then
44 for ((i=argc_index-1; i >= argc_index-argc; i--)); do
45 printf " %s" "${BASH_ARGV[i]}"
46 done
47 fi
48 echo \'
49 done
50 return 0
51 }
52
53 #######################################
54 # On error print stack trace and exit
55 #
56 # Globals:
57 # errcatch-cleanup If set, this command will run just before exiting.
58 #######################################
59 err-catch() {
60 set -E;
61 # This condition avoids starting the bash debugger in the case that
62 # this is sourced from a startup file, and you use a login shell to
63 # run a command. Avoid doing that if you want function arguments in
64 # your trace.
65 if [[ $- != *c* ]] && ! shopt login_shell >/dev/null; then
66 shopt -s extdebug
67 fi
68 _err-trap() {
69 err=$?
70 exec >&2
71 set +x
72 local msg="${BASH_SOURCE[1]}:${BASH_LINENO[0]}: \`$BASH_COMMAND' returned $err"
73 if (( ${#FUNCNAME[@]} > 2 )); then
74 local _frame_start=2
75 err-bash-trace "$msg"
76 else
77 echo "$msg"
78 fi
79 set -e # err trap does not work within an error trap
80 if type -t errcatch-cleanup >/dev/null; then
81 errcatch-cleanup
82 fi
83 echo "$0: exiting with status $err"
84 exit $err
85 }
86 trap _err-trap ERR
87 set -o pipefail
88 }
89
90
91 #######################################
92 # For interactive shells: on error, print stack trace and return
93 #
94 # Globals:
95 # err_catch_ignore Array containing glob patterns to test against filenames to ignore
96 # errors from. Initialized to ignore bash-completion scripts on debian
97 # based systems.
98 # _err_func_last Used internally.
99 # _err_catch_err Used internally.
100 # _err_catch_i Used internally.
101 # _err_catch_ignore Used internally.
102 #
103 # misc: All shellcheck disables for this function are false positives.
104 #######################################
105 # shellcheck disable=SC2120
106 err-catch-interactive() {
107 err_catch_ignore=(
108 '/etc/bash_completion.d/*'
109 )
110 # shellcheck disable=SC2034
111 declare -i _err_func_last=0
112 set -E; shopt -s extdebug
113 # shellcheck disable=SC2154
114 trap '_err_catch_err=$? _trap_bc="$BASH_COMMAND"
115 _err_catch_ignore=false
116 for _err_catch_i in "${err_catch_ignore[@]}"; do
117 if [[ ${BASH_SOURCE[0]} == $_err_catch_i ]]; then
118 _err_catch_ignore=true
119 break
120 fi
121 done
122 if ! $_err_catch_ignore; then
123 if (( ${#FUNCNAME[@]} > _err_func_last )); then
124 echo ERR: \`$_trap_bc'"\'"' returned $_err_catch_err
125 fi
126 _err_func_last=${#FUNCNAME[@]}
127 if (( _err_func_last )); then
128 printf " from %s:%s:in \`%s" "${BASH_SOURCE[0]}" "$(declare -F "${FUNCNAME[0]}"|awk "{print \$2}")" "${FUNCNAME[0]}"
129 if shopt extdebug >/dev/null; then
130 for ((_err_catch_i=${BASH_ARGC[0]}-1; _err_catch_i >= 0; _err_catch_i--)); do
131 printf " %s" "${BASH_ARGV[_err_catch_i]}"
132 done
133 fi
134 echo '"\'"'
135 return $_err_catch_err
136 fi
137 fi' ERR
138 set -o pipefail
139 }
140
141
142 #######################################
143 # Undoes err-catch/err-catch-interactive
144 #######################################
145 err-allow() {
146 shopt -u extdebug
147 set +E +o pipefail
148 trap ERR
149 }
150
151 #######################################
152 # On error, print stack trace
153 #######################################
154 err-print() {
155 # help: on errors: print stack trace
156 #
157 # This function depends on err-bash-trace.
158
159 set -E; shopt -s extdebug
160 _err-trap() {
161 err=$?
162 exec >&2
163 set +x
164 echo "${BASH_SOURCE[1]}:${BASH_LINENO[0]}: \`$BASH_COMMAND' returned $err"
165 err-bash-trace 2
166 }
167 trap _err-trap ERR
168 set -o pipefail
169 }
170
171
172 #######################################
173 # Print stack trace and exit
174 #
175 # Use this instead of the exit command to be more informative.
176 #
177 # usage: err-exit [EXIT_CODE] [MESSAGE]
178 #
179 # EXIT_CODE Default is 1.
180 # MESSAGE Print MESSAGE to stderr. If only one of EXIT_CODE
181 # and MESSAGE is given, we consider it to be an
182 # exit code if it is a number.
183 #######################################
184 err-exit() {
185 exec >&2
186 code=1
187 if [[ "$*" ]]; then
188 if [[ ${1/[^0-9]/} == "$1" ]]; then
189 code=$1
190 if [[ $2 ]]; then
191 printf '%s\n' "$2" >&2
192 fi
193 else
194 printf '%s\n' "$0: $1" >&2
195 fi
196 fi
197 echo "${BASH_SOURCE[1]}:${BASH_LINENO[0]}"
198 err-bash-trace 2
199 echo "$0: exiting with code $code"
200 exit $err
201 }
202
203 # We want this more often than not, so run it now.
204 if [[ $- == *i* ]]; then
205 err-catch-interactive
206 else
207 err-catch
208 fi