|
#!/usr/bin/env bash |
|
set -uo pipefail |
|
IFS=$'\n\t' |
|
set +o noclobber |
|
|
|
# ----------------------------------------------------------------------------- |
|
# @file debug_script.sh |
|
# @brief A template script with integrated debug functionality. |
|
# @details This script is designed to be used as a building block for |
|
# developers who wish to integrate debugging capabilities into their |
|
# own scripts. It provides a mechanism for enabling and passing debug |
|
# information throughout the script, making it easy to trace function |
|
# calls and view debug messages during execution. |
|
# |
|
# The script uses the `debug` flag, which can be passed to the script |
|
# or individual functions. By using the variable `$debug` in all |
|
# function calls, developers can propagate debug printing throughout |
|
# the call chain to sub-functions, helping them identify issues or |
|
# inspect the flow of execution. |
|
# |
|
# The primary features of this script are: |
|
# - Debug messages are printed if the `debug` flag is provided, |
|
# either at the script level or for individual functions. |
|
# - The `debug` flag is passed to all sub-functions if it is included |
|
# in the function call. |
|
# - Developers can control the verbosity of the debug output by |
|
# simply adding the `debug` flag to the function calls they want |
|
# to debug. |
|
# |
|
# The `debug_print` function logs a debug message if the `debug` |
|
# flag is set. For example, calling `debug_print "This is a test" |
|
# "$debug"` will print the message `"This is a test"` to stderr if |
|
# `debug` is passed to the function. |
|
# |
|
# The `debug_end` function is used at the end of a function to log |
|
# the function's exit if `debug` is set. Calling `debug_end "$debug"` |
|
# prints the function's exit message, including the function name and |
|
# line number, if the `debug` flag is passed. |
|
# |
|
# @usage |
|
# Example 1: Run the entire script with debug output. |
|
# ./debug_script.sh debug |
|
# |
|
# Example 2: Run a specific function with debug output by appending the debug |
|
# flag. |
|
# ./debug_script.sh debug {other args} |
|
# |
|
# Example 3: Use the debug flag in individual function calls. |
|
# debug="debug" |
|
# test "$debug" |
|
# |
|
# @note |
|
# - The script uses the `debug` flag to toggle debug output. |
|
# - If the `debug` flag is provided when calling the script, debug output is |
|
# generated for the entire execution. |
|
# - Each function uses `local debug=$(debug_start "$@")` to leverage the local |
|
# `$debug` variable. |
|
# - Subsequently, the optional `eval set -- "$(debug_filter "$@")"` line will |
|
# remove the debug argument from the arguments passed to the function, |
|
# eliminating the need to account for it further. |
|
# - If `debug` is passed to specific functions, debug messages related to those |
|
# functions and all sub-calls will be printed. |
|
# - The `debug_print` function logs a debug message if the `debug` flag is set. |
|
# For example, `debug_print "This is a test" "$debug"` will print the message |
|
# `"This is a test"` if `debug` is passed. |
|
# - The `debug_end` function logs the function's exit if the `debug` flag is |
|
# set. Using `debug_end "$debug"` at the end of a function will print a message |
|
# indicating the function's exit along with the function name and line number. |
|
# ----------------------------------------------------------------------------- |
|
|
|
# ----------------------------------------------------------------------------- |
|
# @brief Determines the script name to use. |
|
# @details This block of code determines the value of `THIS_SCRIPT` based on |
|
# the following logic: |
|
# 1. If `THIS_SCRIPT` is already set in the environment, it is used. |
|
# 2. If `THIS_SCRIPT` is not set, the script checks if |
|
# `${BASH_SOURCE[0]}` is available: |
|
# - If `${BASH_SOURCE[0]}` is set and not equal to `"bash"`, the |
|
# script extracts the filename (without the path) using |
|
# `basename` and assigns it to `THIS_SCRIPT`. |
|
# - If `${BASH_SOURCE[0]}` is unbound or equals `"bash"`, it falls |
|
# back to using the value of `FALLBACK_SCRIPT_NAME`, which |
|
# defaults to `debug_print.sh`. |
|
# |
|
# @var FALLBACK_SCRIPT_NAME |
|
# @brief Default name for the script in case `BASH_SOURCE[0]` is unavailable. |
|
# @details This variable is used as a fallback value if `BASH_SOURCE[0]` is |
|
# not set or equals `"bash"`. The default value is `"debug_print.sh"`. |
|
# |
|
# @var THIS_SCRIPT |
|
# @brief Holds the name of the script to use. |
|
# @details The script attempts to determine the name of the script to use. If |
|
# `THIS_SCRIPT` is already set in the environment, it is used |
|
# directly. Otherwise, the script tries to extract the filename from |
|
# `${BASH_SOURCE[0]}` (using `basename`). If that fails, it defaults |
|
# to `FALLBACK_SCRIPT_NAME`. |
|
# ----------------------------------------------------------------------------- |
|
declare FALLBACK_SCRIPT_NAME="${FALLBACK_SCRIPT_NAME:-debug_print.sh}" |
|
if [[ -z "${THIS_SCRIPT:-}" ]]; then |
|
if [[ -n "${BASH_SOURCE[0]:-}" && "${BASH_SOURCE[0]:-}" != "bash" ]]; then |
|
# Use BASH_SOURCE[0] if it is available and not "bash" |
|
THIS_SCRIPT=$(basename "${BASH_SOURCE[0]}") |
|
else |
|
# If BASH_SOURCE[0] is unbound or equals "bash", use FALLBACK_SCRIPT_NAME |
|
THIS_SCRIPT="${FALLBACK_SCRIPT_NAME}" |
|
fi |
|
fi |
|
|
|
# ----------------------------------------------------------------------------- |
|
# @brief Starts the debug process. |
|
# @details This function checks if the "debug" flag is present in the |
|
# arguments, and if so, prints the debug information including the |
|
# function call and the line number. |
|
# |
|
# @param "$@" Arguments to check for the "debug" flag. |
|
# @return The "debug" flag if present, or an empty string if not. |
|
# ----------------------------------------------------------------------------- |
|
debug_start() { |
|
local debug="" |
|
local args=() # Array to hold non-debug arguments |
|
for arg in "$@"; do |
|
if [[ "$arg" == "debug" ]]; then |
|
debug="debug" |
|
break # Exit the loop as soon as we find "debug" |
|
fi |
|
done |
|
|
|
# Handle empty or unset FUNCNAME and BASH_LINENO gracefully |
|
local func_name="${FUNCNAME[1]:-main}" |
|
local caller_name="${FUNCNAME[2]:-main}" |
|
local caller_line=${BASH_LINENO[0]:-0} |
|
|
|
# Print debug information if the flag is set |
|
if [[ "$debug" == "debug" ]]; then |
|
printf "[DEBUG:%s] Starting function %s() called by %s():%d.\n" \ |
|
"$THIS_SCRIPT" "$func_name" "$caller_name" "$caller_line" >&2 |
|
fi |
|
|
|
# Return debug flag if present |
|
printf "%s\n" "${debug:-}" |
|
return 0 |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# @brief Filters out the "debug" flag from the arguments. |
|
# @details This function removes the "debug" flag from the list of arguments |
|
# and returns the filtered arguments. The debug flag is not passed |
|
# to other functions. |
|
# |
|
# @param "$@" Arguments to filter. |
|
# @return Filtered arguments, excluding "debug". |
|
# ----------------------------------------------------------------------------- |
|
debug_filter() { |
|
local args=() |
|
for arg in "$@"; do |
|
[[ "$arg" == "debug" ]] || args+=("$arg") |
|
done |
|
printf "%q " "${args[@]}" |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# @brief Prints a debug message if the debug flag is set. |
|
# @details This function checks if the "debug" flag is present in the arguments |
|
# and, if so, prints the provided debug message along with the function |
|
# and line number where it was called. |
|
# |
|
# @param "$@" Arguments to check for the "debug" flag and message. |
|
# @global debug Debug flag, passed from the calling function. |
|
# @return None |
|
# ----------------------------------------------------------------------------- |
|
debug_print() { |
|
local debug="" |
|
local args=() # Array to hold non-debug arguments |
|
for arg in "$@"; do |
|
if [[ "$arg" == "debug" ]]; then |
|
debug="debug" |
|
else |
|
args+=("$arg") # Add non-debug arguments to the array |
|
fi |
|
done |
|
# Restore positional parameters |
|
set -- "${args[@]}" |
|
|
|
# Handle empty or unset FUNCNAME and BASH_LINENO gracefully |
|
local caller_name="${FUNCNAME[1]:-main}" |
|
local caller_line=${BASH_LINENO[0]:-0} |
|
|
|
# Assign the remaining argument to the message. Defaults to <unset> |
|
local message="${1:-<unset>}" |
|
|
|
# Print debug information if the flag is set |
|
if [[ "$debug" == "debug" ]]; then |
|
printf "[DEBUG:%s] Message: '%s' sent by %s():%d.\n" \ |
|
"$THIS_SCRIPT" "$message" "$caller_name" "$caller_line" >&2 |
|
fi |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# @brief Ends the debug process. |
|
# @details This function checks if the "debug" flag is present in the arguments |
|
# and, if so, prints the debug information indicating the exit of |
|
# the function, along with the function name and line number. |
|
# |
|
# @param "$@" Arguments to check for the "debug" flag. |
|
# @global debug Debug flag, passed from the calling function. |
|
# @return None |
|
# ----------------------------------------------------------------------------- |
|
debug_end() { |
|
local debug="" |
|
local args=() # Array to hold non-debug arguments |
|
for arg in "$@"; do |
|
if [[ "$arg" == "debug" ]]; then |
|
debug="debug" |
|
break # Exit the loop as soon as we find "debug" |
|
fi |
|
done |
|
|
|
# Handle empty or unset FUNCNAME and BASH_LINENO gracefully |
|
local func_name="${FUNCNAME[1]:-main}" |
|
local caller_name="${FUNCNAME[2]:-main}" |
|
local caller_line=${BASH_LINENO[0]:-0} |
|
|
|
# Print debug information if the flag is set |
|
if [[ "$debug" == "debug" ]]; then |
|
printf "[DEBUG:%s] Exiting function %s() called by %s():%d.\n" \ |
|
"$THIS_SCRIPT" "$func_name" "$caller_name" "$caller_line" >&2 |
|
fi |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# @brief Runs a simple test with debug output. |
|
# @details This function calls `debug_print` to show how a debug message is |
|
# printed and demonstrates the debug process. |
|
# |
|
# @param "$@" Arguments to be passed to `debug_start`, `debug_filter`, |
|
# `debug_print`, and `debug_end`. |
|
# @return Returns the status code from the test. |
|
# ----------------------------------------------------------------------------- |
|
test() { |
|
local debug=$(debug_start "$@") |
|
eval set -- "$(debug_filter "$@")" |
|
local retval=0 |
|
|
|
# You can use this as a template for each of your new functions. |
|
|
|
debug_end "$debug" |
|
return "$retval" |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# @brief Main function to run the script. |
|
# @details This function may be renamed to anything you like, except for |
|
# `main()`. If you update the name, be sure to update the name of |
|
# `_main` in the line/function `main() { _main "$@"; return "$?"; }`. |
|
# |
|
# @param "$@" Arguments to be passed to `_main`. |
|
# @return Returns the status code from `_main`. |
|
# ----------------------------------------------------------------------------- |
|
_main() { |
|
# This first line captures the debug variable for the function and logs a |
|
# debug line if it is present. |
|
local debug=$(debug_start "$@") |
|
|
|
# This second line strips the debug variable out of the arguments to prevent |
|
# interference with any subsequent argument processing within this function. |
|
eval set -- "$(debug_filter "$@")" |
|
|
|
# We assume this function will have a return valiue. Be sure to declare |
|
# all variables local to teh function as `local`. |
|
local retval=0 |
|
|
|
# This line shows how a debug message is printed. Adding "$debug" to the end |
|
# makes it conditional; it will only print if the debug flag is set. |
|
debug_print "This is a test" "$debug" |
|
|
|
# If you call additional functions, always pass "$debug" to have the debug |
|
# argument follow the execution path from where you turn it on. |
|
# |
|
# If you did not execute the script with "debug" as an argument, simply call |
|
# the function where you want debugging to start with |
|
# |
|
# {function_name} debug |
|
# |
|
test "$debug" |
|
|
|
# This line logs an "Exiting function ..." message with the line number equal |
|
# to the line after the return. If you have no return line after this, it |
|
# will point to the next line, which may be the closing brace for the function |
|
# or a blank line. |
|
debug_end "$debug" |
|
return "$retval" |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# @brief Main function entry point. |
|
# @details This function calls `_main` to initiate the script execution. By |
|
# calling `main`, we enable the correct reporting of the calling |
|
# function in Bash, ensuring that the stack trace and function call |
|
# are handled appropriately during the script execution. |
|
# |
|
# @param "$@" Arguments to be passed to `_main`. |
|
# @return Returns the status code from `_main`. |
|
# ----------------------------------------------------------------------------- |
|
main() { _main "$@"; return "$?"; } |
|
|
|
# Call the main function |
|
debug=$(debug_start "$@") # Print start message if "debug" is passed to the script |
|
eval set -- "$(debug_filter "$@")" # Strip "debug" from args if it exists |
|
retval=0 ; main "$@" "$debug" ; retval="$?" # Start main and return value |
|
debug_end "$debug" # Log an exit debug message if debug is set |
|
exit "$retval" # Exit with a return value |