Files
profile/profile.d/debug.sh
T

221 lines
8.9 KiB
Bash

#!/usr/bin/env bash
# ------------------------------------------------------------------------------
# Copyright (c) 2013-2026 Geoffray Levasseur <fatalerrors@geoffray-levasseur.org>
# 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.
# ------------------------------------------------------------------------------
# Common helpers (helpers/common.sh)
# shellcheck source=../helpers/common.sh
declare -F _profile_load_helpers >/dev/null || . "$(dirname -- "${BASH_SOURCE[0]}")/../helpers/common.sh"
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
# shellcheck disable=SC2120 # only --help is a possible argument
backtrace()
{
if [[ "${1:-}" == -h || "${1:-}" == --help ]]; then
printf "backtrace: print the current call stack, innermost frame first.\n"
printf "Usage: backtrace\n"
return 0
fi
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)
local -a _pargs=()
_getopt_split _pargs "$PARSED"
set -- "${_pargs[@]}"
# 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