#!/bin/bash # # This library contains an implementation of a stack trace for Bash scripts. # os::log::stacktrace::install installs the stacktrace as a handler for the ERR signal if one # has not already been installed and sets `set -o errtrace` in order to propagate the handler # If the ERR trap is not initialized, installing this plugin will initialize it. # # Globals: # None # Arguments: # None # Returns: # - export OS_USE_STACKTRACE function os::log::stacktrace::install() { # setting 'errtrace' propagates our ERR handler to functions, expansions and subshells set -o errtrace # OS_USE_STACKTRACE is read by os::util::trap at runtime to request a stacktrace export OS_USE_STACKTRACE=true os::util::trap::init_err } readonly -f os::log::stacktrace::install # os::log::stacktrace::print prints the stacktrace and exits with the return code from the script that # called for a stack trace. This function will always return 0 if it is not handling the signal, and if it # is handling the signal, this function will always `exit`, not return, the return code it receives as # its first argument. # # Globals: # - BASH_SOURCE # - BASH_LINENO # - FUNCNAME # Arguments: # - 1: the return code of the command in the script that generated the ERR signal # - 2: the last command that ran before handlers were invoked # - 3: whether or not `set -o errexit` was set in the script that generated the ERR signal # Returns: # None function os::log::stacktrace::print() { local return_code=$1 local last_command=$2 local errexit_set=${3:-} if [[ "${return_code}" = "0" ]]; then # we're not supposed to respond when no error has occurred return 0 fi if [[ -z "${errexit_set}" ]]; then # if errexit wasn't set in the shell when the ERR signal was issued, then we can ignore the signal # as this is not cause for failure return 0 fi # iterate backwards through the stack until we leave library files, so we can be sure we start logging # actual script code and not this handler's call local stack_begin_index for (( stack_begin_index = 0; stack_begin_index < ${#BASH_SOURCE[@]}; stack_begin_index++ )); do if [[ ! "${BASH_SOURCE[${stack_begin_index}]}" =~ hack/lib/(log/stacktrace|util/trap)\.sh ]]; then break fi done local preamble_finished local stack_index=1 local i for (( i = stack_begin_index; i < ${#BASH_SOURCE[@]}; i++ )); do local bash_source bash_source="$( os::util::repository_relative_path "${BASH_SOURCE[$i]}" )" if [[ -z "${preamble_finished:-}" ]]; then preamble_finished=true os::log::error "PID ${BASHPID:-$$}: ${bash_source}:${BASH_LINENO[$i-1]}: \`${last_command}\` exited with status ${return_code}." >&2 os::log::info $'\t\t'"Stack Trace: " >&2 os::log::info $'\t\t'" ${stack_index}: ${bash_source}:${BASH_LINENO[$i-1]}: \`${last_command}\`" >&2 else os::log::info $'\t\t'" ${stack_index}: ${bash_source}:${BASH_LINENO[$i-1]}: ${FUNCNAME[$i-1]}" >&2 fi stack_index=$(( stack_index + 1 )) done # we know we're the privileged handler in this chain, so we can safely exit the shell without # starving another handler of the privilege of reacting to this signal os::log::info " Exiting with code ${return_code}." >&2 exit "${return_code}" } readonly -f os::log::stacktrace::print