Fix error with ssh, big 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
e58bbdf6
IK
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.
acd03e1e 8
e58bbdf6
IK
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}
a5c2ac43 111
a5c2ac43
IK
112#######################################
113# Print stack trace
114#
e58bbdf6 115# usage: err-bash-trace [FRAME_START]
a5c2ac43 116#
62aed8de
IK
117# This function is called by the other functions which print stack
118# traces.
119#
a5c2ac43
IK
120# It does not show function args unless you first run:
121# shopt -s extdebug
e58bbdf6 122# which err-catch does for you.
a5c2ac43 123#
e58bbdf6
IK
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.
5a600375 129#
a5c2ac43
IK
130#######################################
131err-bash-trace() {
e58bbdf6
IK
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
acd03e1e 136 fi
5a600375 137 for ((frame=0; frame < ${#FUNCNAME[@]}; frame++)); do
acd03e1e
IK
138 argc=${BASH_ARGC[frame]}
139 argc_index+=$argc
e58bbdf6 140 if ((frame < frame_start)); then continue; fi
acd03e1e 141 if (( ${#BASH_SOURCE[@]} > 1 )); then
e58bbdf6 142 source_loc="${BASH_SOURCE[frame]}:${BASH_LINENO[frame-1]}:"
acd03e1e 143 fi
e58bbdf6 144 printf " from %sin \`%s" "$source_loc" "${FUNCNAME[frame]}" >&2
5a600375 145 if shopt extdebug >/dev/null; then
acd03e1e 146 for ((i=argc_index-1; i >= argc_index-argc; i--)); do
e58bbdf6 147 printf " %s" "${BASH_ARGV[i]}" >&2
acd03e1e 148 done
78a1a75c 149 fi
e58bbdf6 150 echo \' >&2
acd03e1e 151 done
a5c2ac43 152 return 0
78a1a75c 153}
364203b2 154
a5c2ac43 155#######################################
e58bbdf6
IK
156# Internal function for err-catch. Prints stack trace from interactive
157# shell trap.
a5c2ac43 158#
5f3350ec
IK
159# Usage: see err-catch-interactive
160#######################################
5f3350ec 161_err-bash-trace-interactive() {
55f98600
IK
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
3fb24829
IK
173 local ret bash_command argc pattern i last
174 last=$_err_func_last
175 _err_func_last=${#FUNCNAME[@]}
5f3350ec
IK
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=("$@")
3fb24829
IK
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
e58bbdf6 186 printf "ERR: \`%s\' returned %s\n" "$bash_command" $ret >&2
5f3350ec 187 fi
e58bbdf6 188 printf " from \`%s" "${FUNCNAME[1]}" >&2
3fb24829
IK
189 if shopt extdebug >/dev/null; then
190 for ((i=argc; i >= 0; i--)); do
e58bbdf6 191 printf " %s" "${argv[i]}" >&2
3fb24829 192 done
5f3350ec 193 fi
e58bbdf6
IK
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
5f3350ec 208 fi
9ce3dca1
IK
209}
210
e58bbdf6 211# Credits etc:
a5c2ac43 212#
e58bbdf6 213# Related: see my bash script template repo at https://iankelling.org/git.
a5c2ac43 214#
a5c2ac43 215#
e58bbdf6
IK
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