more accurate interactive trace, refactor
[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 # Internal function for err-catch-interactive.
93 # Prints stack trace from interactive shell trap.
94 # Usage: see err-catch-interactive
95 #######################################
96
97 _err-bash-trace-interactive() {
98 local ret bash_command argc pattern i
99 # We have these passed to us because they are lost inside the
100 # function.
101 ret=$1
102 bash_command="$2"
103 argc=$(( $3 - 1 ))
104 shift 3
105 argv=("$@")
106 for pattern in "${err_catch_ignore[@]}"; do
107 # shellcheck disable=SC2053
108 if [[ ${BASH_SOURCE[0]} == $pattern ]]; then
109 return 0
110 fi
111 done
112 if (( ${#FUNCNAME[@]} > _err_func_last )); then
113 echo ERR: \`$bash_command\' returned $ret
114 fi
115 _err_func_last=${#FUNCNAME[@]}
116 if (( _err_func_last > 1 )); then
117 printf " from \`%s" "${FUNCNAME[1]}"
118 if shopt extdebug >/dev/null; then
119 for ((i=argc; i >= 0; i--)); do
120 printf " %s" "${argv[i]}"
121 done
122 fi
123 printf "\' defined at %s:%s\n" "${BASH_SOURCE[1]}" "$(declare -F "${FUNCNAME[1]}"|awk "{print \$2}")"
124 return $ret
125 fi
126 }
127
128 #######################################
129 # For interactive shells: on error, print stack trace and return
130 #
131 # Note: calling line number is not available, so we print function
132 # definition lines.
133 #
134 # Globals:
135 # err_catch_ignore Array containing glob patterns to test against filenames to ignore
136 # errors from. Initialized to ignore bash-completion scripts on debian
137 # based systems.
138 # _err_func_last Used internally in err-bash-trace-interactive
139 #
140 #######################################
141 err-catch-interactive() {
142 if ! test ${err_catch_ignore+defined}; then
143 err_catch_ignore=(
144 '/etc/bash_completion.d/*'
145 '*/bash-completion/*'
146 )
147 fi
148 declare -i _err_func_last=0
149 set -E; shopt -s extdebug
150 # shellcheck disable=SC2154
151 trap '_err-bash-trace-interactive $? "$BASH_COMMAND" ${BASH_ARGC[0]} "${BASH_ARGV[@]}"' ERR
152 set -o pipefail
153 }
154
155
156 #######################################
157 # Undoes err-catch/err-catch-interactive
158 #######################################
159 err-allow() {
160 shopt -u extdebug
161 set +E +o pipefail
162 trap ERR
163 }
164
165 #######################################
166 # On error, print stack trace
167 #######################################
168 err-print() {
169 # help: on errors: print stack trace
170 #
171 # This function depends on err-bash-trace.
172
173 set -E; shopt -s extdebug
174 _err-trap() {
175 err=$?
176 exec >&2
177 set +x
178 echo "${BASH_SOURCE[1]}:${BASH_LINENO[0]}: \`$BASH_COMMAND' returned $err"
179 err-bash-trace 2
180 }
181 trap _err-trap ERR
182 set -o pipefail
183 }
184
185
186 #######################################
187 # Print stack trace and exit
188 #
189 # Use this instead of the exit command to be more informative.
190 #
191 # usage: err-exit [EXIT_CODE] [MESSAGE]
192 #
193 # EXIT_CODE Default is 1.
194 # MESSAGE Print MESSAGE to stderr. If only one of EXIT_CODE
195 # and MESSAGE is given, we consider it to be an
196 # exit code if it is a number.
197 #######################################
198 err-exit() {
199 exec >&2
200 code=1
201 if [[ "$*" ]]; then
202 if [[ ${1/[^0-9]/} == "$1" ]]; then
203 code=$1
204 if [[ $2 ]]; then
205 printf '%s\n' "$2" >&2
206 fi
207 else
208 printf '%s\n' "$0: $1" >&2
209 fi
210 fi
211 echo "${BASH_SOURCE[1]}:${BASH_LINENO[0]}"
212 err-bash-trace 2
213 echo "$0: exiting with code $code"
214 exit $err
215 }
216
217 # We want this more often than not, so run it now.
218 if [[ $- == *i* ]]; then
219 err-catch-interactive
220 else
221 err-catch
222 fi