more accurate interactive trace, refactor
[errhandle.git] / err
CommitLineData
78a1a75c 1#!/bin/bash
fc2d9041
IK
2# Copyright (C) 2019 Ian Kelling
3# SPDX-License-Identifier: GPL-3.0-or-later
acd03e1e 4
acd03e1e
IK
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
a5c2ac43 10
a5c2ac43
IK
11#######################################
12# Print stack trace
13#
5a600375 14# usage: err-bash-trace [MESSAGE]
a5c2ac43 15#
62aed8de
IK
16# This function is called by the other functions which print stack
17# traces.
18#
a5c2ac43
IK
19# It does not show function args unless you first run:
20# shopt -s extdebug
62aed8de 21# which err-catch & err-print do for you.
a5c2ac43 22#
5a600375
IK
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.
a5c2ac43
IK
28#######################################
29err-bash-trace() {
5a600375 30 local -i argc_index=0 frame i start=${_frame_start:-1}
acd03e1e 31 local source
5a600375
IK
32 if [[ $1 ]]; then
33 printf "%s\n" "$1"
acd03e1e 34 fi
5a600375 35 for ((frame=0; frame < ${#FUNCNAME[@]}; frame++)); do
acd03e1e
IK
36 argc=${BASH_ARGC[frame]}
37 argc_index+=$argc
fb79b8bd 38 ((frame < start)) && continue
acd03e1e 39 if (( ${#BASH_SOURCE[@]} > 1 )); then
5a600375 40 source="${BASH_SOURCE[frame]}:${BASH_LINENO[frame-1]}:"
acd03e1e 41 fi
5a600375
IK
42 printf " from %sin \`%s" "$source" "${FUNCNAME[frame]}"
43 if shopt extdebug >/dev/null; then
acd03e1e
IK
44 for ((i=argc_index-1; i >= argc_index-argc; i--)); do
45 printf " %s" "${BASH_ARGV[i]}"
46 done
78a1a75c 47 fi
acd03e1e
IK
48 echo \'
49 done
a5c2ac43 50 return 0
78a1a75c 51}
364203b2 52
a5c2ac43
IK
53#######################################
54# On error print stack trace and exit
55#
56# Globals:
fc2d9041 57# errcatch-cleanup If set, this command will run just before exiting.
a5c2ac43 58#######################################
997eb0be 59err-catch() {
fc2d9041
IK
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.
5f3350ec 65 if [[ $- != *c* ]] || ! shopt login_shell >/dev/null; then
fc2d9041
IK
66 shopt -s extdebug
67 fi
acd03e1e
IK
68 _err-trap() {
69 err=$?
70 exec >&2
71 set +x
5a600375
IK
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
9ce3dca1 79 set -e # err trap does not work within an error trap
fc2d9041
IK
80 if type -t errcatch-cleanup >/dev/null; then
81 errcatch-cleanup
82 fi
5a600375 83 echo "$0: exiting with status $err"
acd03e1e
IK
84 exit $err
85 }
86 trap _err-trap ERR
87 set -o pipefail
78a1a75c 88}
364203b2 89
5a600375 90
5f3350ec
IK
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
5a600375
IK
128#######################################
129# For interactive shells: on error, print stack trace and return
130#
5f3350ec
IK
131# Note: calling line number is not available, so we print function
132# definition lines.
133#
5a600375 134# Globals:
dc069500
IK
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.
5f3350ec 138# _err_func_last Used internally in err-bash-trace-interactive
5a600375 139#
5a600375 140#######################################
5a600375 141err-catch-interactive() {
5f3350ec
IK
142 if ! test ${err_catch_ignore+defined}; then
143 err_catch_ignore=(
144 '/etc/bash_completion.d/*'
145 '*/bash-completion/*'
146 )
147 fi
5a600375
IK
148 declare -i _err_func_last=0
149 set -E; shopt -s extdebug
150 # shellcheck disable=SC2154
5f3350ec 151 trap '_err-bash-trace-interactive $? "$BASH_COMMAND" ${BASH_ARGC[0]} "${BASH_ARGV[@]}"' ERR
5a600375
IK
152 set -o pipefail
153}
154
155
62aed8de 156#######################################
dc069500 157# Undoes err-catch/err-catch-interactive
62aed8de
IK
158#######################################
159err-allow() {
dc069500
IK
160 shopt -u extdebug
161 set +E +o pipefail
162 trap ERR
62aed8de
IK
163}
164
a5c2ac43
IK
165#######################################
166# On error, print stack trace
167#######################################
9ce3dca1
IK
168err-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
a5c2ac43 186#######################################
62aed8de 187# Print stack trace and exit
a5c2ac43
IK
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#######################################
997eb0be 198err-exit() {
acd03e1e
IK
199 exec >&2
200 code=1
41be570a 201 if [[ "$*" ]]; then
acd03e1e
IK
202 if [[ ${1/[^0-9]/} == "$1" ]]; then
203 code=$1
204 if [[ $2 ]]; then
a5c2ac43 205 printf '%s\n' "$2" >&2
acd03e1e
IK
206 fi
207 else
a5c2ac43 208 printf '%s\n' "$0: $1" >&2
78a1a75c 209 fi
acd03e1e
IK
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
78a1a75c 215}
364203b2 216
a5c2ac43 217# We want this more often than not, so run it now.
5a600375
IK
218if [[ $- == *i* ]]; then
219 err-catch-interactive
220else
221 err-catch
222fi