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.
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.