From e2e76ed34cee0ff25fd137701e09b44508dbf1d1 Mon Sep 17 00:00:00 2001 From: Ian Kelling Date: Mon, 19 Jan 2026 08:35:14 -0500 Subject: [PATCH] better handling of collision with a theoretical third party err trap --- bash-bear | 81 ++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 60 insertions(+), 21 deletions(-) diff --git a/bash-bear b/bash-bear index 5f79f52..611d604 100644 --- a/bash-bear +++ b/bash-bear @@ -2,7 +2,7 @@ # # Bash Bear Trap: error handling with stack traces. # -# Copyright (C) 2023 Free Software Foundation +# Copyright (C) 2026 Free Software Foundation # # Note: the FSF recommends copyleft licensing for programs > 300 lines, see # https://www.gnu.org/licenses/license-recommendations.en.html. @@ -34,10 +34,13 @@ # It is also good for your .bashrc: in an interactive shell, it returns from # functions instead of exiting. # -# If err-cleanup is a command, it runs that before the stack +# If err-cleanup is an alias or function it runs that before the stack # trace. Functions are documented inline below for additional use cases. # -# +# Note: we set a few global settings which are not used by normal bash +# programs such as trap on ERR, but of course it is possible for them to +# conflict. They are described in err-catch function documentation +# below. # # Version: 1.0 # @@ -50,20 +53,28 @@ # * 5.1.16(1)-release (x86_64-pc-linux-gnu). -# TODO: investigate to see if we can format output betting in case of +# TODO: investigate to see if we can format output better in case of # subshell failure. Right now, we get independent trace from inside and # outside of the subshell. Note, errexit + inherit_errexit doesn't have # any smarts around this either. +# shellcheck disable=SC2317 + if ! test "$BASH_VERSION"; then echo "error: shell is not bash" >&2; exit 1; fi ####################################### -# err-catch: Setup trap on ERR to print stack trace and exit (or return +# err-catch: [-f] Setup trap on ERR to print stack trace and exit (or return # if the shell is interactive). This is the most common use case so we -# run it after defining it, you can call err-allow to undo that. +# run it at the end of the file. You can call err-allow to undo that. +# +# -f Overwrite any existing ERR trap. Otherwise, if we see an ERR trap +# that isn't the same string as what we would set, return an error. # -# This also sets pipefail because it's a good practice to catch more -# errors. +# This sets pipefail because it is a good practice to catch more errors. +# +# This sets extdebug with shopt -s extdebug. tldr of extdebug is +# "behavior intended for use by debuggers". Bash-bear-trap will work +# without this, but it won't print function arguments in stack traces. # # Note: In interactive shell, stack calling line number is not # available, so we print function definition lines. @@ -79,15 +90,36 @@ if ! test "$BASH_VERSION"; then echo "error: shell is not bash" >&2; exit 1; fi # shell. Initialized to ignore bash-completion # scripts on debian based systems. # -# err-cleanup If set, this command will run just before exiting. +# err-cleanup If this is an alias or function, it will run just before exiting. # This does nothing in an interactive shell, I'm not # sure if I could make it work there. # # _err_func_last Used internally in err-bash-trace-interactive # ####################################### +# shellcheck disable=SC2120 # expected because it is a library. err-catch() { - set -E; + local err_trap_cmd existing_err_trap overwrite_err=false + set -E + if [[ $# == 1 && $1 == -f ]]; then + overwrite_err=true + fi + + if ! $overwrite_err; then + if [[ $- == *i* ]]; then + # shellcheck disable=SC2016 + err_trap_cmd='trap -- '\''_err-bash-trace-interactive $? "${PIPESTATUS[*]}" "$BASH_COMMAND" ${BASH_ARGC[0]} "${BASH_ARGV[@]}" || return $?'\'' ERR' + else + err_trap_cmd="trap -- 'err-exit' ERR" + fi + + existing_err_trap=$(trap -p ERR 2>&1) + if [[ $existing_err_trap && $existing_err_trap != "$err_trap_cmd" ]]; then + printf "%s\n" "bash-bear: error: found different existing ERR trap. rerun with -f to overwrite it." + return 1 + fi + fi + if [[ $- == *i* ]]; then if ! test ${err_catch_ignore+defined}; then err_catch_ignore=( @@ -99,6 +131,8 @@ err-catch() { if [[ $- != *c* ]]; then shopt -s extdebug fi + + # shellcheck disable=SC2154 trap '_err-bash-trace-interactive $? "${PIPESTATUS[*]}" "$BASH_COMMAND" ${BASH_ARGC[0]} "${BASH_ARGV[@]}" || return $?' ERR else @@ -109,15 +143,13 @@ err-catch() { # invocation. # # extdebug allows us to print function arguments in our stack trace. - if ! shopt login_shell >/dev/null && [[ ! $SSH_CONNECTION ]]; then + if ! shopt login_shell >/dev/null && [[ ! -v SSH_CONNECTION && ! $SSH_CONNECTION ]]; then shopt -s extdebug fi trap err-exit ERR fi set -o pipefail } -# This is the most common use case so run it now. -err-catch ####################################### # Undo err-catch/err-catch-interactive @@ -141,29 +173,29 @@ err-allow() { # # Globals # -# err-cleanup If set, this command will run just before exiting. +# err-cleanup If set to an alias of function, it will run just before exiting. # ####################################### err-exit() { # vars have _ prefix so that we can inspect existing set vars without # too much overwriting of them. - local _err=$? _pipestatus="${PIPESTATUS[*]}" + local cleanup_type _err=$? _pipestatus="${PIPESTATUS[*]}" # This has to come before most things or vars get changed local _msg="${BASH_SOURCE[1]}:${BASH_LINENO[0]}: \`$BASH_COMMAND' returned $_err" local _cmdr="$BASH_COMMAND" # command right. we chop of the left, keep the right. - if [[ $_pipestatus && $_pipestatus != "$_err" ]]; then + if [[ -v _pipestatus && $_pipestatus && $_pipestatus != "$_err" ]]; then _msg+=", PIPESTATUS: $_pipestatus" fi set +x - if [[ $1 == -* ]]; then - _err=${1#-} + if [[ $# -ge 1 && $1 == -* ]]; then + _err="${1#-}" shift elif (( ! _err )); then _err=1 fi - if [[ $1 ]]; then + if (( $# )); then _msg="$1" fi @@ -201,7 +233,9 @@ err-exit() { printf "%s\n" "$_msg" >&2 err-bash-trace 2 set -e # err trap does not work within an error trap - if type -t err-cleanup >/dev/null; then + + cleanup_type=$(type -t err-cleanup 2>/dev/null) + if [[ $cleanup_type == alias || $cleanup_type == function ]]; then err-cleanup fi printf "%s: exiting with status %s\n" "$0" "$_err" >&2 @@ -258,6 +292,8 @@ err-bash-trace() { # Usage: see err-catch-interactive ####################################### _err-bash-trace-interactive() { + local pattern ret pipestatus bash_command argc argv + local -i ret if (( ${#FUNCNAME[@]} <= 1 )); then return 0 fi @@ -269,7 +305,7 @@ _err-bash-trace-interactive() { fi done - local ret bash_command argc pattern i last + local last ret pipestatus bash_command argc argv pattern i last=$_err_func_last _err_func_last=${#FUNCNAME[@]} # We have these passed to us because they are lost inside the @@ -312,3 +348,6 @@ _err-bash-trace-interactive() { return 0 fi } + +# This is the most common use case so run it now. +err-catch -- 2.25.1