How to Debug Bash Scripts

There are techniques from traditional programming environments that can help.
Some basic tools like using an editor with syntax highlighting will help as well.
There are builtin options that Bash provides to make debugging and your everyday Linux System Administration job easier.

In this article you will learn some useful methods of debugging Bash scripts:

  • How to use traditonal techniques
  • How to use the xtrace option
  • How to use other Bash options
  • How to use trap

Bash Terminal

The most effective debugging tool is still careful thought, coupled with judiciously placed print statements. – Brian Kernighan, “Unix for Beginners” (1979)

Software Requirements and Conventions Used

Software Requirements and Linux Command Line Conventions
Category Requirements, Conventions or Software Version Used
System Any GNU/Linux Distribution
Software GNU Bash
Other N/A
Conventions # – requires given linux commands to be executed with root privileges either directly as a root user or by use of sudo command
$ – requires given linux commands to be executed as a regular non-privileged user

Using Traditional Techniques

Debugging code can be tricky, even if the mistakes are simple and obvious. Programmers have traditionally taken advantage of tools like debuggers and syntax highlighting in editors to assist them. It’s no different when writing Bash scripts. Simply having syntax highlighting will allow you to catch mistakes as you write the code, saving you the time-consuming task of tracking down mistakes later.

Some programming languages come with companion debugging environments, like gcc and gdb that let you step through code, set breakpoints, examine the state of everything at those points in the execution and more – but there’s generally less need for a heavy handed approach like that with shell scripts since the code is simply interpreted instead of being compiled into binaries.

There are techniques used in traditional programming environments that can be useful with complex Bash scripts, such as using assertions. These are basically a ways of explicitly asserting conditions or the state of things at a point in time. Assertions can pinpoint even the subtlest of bugs. They may be implemented as a short function that shows the timing, line number and such, or something like this:

$ echo "function_name(): value of \\$var is ${var}"

How to use Bash xtrace option

When writing shell scripts, the programming logic tends to be shorter and is often contained within a single file. So there are a few built-in debugging options we can use to see what is going wrong. The first option to mention is probably the most useful too – the xtrace option. This can be applied to a script by invoking Bash with the -x switch.

$ bash -x <scriptname>


This tells Bash to show us what each statement looks like after evaluation, just before it is executed. We’ll see an example of this in action shortly, but first let’s contrast -x with its opposite -v, which shows each line before it is evaluated instead of after. Options can be combined and by using both -x and -v you can see what statements look like before and after variable substitutions take place.

set xv options at command line

Setting x and v options at the command line

Notice how using -x and -v options together allows us to see the original if statement before the $USER variable is expanded, thanks to the -v option. We also see on the line beginning with a plus sign what the statement looks like again after the substitution, which shows us the actual values compared inside the if statement. In more complicated examples this can be quite useful.

How to use other Bash options

The Bash options for debugging are turned off by default, but once they are turned on by using the set command, they stay on until explicitly turned off. If you are not sure which options are enabled, you can examine the $- variable to see the current state of all the variables.

$ echo $-
himBHs
$ set -xv && echo $-
himvxBHs

There is another useful switch we can use to help us find variables referenced without having any value set. This is the -u switch, and just like -x and -v it can also be used on the command line, as we see in the following example:

set u option at command line

Setting u option at the command line

We mistakenly assigned a value of 7 to the variable called “level” then tried to echo a variable named “score” that simply resulted in printing nothing at all to the screen. Absolutely no debug information was given. Setting our -u switch allows us to see a specific error message, “score: unbound variable” that indicates exactly what went wrong.

We can use those options in short Bash scripts to give us debug information to identify problems that do not otherwise trigger feedback from the Bash interpreter. Let’s walk through a couple of examples.

#!/bin/bash

read -p "Path to be added: " $path

if [ "$path" = "/home/mike/bin" ]; then
	echo $path >> $PATH
	echo "new path: $PATH"
else
	echo "did not modify PATH"
fi
results from addpath script

Using x option when running your Bash script

In the example above we run the addpath script normally and it simply does not modify our PATH. It does not give us any indication of why or clues to mistakes made. Running it again using the -x option clearly shows us that the left side of our comparison is an empty string. $path is an empty string because we accidentally put a dollar sign in front of “path” in our read statement. Sometimes we look right at a mistake like this and it doesn’t look wrong until we get a clue and think, “Why is $path evaluated to an empty string?”

Looking this next example, we also get no indication of an error from the interpreter. We only get one value printed per line instead of two. This is not an error that will halt execution of the script, so we’re left to simply wonder without being given any clues. Using the -u switch,we immediately get a notification that our variable j is not bound to a value. So these are real time savers when we make mistakes that do not result in actual errors from the Bash interpreter’s point of view.

#!/bin/bash

for i in 1 2 3
do
	echo $i $j
done
results from count.sh script

Using u option running your script from the command line

Now surely you are thinking that sounds fine, but we seldom need help debugging mistakes made in one-liners at the command line or in short scripts like these. We typically struggle with debugging when we deal with longer and more complicated scripts, and we rarely need to set these options and leave them set while we run multiple scripts. Setting -xv options and then running a more complex script will often add confusion by doubling or tripling the amount of output generated.

Fortunately we can use these options in a more precise way by placing them inside our scripts. Instead of explicitly invoking a Bash shell with an option from the command line, we can set an option by adding it to the shebang line instead.

#!/bin/bash -x

This will set the -x option for the entire file or until it is unset during the script execution, allowing you to simply run the script by typing the filename instead of passing it to Bash as a parameter. A long script or one that has a lot of output will still become unwieldy using this technique however, so let’s look at a more specific way to use options.



For a more targeted approach, surround only the suspicious blocks of code with the options you want. This approach is great for scripts that generate menus or detailed output, and it is accomplished by using the set keyword with plus or minus once again.

#!/bin/bash

read -p "Path to be added: " $path

set -xv
if [ "$path" = "/home/mike/bin" ]; then
	echo $path >> $PATH
	echo "new path: $PATH"
else
	echo "did not modify PATH"
fi
set +xv
results from addpath script

Wrapping options around a block of code in your script

We surrounded only the blocks of code we suspect in order to reduce the output, making our task easier in the process. Notice we turn on our options only for the code block containing our if-then-else statement, then turn off the option(s) at the end of the suspect block. We can turn these options on and off multiple times in a single script if we can’t narrow down the suspicious areas, or if we want to evaluate the state of variables at various points as we progress through the script. There is no need to turn off an option If we want it to continue for the remainder of the script execution.

For completeness sake we should mention also that there are debuggers written by third parties that will allow us to step through the code execution line by line. You might want to investigate these tools, but most people find that that they are not actually needed.

As seasoned programmers will suggest, if your code is too complex to isolate suspicious blocks with these options then the real problem is that the code should be refactored. Overly complex code means bugs can be difficult to detect and maintenance can be time consuming and costly.

One final thing to mention regarding Bash debugging options is that a file globbing option also exists and is set with -f. Setting this option will turn off globbing (expansion of wildcards to generate file names) while it is enabled. This -f option can be a switch used at the command line with bash, after the shebang in a file or, as in this example to surround a block of code.

#!/bin/bash

echo "ignore fileglobbing option turned off"
ls *

echo "ignore file globbing option set"
set -f
ls *
set +f
results from -f option

Using f option to turn off file globbing

How to use trap to help debug

There are more involved techniques worth considering if your scripts are complicated, including using an assert function as mentioned earlier. One such method to keep in mind is the use of trap. Shell scripts allow us to trap signals and do something at that point.

A simple but useful example you can use in your Bash scripts is to trap on EXIT.

#!/bin/bash

trap 'echo score is $score, status is $status' EXIT

if [ -z $1 ]; then
	status="default"
else
	status=$1
fi

score=0
if [ ${USER} = 'superman' ]; then
	score=99
elif [ $# -gt 1 ]; then
	score=$2
fi
results from using trap EXIT

Using trap EXIT to help debug your script


As you can see just dumping the current values of variables to the screen can be useful to show where your logic is failing. The EXIT signal obviously does not need an explicit exit statement to be generated; in this case the echo statement is executed when the end of the script is reached.

Another useful trap to use with Bash scripts is DEBUG. This happens after every statement, so it can be used as a brute force way to show the values of variables at each step in the script execution.

#!/bin/bash

trap 'echo "line ${LINENO}: score is $score"' DEBUG

score=0

if [ "${USER}" = "mike" ]; then
	let "score += 1"
fi

let "score += 1"

if [ "$1" = "7" ]; then
	score=7
fi
exit 0
results from using trap DEBUG

Using trap DEBUG to help debug your script

Conclusion

When you notice your Bash script not behaving as expected and the reason is not clear to you for whatever reason, consider what information would be useful to help you identify the cause then use the most comfortable tools available to help you pinpoint the issue. The xtrace option -x is easy to use and probably the most useful of the options presented here, so consider trying it out next time you’re faced with a script that’s not doing what you thought it would.



Comments and Discussions
Linux Forum