1.1 A Sample BASH Debugger Session

You can use this manual at your leisure to read all about the BASH debugger. However, a handful of commands are enough to get started using the debugger. This chapter illustrates those commands.

Below we will debug a script that contains a function to compute the factorial of a number: fact(0) is 1 and fact(n) is n*fact(n-1).

$ bashdb -L .  /tmp/fact.sh
Bourne-Again Shell Debugger, release bash-5.2-1.1.2
Copyright 2002, 2003, 2004, 2006, 2007, 2008, 2009, 2011 Rocky Bernstein
This is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.

(/tmp/fact.sh:9):
  9:	echo fact 0 is: `fact 0`
bashdb<0> -
  1:    #!/usr/local/bin/bash
  2:    fact() {
  3:    ((n==0)) && echo 1 && return
  4:    ((nm1=n-1))
  5:    ((result=n*`fact $nm1`))
  6:    echo $result
  7:    }
  8:
  9:==> echo fact 0 is: `fact 0`
bashdb<1> list
 10:   echo fact 3 is: $(fact 3)

The command invocation uses the option “-L .” Here we assume that the bashdb script and the debugger files are in the same location. If you are running from the source code, this will be the case. However if bashdb has been installed this probably won’t be true and here you probably don’t need to use “-L .” Instead you would type simply bashdb /tmp/fact.sh.

Position information consists of a filename and line number, e.g. (/tmp/fact.sh:9) and is given parenthesis. This position format is similar to that used in a dozen or so other debuggers; GNU Emacs and DDD can parse this format.

In the first debugger command we gave -, we listed a window of lines before where we were executing. Because the window, 10 lines, is larger than the number of lines to the top of the file we printed only 9 lines here. The next command, list, starts from the current line and again wants to print 10 lines but because there are only one remaining line, that is what is printed.

bashdb<2> step
(/tmp/fact.sh:9):
fact 0
9:	echo fact 0 is: `fact 0`
bashdb<(3)> RET
2:	fact() {
bashdb<(4)> RET
3:	((n==0)) && echo 1 && return
bashdb<(5)> print $n

bashdb<(6)>

Ooops... The variable n isn’t initialized.1

The first step command steps the script one instruction. It may seem odd that the line printed is exactly the same one as before. What has happened though is that we’ve “stepped” into the subshell needed to run `fact 0`; we haven’t however started running anything inside that subshell yet though.

To indicate that which piece of the multi-part line echo fact 0 is: `fact 0` we show that part all by itself fact 0. If nothing is shown then it means we are running the beginning statement or in this case the outermost statement.

To indicate that we are now nested in a subshell, notice that the command number, starting with 3, or the third command entered, now appears in parenthesis. Each subshell nesting adds a set of parenthesis.

The first step command steps the script one instruction; it didn’t advance the line number, 9, at all. That is because we were stopping before the command substitution or backtick is to take place. The second command we entered was just hitting the return key; bashdb remembers that you entered step previously, so it runs the step rather than next, the other alternative when you hit RET. Step one more instruction and we are just before running the first statement of the function.

Next, we print the value of the variable n. Notice we need to add a preceding dollar simple to get the substitution or value of n. As we will see later, if the pe command were used this would not be necessary.

We now modify the file to add an assignment to local variable n and restart.

bashdb<6> restart
Restarting with: /usr/local/bin/bashdb -L . fact.sh
(/tmp/fact.sh:10):
10:	echo fact 0 is: `fact 0`
bashdb<0> list 1
  1:    #!/usr/local/bin/bash
  2:    fact() {
  3:    local -i n=${1:0}
  4:    ((n==0)) && echo 1 && return
  5:    ((nm1=n-1))
  6:    ((result=n*`fact $nm1`))
  7:    echo $result
  8:    }
  9:
 10:==> echo fact 0 is: `fact 0`
bashdb<1> s 3
(/tmp/fact.sh:3):
3:	local -i n=${1:0}
bashdb<(2)> step
(/tmp/fact.sh:4):
4:	((n==0)) && echo 1 && return
bashdb<(3)> print $n
print $n
0

This time we use the list debugger command to list the lines in the file. From before we know it takes three step commands before we get into the fact() function, so we add a count onto the step command. Notice we abbreviate step with s; we could have done likewise and abbreviated list with l.

bashdb<(4)> RET
(/tmp/fact.sh:4):
4:	((n==0)) && echo 1 && return
echo 1
bashdb<(5)> RET
(/tmp/fact.sh:4):
4:	((n==0)) && echo 1 && return
return

Again we just use RET to repeat the last step commands. And again the fact that we are staying on the same line 4 means that the next condition in the line is about to be executed. Notice that we see the command (echo 1 or return) listed when we stay on the same line which has multiple stopping points in it. Given the information above, we know that the value echo’ed on return will be 1.

bashdb<(6)> RET
fact 0 is: 1
(/tmp/fact.sh:12):
12:	echo fact 3 is: $(fact 3)
bashdb<(7)> break 5
Breakpoint 1 set in file fact.sh, line 5.
bashdb<(8)> continue

We saw that we could step with a count into the function fact(). However above took another approach: we set a stopping point or “breakpoint” at line 5 to get us a little ways into the fact() subroutine. Just before line 5 is to executed, we will get back into the debugger. The continue command just resumes execution until the next stopping point which has been set up in some way.

(/tmp/fact.sh:5):
5:      ((nm1=n-1))
Breakpoint 1 hit(1 times).
bashdb<(8)> x n-1
2
bashdb<(9)> s
(/tmp/fact.sh:5):
6:     ((result=n*`fact $nm1`))
bashdb<(10)> c
fact.sh: line 6: ((: result=n*: syntax error: operand expected (error token is "*")
bashdb<(7)> R
Restarting with: bash --debugger fact.sh
11:	echo fact 0 is: `fact 0`
bashdb<0> l fact
 2:    fact ()
 3:    {
 4:       local -i n=${1:0};
 5:       (( "n==0" )) && echo 1 && return;
 6:       (( nm1=n-1 ));
 7:       ((fact_nm1=`fact $nm1`))
 8:       (( "result=n*fact_nm1" ));
 9:       echo $result
10:    }

In addition to listing by line numbers, we can also list giving a function name. Below, instead of setting a breakpoint at line 5 and running “continue” as we did above, we try something slightly shorter and slightly different. We give the line number on the “continue” statement. This is a little different in that a one-time break is made on line 5. Once that statement is reached the breakpoint is removed.

bashdb<1> continue 5
One-time breakpoint 1 set in file fact.sh, line 5.
fact 0 is: 1
(/tmp/fact.sh:5):
5:	((nm1=n-1))
bashdb<(2)> s
6:	((fact_nm1=`fact $nm1`))
bashdb<(3)> s
2:	fact() {
bashdb<(4)> T
->0 in file `fact.sh' at line 2
##1 fact("3") called from file `fact.sh' at line 12
##2 source("fact.sh") called from file `/usr/local/bin/bashdb' at line 154
##3 main("fact.sh") called from file `/usr/local/bin/bashdb' at line 0
bashdb<(5)> c
fact 3 is: 6
Debugged program terminated normally. Use q to quit or R to restart.

When we stop at line 5 above, we have already run fact(0) and output the correct results. The output from the program “fact 0 is: 1” is intermixed with the debugger output. The T command above requests call stack output and this confirms that we are not in the fact(0) call but in the fact(3) call. There are 4 lines listed in the stack trace even though there is just one call from the main program. The top line of the trace doesn’t really represent a call, it’s just where we currently are in the program. That last line is an artifact of invoking bash from the bashdb script rather than running bash --debugger.

The last message in the output above ‘Debugged program exited normally.’ is from the BASH debugger; it indicates script has finished executing. We can end our bashdb session with the quit command.

Above we did our debugging session on the command line. If you are a GNU Emacs user, you can do your debugging inside that. Also there is a(nother) GUI interface called DDD that supports the BASH debugger.


Footnotes

(1)

Recall that variables in BASH don’t need to be declared before they are referred to and that the default value would be the a null value which here prints as an empty string.