Skip to content

Instantly share code, notes, and snippets.

@lbussy
Last active December 30, 2024 17:26
Show Gist options
  • Save lbussy/859880aeef4ad04280fb832c3797ba1d to your computer and use it in GitHub Desktop.
Save lbussy/859880aeef4ad04280fb832c3797ba1d to your computer and use it in GitHub Desktop.
A framework for providing valuable debug printing during Bash developement, without having to remove it all manually when you are done.

Debug Script Example

This script demonstrates how to implement debug functionality to a Bash script in a simple, extensible manner. It provides a mechanism to enable and pass debug information throughout the script, allowing developers to trace function calls and view debug messages during execution.

How is this different than just dropping echo everywhere?

I'm glad you asked. Invariably, you forget to find and delete those pesky notes. You will probbaly just:

echo "Foo"

You don't see anything wrong with that, do you? I'll tell you what's wrong with that. It prints to stdout. You will blow up your script if you have functions that return text with echo or printf.

When you are done using debug or want to turn them ALL off temporarily to see it work as you intended, don't pass "debug."

Features

  • Debugging: The script prints debug messages when the debug flag is passed to the script or individual functions.
  • Debug Propagation: If included in the function call, the debug flag is conditionally passed to all sub-functions.
  • Control Debug Verbosity: Developers can control the verbosity of debug output by adding the debug flag to specific functions.

How it works

  • The script uses the debug flag, which can be passed to the script or to individual functions.
  • Using the variable $debug in all function calls, you can propagate debug printing throughout the call chain to sub-functions.
  • The debug_print function will print debug messages to stderr when the debug flag is set.
  • The debug_end function will log the exit of a function with the function's name and line number when the debug flag is passed.

Example Usage

1. Run the entire script with debug output:

./debug_script.sh debug

2. Run a specific function with debug output:

./debug_script.sh debug {other args}

3. Use the debug flag in individual function calls:

debug="debug"
test "$debug"

4. Exemplar Function

test() {
    local debug=$(debug_start "$@")
    eval set -- "$(debug_filter "$@")"
    local retval=0

    debug_print "This is a debug message" "$debug"

    debug_end "$debug"
    return "$retval"
}

5. Example Output

Here's what this script looks like when run as-is:

pi@pi:~ $ ./debug_print.sh debug
[DEBUG:debug_print.sh] Starting function main() called by main():309.
[DEBUG:debug_print.sh] Starting function _main() called by main():264.
[DEBUG:debug_print.sh] Message: 'This is a test' sent by _main():276.
[DEBUG:debug_print.sh] Starting function test() called by _main():242.
[DEBUG:debug_print.sh] Exiting function test() called by _main():248.
[DEBUG:debug_print.sh] Exiting function _main() called by main():292.
[DEBUG:debug_print.sh] Exiting function main() called by main():312.
pi@pi:~ $

Notes

  • The script uses the debug flag to toggle debug output.
  • If the debug flag is provided when calling the script, the debug output is generated for the entire execution.
  • Each function uses local debug=$(debug_start "$@") to leverage the local $debug variable.
  • 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.
#!/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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment