Fix error with ssh, big refactor
[errhandle.git] / err
... / ...
CommitLineData
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#######################################
32err-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.
61err-catch
62
63#######################################
64# Undo err-catch/err-catch-interactive
65#######################################
66err-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#######################################
88err-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#######################################
131err-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