Skip to content

Instantly share code, notes, and snippets.

@Potherca
Last active January 10, 2024 21:29
Show Gist options
  • Star 13 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save Potherca/4f4ce1c8d4bcf4cd4aab to your computer and use it in GitHub Desktop.
Save Potherca/4f4ce1c8d4bcf4cd4aab to your computer and use it in GitHub Desktop.
Sometimes you want to be able to debug a bash script. This gist gives an example of how to do this.

Introduction

Sometimes you want to be able to debug a bash script. Usually the -x option will suffice but sometimes something more sophisticated is needed.

In such instances using the DEBUG trap is often a good choice.

Attached to this gist is a example script to demonstrate how such a thing would work.

To easily demonstrate it's working, the DEBUG_LEVEL has been set as a parameter. In real live situations it would more likely be set as an environmental variable rather than passed in as a parameter.

Example output

Below is the output of the script when the script is run with various debug levels:

Error Output

$ bash /path/to/debug-example.sh

Errors occurred:

 Wrong parameter count

==============================================================================
                            DEBUG EXAMPLE SCRIPT
------------------------------------------------------------------------------
Usage: debug-example.sh <name> <debug-level>

This script gives an example of how built-in debugging can be implemented in
a bash script. It offers the infamous "Hello world!" functionality to
demonstrate it's workings.

This script requires at least one parameter: a string that will be output.
An optional second parameter can be given to set the debug level.
The default is set to 0, see below for other values:

DEBUG_LEVEL 0 = No Debugging
DEBUG_LEVEL 1 = Show Debug messages
DEBUG_LEVEL 2 = " and show Application Calls
DEBUG_LEVEL 3 = " and show called command
DEBUG_LEVEL 4 = " and show all other commands (=set +x)
DEBUG_LEVEL 5 = Show All Commands, without Debug Messages or Application Calls
==============================================================================

Debug Level 0 (the default)

 $ bash /path/to/debug-example.sh World
# Hello World!
# Done.

Debug Level 1

 $ bash /path/to/debug-example.sh World 1
# Debugging on - Debug Level : 1
# Hello World!
# Done.

Debug Level 2

 $ bash /path/to/debug-example.sh World 2
#[DEBUG] [debug-example.sh:179] [ ${g_iExitCode} -eq 0 ]
#[DEBUG] [debug-example.sh:181] run ${@:-}
# Debugging on - Debug Level : 2
# Hello World!
#[DEBUG] [debug-example.sh:183] [ ${#g_aErrorMessages[*]} -ne 0 ]
#[DEBUG] [debug-example.sh:186] message 'Done.'
# Done.
#[DEBUG] [debug-example.sh:192] echo -e "# ${@}" 1>&1

Debug Level 3

 $ bash /path/to/debug-example.sh World 3
+ declare -a g_aErrorMessages
+ declare -i g_iExitCode=0
+ declare -i g_iErrorCount=0
+ registerTraps
+ trap finish EXIT
+ '[' 3 -gt 1 ']'
+ '[' 3 -lt 5 ']'
+ trap '(debugTrapMessage "$(basename ${BASH_SOURCE[0]})" "${LINENO[0]}" "${BASH_COMMAND}");' DEBUG
+++ basename /path/to/debug-example.sh
++ debugTrapMessage debug-example.sh 179 '[ ${g_iExitCode} -eq 0 ]'
++ debug '[debug-example.sh:179] [ ${g_iExitCode} -eq 0 ]'
++ echo -e '#[DEBUG] [debug-example.sh:179] [ ${g_iExitCode} -eq 0 ]'
#[DEBUG] [debug-example.sh:179] [ ${g_iExitCode} -eq 0 ]
+ '[' 0 -eq 0 ']'
+++ basename /path/to/debug-example.sh
++ debugTrapMessage debug-example.sh 181 'run ${@:-}'
++ debug '[debug-example.sh:181] run ${@:-}'
++ echo -e '#[DEBUG] [debug-example.sh:181] run ${@:-}'
#[DEBUG] [debug-example.sh:181] run ${@:-}
+ run World 3
+ '[' 3 -gt 0 ']'
+ message 'Debugging on - Debug Level : 3'
+ echo -e '# Debugging on - Debug Level : 3'
# Debugging on - Debug Level : 3
+ '[' 2 -ne 1 ']'
+ '[' 2 -ne 2 ']'
+ message 'Hello World!'
+ echo -e '# Hello World!'
# Hello World!
+++ basename /path/to/debug-example.sh
++ debugTrapMessage debug-example.sh 183 '[ ${#g_aErrorMessages[*]} -ne 0 ]'
++ debug '[debug-example.sh:183] [ ${#g_aErrorMessages[*]} -ne 0 ]'
++ echo -e '#[DEBUG] [debug-example.sh:183] [ ${#g_aErrorMessages[*]} -ne 0 ]'
#[DEBUG] [debug-example.sh:183] [ ${#g_aErrorMessages[*]} -ne 0 ]
+ '[' 0 -ne 0 ']'
+++ basename /path/to/debug-example.sh
++ debugTrapMessage debug-example.sh 186 'message '\''Done.'\'''
++ debug '[debug-example.sh:186] message '\''Done.'\'''
++ echo -e '#[DEBUG] [debug-example.sh:186] message '\''Done.'\'''
#[DEBUG] [debug-example.sh:186] message 'Done.'
+ message Done.
+ echo -e '# Done.'
# Done.
+++ basename /path/to/debug-example.sh
++ debugTrapMessage debug-example.sh 192 'echo -e "# ${@}" 1>&1'
++ debug '[debug-example.sh:192] echo -e "# ${@}" 1>&1'
++ echo -e '#[DEBUG] [debug-example.sh:192] echo -e "# ${@}" 1>&1'
#[DEBUG] [debug-example.sh:192] echo -e "# ${@}" 1>&1
+ finish
+ '[' '!' 0 -eq 0 ']'
+ exit 0

Debug Level 4

 $ bash /path/to/debug-example.sh World 4
+ declare -a g_aErrorMessages
+ declare -i g_iExitCode=0
+ declare -i g_iErrorCount=0
+ registerTraps
+ trap finish EXIT
+ '[' 4 -gt 1 ']'
+ '[' 4 -lt 5 ']'
+ trap '(debugTrapMessage "$(basename ${BASH_SOURCE[0]})" "${LINENO[0]}" "${BASH_COMMAND}");' DEBUG
+++ basename /path/to/debug-example.sh
++ debugTrapMessage debug-example.sh 179 '[ ${g_iExitCode} -eq 0 ]'
++ debug '[debug-example.sh:179] [ ${g_iExitCode} -eq 0 ]'
++ echo -e '#[DEBUG] [debug-example.sh:179] [ ${g_iExitCode} -eq 0 ]'
#[DEBUG] [debug-example.sh:179] [ ${g_iExitCode} -eq 0 ]
+ '[' 0 -eq 0 ']'
+++ basename /path/to/debug-example.sh
++ debugTrapMessage debug-example.sh 181 'run ${@:-}'
++ debug '[debug-example.sh:181] run ${@:-}'
++ echo -e '#[DEBUG] [debug-example.sh:181] run ${@:-}'
#[DEBUG] [debug-example.sh:181] run ${@:-}
+ run World 4
+ '[' 4 -gt 0 ']'
+ message 'Debugging on - Debug Level : 4'
+ echo -e '# Debugging on - Debug Level : 4'
# Debugging on - Debug Level : 4
+ '[' 2 -ne 1 ']'
+ '[' 2 -ne 2 ']'
+ message 'Hello World!'
+ echo -e '# Hello World!'
# Hello World!
+++ basename /path/to/debug-example.sh
++ debugTrapMessage debug-example.sh 183 '[ ${#g_aErrorMessages[*]} -ne 0 ]'
++ debug '[debug-example.sh:183] [ ${#g_aErrorMessages[*]} -ne 0 ]'
++ echo -e '#[DEBUG] [debug-example.sh:183] [ ${#g_aErrorMessages[*]} -ne 0 ]'
#[DEBUG] [debug-example.sh:183] [ ${#g_aErrorMessages[*]} -ne 0 ]
+ '[' 0 -ne 0 ']'
+++ basename /path/to/debug-example.sh
++ debugTrapMessage debug-example.sh 186 'message '\''Done.'\'''
++ debug '[debug-example.sh:186] message '\''Done.'\'''
++ echo -e '#[DEBUG] [debug-example.sh:186] message '\''Done.'\'''
#[DEBUG] [debug-example.sh:186] message 'Done.'
+ message Done.
+ echo -e '# Done.'
# Done.
+++ basename /path/to/debug-example.sh
++ debugTrapMessage debug-example.sh 192 'echo -e "# ${@}" 1>&1'
++ debug '[debug-example.sh:192] echo -e "# ${@}" 1>&1'
++ echo -e '#[DEBUG] [debug-example.sh:192] echo -e "# ${@}" 1>&1'
#[DEBUG] [debug-example.sh:192] echo -e "# ${@}" 1>&1
+ finish
+ '[' '!' 0 -eq 0 ']'
+ exit 0

Debug Level 5

 $ bash /path/to/debug-example.sh World 5
+ declare -a g_aErrorMessages
+ declare -i g_iExitCode=0
+ declare -i g_iErrorCount=0
+ registerTraps
+ trap finish EXIT
+ '[' 5 -gt 1 ']'
+ '[' 5 -lt 5 ']'
+ '[' 0 -eq 0 ']'
+ run World 5
+ '[' 5 -gt 0 ']'
+ message 'Debugging on - Debug Level : 5'
+ echo -e '# Debugging on - Debug Level : 5'
# Debugging on - Debug Level : 5
+ '[' 2 -ne 1 ']'
+ '[' 2 -ne 2 ']'
+ message 'Hello World!'
+ echo -e '# Hello World!'
# Hello World!
+ '[' 0 -ne 0 ']'
+ message Done.
+ echo -e '# Done.'
# Done.
+ finish
+ '[' '!' 0 -eq 0 ']'
+ exit 0
#!/usr/bin/env bash
##==============================================================================
## DEBUG EXAMPLE SCRIPT
##------------------------------------------------------------------------------
#
# @NOTE: The variable naming scheme used in this code is an adaption of Systems
# Hungarian which is explained at http://pother.ca/VariableNamingConvention/
#
# ------------------------------------------------------------------------------
##
## Usage: debug-example.sh <name> <debug-level>
##
## This script gives an example of how built-in debugging can be implemented in
## a bash script. It offers the infamous "Hello world!" functionality to
## demonstrate it's workings.
##
## This script requires at least one parameter: a string that will be output.
## An optional second parameter can be given to set the debug level.
##
## The default is set to 0, see below for other values:
##
# ==============================================================================
# ==============================================================================
# CONFIG VARS
# ------------------------------------------------------------------------------
## DEBUG_LEVEL 0 = No Debugging
## DEBUG_LEVEL 1 = Show Debug messages
## DEBUG_LEVEL 2 = " and show Application Calls
## DEBUG_LEVEL 3 = " and show called command
## DEBUG_LEVEL 4 = " and show all other commands (=set +x)
## DEBUG_LEVEL 5 = Show All Commands, without Debug Messages or Application Calls
readonly DEBUG_LEVEL=${2:-0}
## ==============================================================================
# ==============================================================================
# APPLICATION VARS
# ------------------------------------------------------------------------------
# For all options see http://www.tldp.org/LDP/abs/html/options.html
set -o nounset # Exit script on use of an undefined variable, same as "set -u"
set -o errexit # Exit script when a command exits with non-zero status, same as "set -e"
set -o pipefail # Makes pipeline return the exit status of the last command in the pipe that failed
if [ "${DEBUG_LEVEL}" -gt 2 ];then
set -o xtrace # Similar to -v, but expands commands, same as "set -x"
fi
declare -a g_aErrorMessages
declare -i g_iExitCode=0
declare -i g_iErrorCount=0
# ==============================================================================
# ==============================================================================
# Displays all lines in main script that start with '##'
# ------------------------------------------------------------------------------
usage() {
[ "$*" ] && echo "$(basename $0): $*"
sed -n '/^##/,/^$/s/^## \{0,1\}//p' "$0"
} #2>/dev/null
# ==============================================================================
# ==============================================================================
# Store given message in the ErrorMessage array
# ------------------------------------------------------------------------------
function error() {
if [ ! -z ${2:-} ];then
g_iExitCode=${2}
elif [ ${g_iExitCode} -eq 0 ];then
g_iExitCode=64
fi
g_iErrorCount=$((${g_iErrorCount}+1))
g_aErrorMessages[${g_iErrorCount}]="${1}\n"
return ${g_iExitCode};
}
# ==============================================================================
# ==============================================================================
function message() {
# ------------------------------------------------------------------------------
echo -e "# ${@}" >&1
}
# ==============================================================================
# ==============================================================================
# Output all given Messages to STDERR
# ------------------------------------------------------------------------------
function outputErrorMessages() {
echo -e "\nErrors occurred:\n\n ${@}" >&2
}
# ==============================================================================
# ==============================================================================
# @see: http://www.tldp.org/LDP/Bash-Beginners-Guide/html/sect_02_03.html
# ------------------------------------------------------------------------------
function debug() {
echo -e "#[DEBUG] ${1}" >&1
}
# ==============================================================================
# ==============================================================================
# ------------------------------------------------------------------------------
function debugMessage() {
if [ "${DEBUG_LEVEL}" -gt 0 ] && [ "${DEBUG_LEVEL}" -lt 5 ];then
debug "${1}"
fi
}
# ==============================================================================
# ==============================================================================
# ------------------------------------------------------------------------------
function debugTrapMessage {
debug "[${1}:${2}] ${3}"
}
# ==============================================================================
# ==============================================================================
# ------------------------------------------------------------------------------
function finish() {
if [ ! ${g_iExitCode} -eq 0 ];then
outputErrorMessages ${g_aErrorMessages[*]}
if [ ${g_iExitCode} -eq 65 ];then
usage
fi
fi
exit ${g_iExitCode}
}
# ==============================================================================
# ==============================================================================
# ------------------------------------------------------------------------------
function registerTraps() {
trap finish EXIT
if [ "${DEBUG_LEVEL}" -gt 1 ] && [ "${DEBUG_LEVEL}" -lt 5 ];then
# Trap function is defined inline so we get the correct line number
#trap '(echo -e "#[DEBUG] [$(basename ${BASH_SOURCE[0]}):${LINENO[0]}] ${BASH_COMMAND}");' DEBUG
trap '(debugTrapMessage "$(basename ${BASH_SOURCE[0]})" "${LINENO[0]}" "${BASH_COMMAND}");' DEBUG
fi
}
# ==============================================================================
# ==============================================================================
# ------------------------------------------------------------------------------
function run() {
if [ "${DEBUG_LEVEL}" -gt 0 ];then
message "Debugging on - Debug Level : ${DEBUG_LEVEL}"
fi
# Your logic goes here
if [ "$#" -ne 1 ] && [ "$#" -ne 2 ];then
g_iExitCode=65
error 'Wrong parameter count'
usage
else
message "Hello ${1}!"
fi
}
# ==============================================================================
# ==============================================================================
# RUN LOGIC
# ------------------------------------------------------------------------------
registerTraps
if [ ${g_iExitCode} -eq 0 ];then
run ${@:-}
if [ ${#g_aErrorMessages[*]} -ne 0 ];then
outputErrorMessages "${g_aErrorMessages[*]}"
else
message 'Done.'
fi
fi
# ==============================================================================
#EOF
@Jeffer-Lin
Copy link

Jeffer-Lin commented May 2, 2021

You are good at bash debug. It's so tiny and lite weight. I like it !
Could you help me to understand below code snippet and How about printf %.0s 's meaning ?
Thanks.

seq=`seq $num`
next=`printf '../%.0s' {$seq}`
cd $next

@Potherca
Copy link
Author

Potherca commented May 3, 2021

@Jeffer-Lin For future reference, you might want to ask questions such as this on https://stackoverflow.com/.

Moving on... First of all, you might want to use a tool like https://www.shellcheck.net/ to lint your code... This would lead to a cleaner version:

seq="$(seq "${num}")"
next="$(printf '../%.s' ${seq})"
cd "${next}"

Second, using https://explainshell.com/ can help you get an idea of what a specific command does. It also has links to the manual of each command.

Finally:

  • seq prints out a sequence of numbers for a given value:

    $ seq 3
    1
    2
    3
    
  • printf prints any parameters it gets in the format given as a first parameter.
    Fot instance printf '%s' 1 2 3 will output ../1../2../3. The meaning of the dot . in %.0s is to set a minimum and maximum length of the string to output. It should actually be %0.0, meaning output exactly zero characters. But omitting either min or max sets its value to 0.
    So for a num of 3, printf will output: ``../../../

  • cd changes the current working directory to what it is given, so for a num of 3, it will attempt to go up 3 directories.

Putting these things together you have a piece of code that will change the current directory to its parent at the level of num.

@Jeffer-Lin
Copy link

Jeffer-Lin commented May 3, 2021

Nice reply. You give me a kindly guide line to better understand Bash shell script.
https://stackoverflow.com <-- It's always give me tons of codesnippet , but sometime mixed with partial correct answer (most danger)
so I must one by one to filter out, and select a better match answer to my case, and try it debug it .
https://www.shellcheck.net/ give me a ton of fixed suggest @@@, I must spend time to rewrite my code.
I'm familiar with C / JAVA , seems script language give user too many sugar syntax.
However I write the code into .bashrc shell handtool
.. N to switch back to N level directory. It's my code at bottom.
I've another question ,
${LINENO} give me the function relative line number , If any method to get the absolute lineNumber in .bashrc

toN()  # toN num default
{
    local count_len=${#1} 
    [[ $count_len != 0 ]]  ||  return ${2}
    [[ ${1} =~ [[:digit:]]{${count_len}} ]]  ||  return "0" 
    return ${1}
}


..() {
    toN "$1" "1"
    num=$?
    if [[ $num -gt 0 ]] ; then
      seq=`seq $num`
      next=`printf '../%.0s' {$seq}`
      cd $next
    else
	echo "Usage: ${FUNCNAME} [1..N]"
	echo "     to change to previous N level directory"
        [[ "$1" =~ "--debug" ]] || return        
	echo "*** ${BASH_SOURCE}  ${FUNCNAME}():${LINENO} th line ***"
    fi
}

@Potherca
Copy link
Author

Potherca commented May 3, 2021

Hmmm. LINENO should already give you the absolute line-number. 🤔

If you call your code with -x (for instance bash -x .bashrc you will see all the commands executed by bash. By default each line is prefixed with a + but this can be changed by setting the PS4 variable, for instance PS4='${BASH_SOURCE}:${LINENO} '.

This will allow you to see what line-number in .bashrc is what:

PS4='${BASH_SOURCE}:${LINENO}' bash -x  .bashrc

This should match with whatever line-number your code echo "*** ${BASH_SOURCE} ${FUNCNAME}():${LINENO} th line ***" outputs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment