use ruby style, support interactive shell
[errhandle.git] / err
1 #!/bin/bash
2 # Copyright 2018 Ian Kelling
3
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at
7
8 # http://www.apache.org/licenses/LICENSE-2.0
9
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
15
16
17 # Commentary: Bash stack trace and error handling functions. This file
18 # is meant to be sourced. It loads some functions which you may want to
19 # call manually (see the comments at the start of each one), and then
20 # runs err-catch. See the README file for a slightly longer explanation.
21
22
23 #######################################
24 # Print stack trace
25 #
26 # usage: err-bash-trace [MESSAGE]
27 #
28 # This function is called by the other functions which print stack
29 # traces.
30 #
31 # It does not show function args unless you first run:
32 # shopt -s extdebug
33 # which err-catch & err-print do for you.
34 #
35 # MESSAGE Message to print just before the stack trace.
36 #
37 # _frame_start Optional variable to set before calling. The frame to
38 # start printing on. default=1. Useful when printing from
39 # an ERR trap function to avoid printing that function.
40 #######################################
41 err-bash-trace() {
42 local -i argc_index=0 frame i start=${_frame_start:-1}
43 local source
44 if [[ $1 ]]; then
45 printf "%s\n" "$1"
46 fi
47 for ((frame=0; frame < ${#FUNCNAME[@]}; frame++)); do
48 argc=${BASH_ARGC[frame]}
49 argc_index+=$argc
50 ((frame < start)) && continue
51 if (( ${#BASH_SOURCE[@]} > 1 )); then
52 source="${BASH_SOURCE[frame]}:${BASH_LINENO[frame-1]}:"
53 fi
54 printf " from %sin \`%s" "$source" "${FUNCNAME[frame]}"
55 if shopt extdebug >/dev/null; then
56 for ((i=argc_index-1; i >= argc_index-argc; i--)); do
57 printf " %s" "${BASH_ARGV[i]}"
58 done
59 fi
60 echo \'
61 done
62 return 0
63 }
64
65 #######################################
66 # On error print stack trace and exit
67 #
68 # Globals:
69 # ${_errcatch_cleanup[@]} Optional command & args that will run before exiting
70 #######################################
71 err-catch() {
72 set -E; shopt -s extdebug
73 _err-trap() {
74 err=$?
75 exec >&2
76 set +x
77 local msg="${BASH_SOURCE[1]}:${BASH_LINENO[0]}: \`$BASH_COMMAND' returned $err"
78 if (( ${#FUNCNAME[@]} > 2 )); then
79 local _frame_start=2
80 err-bash-trace "$msg"
81 else
82 echo "$msg"
83 fi
84 set -e # err trap does not work within an error trap
85 # note :-: makes this compatible with set -u, assigns : if unset, but shellcheck
86 # doesn't understand that.
87 # shellcheck disable=SC2154
88 "${_errcatch_cleanup[@]:-:}"
89 echo "$0: exiting with status $err"
90 exit $err
91 }
92 trap _err-trap ERR
93 set -o pipefail
94 }
95
96
97 #######################################
98 # For interactive shells: on error, print stack trace and return
99 #
100 # Globals:
101 # _err_func_last Used internally.
102 # _err_catch_err Used internally.
103 # _err_catch_i Used internally.
104 #
105 # misc: All shellcheck disables for this function are false positives.
106 #######################################
107 # shellcheck disable=SC2120
108 err-catch-interactive() {
109 # shellcheck disable=SC2034
110 declare -i _err_func_last=0
111 set -E; shopt -s extdebug
112 # shellcheck disable=SC2154
113 trap '_err_catch_err=$? _trap_bc="$BASH_COMMAND"
114 if (( ${#FUNCNAME[@]} > _err_func_last )); then
115 echo ERR: \`$_trap_bc'"\'"' returned $_err_catch_err
116 fi
117 _err_func_last=${#FUNCNAME[@]}
118 if (( _err_func_last )); then
119 printf " from %s:%s:in \`%s" "${BASH_SOURCE[0]}" "$(declare -F "${FUNCNAME[0]}"|awk "{print \$2}")" "${FUNCNAME[0]}"
120 if shopt extdebug >/dev/null; then
121 for ((_err_catch_i=${BASH_ARGC[0]}-1; _err_catch_i >= 0; _err_catch_i--)); do
122 printf " %s" "${BASH_ARGV[_err_catch_i]}"
123 done
124 fi
125 echo '"\'"'
126 return $_err_catch_err
127 fi' ERR
128 set -o pipefail
129 }
130
131
132 #######################################
133 # Undoes err-catch. turns off exit and stack trace on error.
134 #######################################
135 err-allow() {
136 set +E +o pipefail; trap ERR
137 }
138
139 #######################################
140 # On error, print stack trace
141 #######################################
142 err-print() {
143 # help: on errors: print stack trace
144 #
145 # This function depends on err-bash-trace.
146
147 set -E; shopt -s extdebug
148 _err-trap() {
149 err=$?
150 exec >&2
151 set +x
152 echo "${BASH_SOURCE[1]}:${BASH_LINENO[0]}: \`$BASH_COMMAND' returned $err"
153 err-bash-trace 2
154 }
155 trap _err-trap ERR
156 set -o pipefail
157 }
158
159
160 #######################################
161 # Print stack trace and exit
162 #
163 # Use this instead of the exit command to be more informative.
164 #
165 # usage: err-exit [EXIT_CODE] [MESSAGE]
166 #
167 # EXIT_CODE Default is 1.
168 # MESSAGE Print MESSAGE to stderr. If only one of EXIT_CODE
169 # and MESSAGE is given, we consider it to be an
170 # exit code if it is a number.
171 #######################################
172 err-exit() {
173 exec >&2
174 code=1
175 if [[ "$*" ]]; then
176 if [[ ${1/[^0-9]/} == "$1" ]]; then
177 code=$1
178 if [[ $2 ]]; then
179 printf '%s\n' "$2" >&2
180 fi
181 else
182 printf '%s\n' "$0: $1" >&2
183 fi
184 fi
185 echo "${BASH_SOURCE[1]}:${BASH_LINENO[0]}"
186 err-bash-trace 2
187 echo "$0: exiting with code $code"
188 exit $err
189 }
190
191 # We want this more often than not, so run it now.
192 if [[ $- == *i* ]]; then
193 err-catch-interactive
194 else
195 err-catch
196 fi