CHAPTER 9|Debugging Shell Programs
This chapter looks at some useful features that you can use to debug shell programs.We’ll look at how you can utilize them in the first part of this chapter. We’ll then look at some powerful new features of bash, not present in most Bourne shell workalikes, which will help in building a shell script debugging tool. At the end of the chapter, we’ll show step by step how to build a debugger for bash. The debugger,called bashdb, is a basic yet functional program that will not only serve as an extended example of various shell programming techniques, but will also provide you with a useful tool for examining the workings of your own shell scripts.
Basic Debugging Aids
Set Options
set -o option |
Command-line option |
Action |
noexec | -n | Don’t run commands; check for syntax errors only |
verbose | -v | Echo commands before running them |
xtrace | -x | Echo commands after command-line processing |
Fake Signals
four fake signals available in bash.
Fake signal |
Sent when |
EXIT |
The shell exits from script |
ERR |
A command returning a non-zero exit status |
DEBUG |
The shell has executed a statement. The DEBUG signal is not available in bash versions prior to 2.0. |
RETURN |
A shell function or a script executed with the . or source builtins finishes executing. The RETURN signal is not available in bash versions prior to 3.0. |
EXIT
#when the number is correct, the echo message won't be showed in the terminal, but exit the terminal directly
cor@debian:~/shell/mar17$ cat 1.sh trap 'echo Thank you for playing!' EXIT magicnum=$(($RANDOM%10+1)) echo 'Guess a number between 1 and 10:' echo "magicnum = "$magicnum while read -p 'Guess: ' guess;do sleep 1 if [ "$guess" = $magicnum ];then echo 'Right!' exit fi echo 'Wrong!' done
#when the number is correct, the echo message won't be showed in the terminal, but exit the terminal directly
cor@debian:~/shell/mar17$ . 1.sh
Guess a number between 1 and 10:
magicnum = 3
Guess: 1
Wrong!
Guess: 2
Wrong!
Guess: 4
Wrong!
Guess: 5
Wrong!
Guess: 6
Wrong!
Guess: 7
Wrong!
Guess: 8
Wrong!
Guess: 9
Wrong!
Guess: 10
Wrong!
Guess:
# it seems the quickly exit is because it goes too fast, let me try adding a sleep there:
trap 'echo Thank you for playing!' EXIT magicnum=$(($RANDOM%10+1)) echo 'Guess a number between 1 and 10:' echo "magicnum = "$magicnum while read -p 'Guess: ' guess;do sleep 1 if [ "$guess" = $magicnum ];then echo 'Right!' sleep 20 exit fi echo 'Wrong!' done
#yes that's it:
Guess a number between 1 and 10: magicnum = 5 Guess: 3 Wrong! Guess: 4 Wrong! Guess: 5 Right!
ERR
DEBUG
For example, you notice the value of a particular variable is running amok. The naive approach is to put in a lot of echo statements to check the variable’s value at several points. The DEBUG trap makes this easier by letting you do this:
function dbgtrap { echo "badvar is $badvar" } trap dbgtrap DEBUG ...section of code in which the problem occurs... trap - DEBUG # turn off the DEBUG trap
This code will print the value of the wayward variable before every statement between the two traps.
One important point to remember when using DEBUG is that it is not inherited by functions called from the shell in which it is set. In other words, if your shell sets a DEBUG trap and then calls a function, the statements within the function will not execute the trap. There are three ways around this. Firstly you can set a trap for DEBUG explicitly within the function. Alternately you can declare the function with the -t option which turns on debug inheritance in functions and allows a function to inherit a DEBUG trap from the caller. Lastly you can use set -o functrace (or set -T)which does the same thing as declare but applies to all functions
RETURN
As with DEBUG, the RETURN trap is not inherited by functions. You again have the options of setting the trap for RETURN within the function, declare the function with the -t option so that that function inherits the trap, or use set -o functrace to turn on the inheritance for all functions.
Here is a simple example of a RETURN trap:
function returntrap { echo "A return occurred" } trap returntrap RETURN function hello { echo "hello world" } hello
Debugging Variables
Bash 3.0 added some useful environment variables to aid in writing a debugger.These include BASH_SOURCE, which contains an array of filenames that correspond to what is currently executing; BASH_LINENO, which is an array of line numbers that correspond to function calls that have been made; BASH_ARGC and BASH_ARGV array variables, the first holding the number of parameters in each frame and the second the parameters themselves.We’ll now look at writing a debugger, although we’ll keep things simple and avoid using these variables. This also means the debugger will work with earlier versions of bash.
A bash Debugger
Specifically, we’ll provide the ability to:
• Specify places in the program at which to stop execution. These are called breakpoints.
• Execute a specified number of statements in the program. This is called stepping.
• Examine and change the state of the program during its execution. This includes being able to print out the values of variables and change them when the program is stopped at a breakpoint or after stepping.
• Print out the source code we are debugging along with indications of where breakpoints are and what line in the program we are currently executing.
• Provide the debugging capability without having to change the original source code of the program we wish to debug in any way.
As you will see, the capability to do all of these things (and more) is easily provided by the constructs and methods we have seen in previous chapters.
Structure of the Debugger
The driver script
The driver script is responsible for setting everything up. It is a script called bashdb and looks like this:
# bashdb - a bash debugger # Driver Script: concatenates the preamble and the target script # and then executes the new script. echo 'bash Debugger version 1.0' _dbname=${0##*/} if (( $# < 1 )) ; then echo "$_dbname: Usage: $_dbname filename" >&2 exit 1 fi _guineapig=$1 if [ ! -r $1 ]; then echo "$_dbname: Cannot read file '$_guineapig'." >&2 exit 1 fi shift _tmpdir=/tmp _libdir=. _debugfile=$_tmpdir/bashdb.$$ # temporary file for script that is being debugged cat $_libdir/bashdb.pre $_guineapig > $_debugfile exec bash $_debugfile $_guineapig $_tmpdir $_libdir "$@"
exec
The last line runs the newly created script with exec, a statement we haven’t discussed yet. We’ve chosen to wait until now to introduce it because—as we think you’ll agree—it can be dangerous. exec takes its arguments as a command line and runs the command in place of the current program, in the same process. In other words, a shell that runs exec will terminate immediately and be replaced by exec’s arguments.
The Preamble
Now we’ll look at the code that gets prepended to the guinea pig script; we call this the preamble. It’s kept in the file bashdb.pre and looks like this:
# # # # # # bashdb preamble This file gets prepended to the shell script being debugged. Arguments: $1 = the name of the original guinea pig script $2 = the directory where temporary files are stored $3 = the directory where bashdb.pre and bashdb.fns are stored _debugfile=$0 _guineapig=$1 _tmpdir=$2 _libdir=$3 shift 3 source $_libdir/bashdb.fns _linebp= let _trace=0 let _i=1 while read; do _lines[$_i]=$REPLY let _i=$_i+1 done < $_guineapig trap _cleanup EXIT let _steps=1 trap '_steptrap $(( $LINENO -29 ))' DEBUG
Debugger Functions
The function _steptrap is the entry point into the debugger; it is defined in the file bashdb.fns. Here is _steptrap:
# After each line of the test script is executed the shell traps to # this function. function _steptrap { _curline=$1 # the number of the line that just ran (( $_trace )) && _msg "$PS4 line $_curline: ${_lines[$_curline]}" if (( $_steps >= 0 )); then let _steps="$_steps - 1" fi # First check to see if a line number breakpoint was reached. # If it was, then enter the debugger. if _at_linenumbp ; then _msg "Reached breakpoint at line $_curline" _cmdloop # It wasn't, so check whether a break condition exists and is true. # If it is, then enter the debugger. elif [ -n "$_brcond" ] && eval $_brcond; then _msg "Break condition $_brcond true at line $_curline" _cmdloop # It wasn't, so check if we are in step mode and the number of steps # is up. If it is then enter the debugger. elif (( $_steps == 0 )); then _msg "Stopped at line $_curline" _cmdloop fi }
Commands
We will explain shortly how _steptrap determines these things; now we will look at _cmdloop. It’s a simple combination of the case statements we saw in Chapter 5,and the calculator loop we saw in the previous chapter.
# The Debugger Command Loop function _cmdloop { local cmd args while read -e -p "bashdb> " cmd args;do case $cmd in ?|h ) _menu ;; #print command menu bc ) _setbc $args ;; #set a break condition bp ) _setbp $args ;; #set a breakpoint at hte given #line cb ) _clearbp $args ;; #clear on or all breakpoints ds ) _displayscript ;; #list the script and show the #breakpoints g ) return ;; #"go": start/resume execution of #the script q ) exit ;; #quit s ) let _steps=${args:-1} # single step N times # (default =1) return x ) _xtrace ;; # toggle execution trace !* ) eval ${cmd#!} $args ;; # pass to the shell * ) _msg "Invalid command: '$cmd'" ;; esac done }
summarizes the debugger commands.
Command |
Action |
bp N |
Set breakpoint at line N |
bp |
List breakpoints and break condition |
bc string |
Set break condition to string |
bc |
Clear break condition |
cb N |
Clear breakpoint at line N |
cb |
Clear all breakpoints |
ds |
Display the test script and breakpoints |
g |
Start/resume execution |
s [N] |
Execute N statements (default 1) |
x |
Toggle execution trace on/off |
h, ? |
Print the help menu |
! string |
Pass string to a shell |
q |
Quit |