misc new stuff
[automated-distro-installer] / fai / config / files / boot / bash-trace / DEFAULT
1 #!/bin/bash
2 # Copyright (C) 2019 Ian Kelling
3 # SPDX-License-Identifier: GPL-3.0-or-later
4
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.
8
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 }
111
112 #######################################
113 # Print stack trace
114 #
115 # usage: err-bash-trace [FRAME_START]
116 #
117 # This function is called by the other functions which print stack
118 # traces.
119 #
120 # It does not show function args unless you first run:
121 # shopt -s extdebug
122 # which err-catch does for you.
123 #
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.
129 #
130 #######################################
131 err-bash-trace() {
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
136 fi
137 for ((frame=0; frame < ${#FUNCNAME[@]}; frame++)); do
138 argc=${BASH_ARGC[frame]}
139 argc_index+=$argc
140 if ((frame < frame_start)); then continue; fi
141 if (( ${#BASH_SOURCE[@]} > 1 )); then
142 source_loc="${BASH_SOURCE[frame]}:${BASH_LINENO[frame-1]}:"
143 fi
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
148 done
149 fi
150 echo \' >&2
151 done
152 return 0
153 }
154
155 #######################################
156 # Internal function for err-catch. Prints stack trace from interactive
157 # shell trap.
158 #
159 # Usage: see err-catch-interactive
160 #######################################
161 _err-bash-trace-interactive() {
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
173 local ret bash_command argc pattern i last
174 last=$_err_func_last
175 _err_func_last=${#FUNCNAME[@]}
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=("$@")
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
187 fi
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
192 done
193 fi
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
208 fi
209 }
210
211 # Credits etc:
212 #
213 # Related: see my bash script template repo at https://iankelling.org/git.
214 #
215 #
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