#!/usr/bin/env bash # ------------------------------------------------------------------------------ # Copyright (c) 2013-2026 Geoffray Levasseur # Protected by the BSD3 license. Please read bellow for details. # # * Redistribution and use in source and binary forms, # * with or without modification, are permitted provided # * that the following conditions are met: # * # * Redistributions of source code must retain the above # * copyright notice, this list of conditions and the # * following disclaimer. # * # * Redistributions in binary form must reproduce the above # * copyright notice, this list of conditions and the following # * disclaimer in the documentation and/or other materials # * provided with the distribution. # * # * Neither the name of the copyright holder nor the names # * of any other contributors may be used to endorse or # * promote products derived from this software without # * specific prior written permission. # * # * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND # * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, # * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES # * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR # * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # * OF SUCH DAMAGE. # ------------------------------------------------------------------------------ load_conf "debug" # ------------------------------------------------------------------------------ # Display a backtrace of the current call stack (innermost frame first), one # line per frame: function, file where it is defined, line of the call site. # Usage: backtrace backtrace() { printf "========= Call stack =========\n" local i=1 # We begin at 1 to ignore backtrace itself while [[ $i -lt ${#FUNCNAME[@]} ]]; do # BASH_SOURCE[i] is the file defining FUNCNAME[i], and BASH_LINENO[i-1] # the line of that file from which FUNCNAME[i-1] was called. printf '%15s() %s:%d\n' \ "${FUNCNAME[$i]}" "${BASH_SOURCE[$i]}" "${BASH_LINENO[$(( i-1 ))]}" i=$(( i + 1 )) done printf "==============================\n" } export -f backtrace # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # Handler installed by settrace as ERR trap: report the failure and the call # stack, then give back the exit status of the failed command so the trap does # not alter the shell behaviour (e.g. under "set -e"). # Everything goes to stderr: with errtrace a failure inside $(...) or a pipe # would otherwise inject the trace into the captured output. # Exported because an exported settrace may be used from child bash processes, # where the trap needs this function to exist. error() { local errcode=$? printf "Command failed with exit status %d\n" "$errcode" >&2 backtrace >&2 return $errcode } export -f error # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # Activate or deactivate error trapping to display backtrace # Usage: settrace <--on|--off|--status> [--force] # Startup behaviour is set by SETTRACE_DEFAULT_ENABLED in the [debug] section. settrace() { local PARSED PARSED=$(getopt -oh --long help,on,off,status,force -n settrace -- "$@") # shellcheck disable=SC2181 # getopt return code is checked immediately after if [[ $? -ne 0 ]]; then disp E "Invalid options, use \"settrace --help\" to display usage." return 1 fi # getopt emits a shell-quoted, normalised argument list ("--" ends options) eval set -- "$PARSED" # First pass: gather the requested action and flags. --on, --off and # --status are mutually exclusive; --force only modifies --on and --off. local action="" force=0 while true; do case $1 in -h|--help) printf "Try to activate backtrace display for script debugging.\n\n" printf "Options:\n" printf "\t--on\t\tActivate backtrace generation\n" printf "\t--force\t\tReplace (with --on) or remove (with --off) an ERR trap\n" printf "\t\t\tnot installed by settrace\n" printf "\t--off\t\tDeactivate backtrace generation\n" printf "\t--status\tDisplay whether the ERR trap is set\n\n" printf "That function activates a trap event on error. If the script you want to\n" printf "debug overloads the ERR bash trap, it will not work.\n\n" printf "To activate it at every interactive startup, set SETTRACE_DEFAULT_ENABLED=1\n" printf "in the [debug] section of the profile configuration.\n" return 0 ;; --on|--off|--status) if [[ -n $action ]]; then disp E "The --on, --off and --status options are mutually exclusive." return 1 fi action="${1#--}" shift ;; --force) force=1 shift ;; --) shift break ;; *) disp E "Invalid options, use \"settrace --help\" to display usage." return 1 ;; esac done # Second pass: evaluate the current trap state, then apply the action. # "trap -p ERR" prints the exact command line that re-creates the trap, so # comparing it tells our own trap ("on") from one set by someone else # ("foreign"); empty output means no trap at all ("off"). # Limitation: bash hides the ERR trap from code running in a function (and # its subshells) unless errtrace is set. Our own trap is always seen since # settrace sets errtrace with it, but a foreign trap set without errtrace is # reported as "off" (and can then be replaced without --force). local status="off" current current=$(trap -p ERR) if [[ -n $current ]]; then status="foreign" [[ $current == "trap -- 'error' ERR" ]] && status="on" fi case $action in on) if [[ $status == "on" ]]; then disp W "ERR signal trap is already set." return 0 fi if [[ $status == "foreign" && $force -eq 0 ]]; then disp E "Another ERR signal trap is already set. Use --force to replace it." return 1 fi # errtrace makes functions and subshells inherit the ERR trap set -o errtrace trap 'error' ERR ;; off) if [[ $status == "foreign" && $force -eq 0 ]]; then disp E "The ERR signal trap was not set by settrace. Use --force to remove it." return 1 fi [[ $status == "off" ]] && disp W "ERR signal trap is already unset!" trap - ERR set +o errtrace ;; status) case $status in foreign) disp I "Trap signal on error is set, but not by settrace." ;; *) disp I "Trap signal on error is ${status}." ;; esac ;; "") disp E "No action specified, use \"settrace --help\" to display usage." return 1 ;; esac } export -f settrace # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # Optional trace activation at startup (SETTRACE_DEFAULT_ENABLED, default 0). # Interactive shells only: this file is sourced by non-interactive shells too, # where an ERR trap would add noise to scripts. [[ $- == *i* ]] is used because # profile.sh sets INTERACTIVE only after the modules are loaded. # disp is not defined yet (modules load alphabetically), so the trap is set only # when none exists, a path that never calls disp. An existing trap is left alone. # Written as "if" so the sourcing status stays 0 when nothing is done. case "${SETTRACE_DEFAULT_ENABLED:-0}" in 1|[Yy][Ee][Ss]|[Tt][Rr][Uu][Ee]|[Oo][Nn]) if [[ $- == *i* && -z $(trap -p ERR) ]]; then settrace --on fi ;; esac # ------------------------------------------------------------------------------ # EOF