How to Debug the Shell Scripts in Linux

Inevitably, administrators who write, use, or maintain shell scripts will encounter bugs with a script. Bugs are typically due to typographical errors, syntactical errors, or poor script logic.

A good way to deal with shell scripting bugs is to make a concerted effort to prevent them from occurring in the first place during the authoring of the script. As previously mentioned, using a text editor with Bash syntactical highlighting can help make mistakes more obvious when writing scripts. Another easy way to avoid introducing bugs into scripts is by adhering to good practices during the creation of the script.

Best Practices to Write a Shell Script

Use comments to help clarify to readers the purpose and logic of the script. The top of every script should include comments providing an overview of the script’s purpose, intended actions, and general logic. Also use comments throughout the script to clarify the key portions, and especially sections that may cause confusion. Comments will not only aid other users in the reading and debugging of the script, but will also often help the author recall the workings of the script once some time has passed.

Structure the contents of the script to improve readability. As long as the syntax is correct, the command interpreter will flawlessly execute the commands within a script with absolutely no regard for their structure or formatting. Here are some good practices to follow:

  • Break up long commands into multiple lines of smaller code chunks. Shorter pieces of code are much easier for readers to digest and comprehend.
  • Line up the beginning and ending of multiline statements to make it easier to see that control structures begin and end, and whether they are being closed properly.
  • Indent lines with multiline statements to represent the hierarchy of code logic and the flow of control structures.
  • Use line spacing to separate command blocks to clarify when one code section ends and another begins.
  • Use consistent formatting through the entirety of a script.

When utilized, these simple practices can make it significantly easier to spot mistakes during authoring, as well as improve the readability of the script for future readers. The following example demonstrates how the incorporation of comments and spacing can greatly improve script readability.

#!/bin/bash
for PACKAGE in $(rpm -qa | grep kernel); do echo "$PACKAGE was installed on $(date -d @$(rpm -q --qf "%{INSTALLTIME}\n" $PACKAGE))"; done
#!/bin/bash
#
# This script provides information regarding when kernel-related packages
# are installed on a system by querying information from the RPM database.
#

# Variables
PACKAGETYPE=kernel
PACKAGES=$(rpm -qa | grep $PACKAGETYPE)

# Loop through packages
for PACKAGE in $PACKAGES; do
    # Determine package install date and time
    INSTALLEPOCH=$(rpm -q --qf "%{INSTALLTIME}\n" $PACKAGE)

    # RPM reports time in epoch, so need to convert
    # it to date and time format with date command
    INSTALLDATETIME=$(date -d @$INSTALLEPOCH)

    # Print message
    echo "$PACKAGE was installed on $INSTALLDATETIME"
done

Do not make assumptions regarding the outcome of actions taken by a script. This is especially true of inputs to the script, such as command-line arguments, input from users, command substitutions, variable expansions, and file name expansions. Rather than making assumptions about the integrity of these inputs, make the worthwhile effort to employ the use of proper quoting and sanity checking.

The same caution should be utilized when acting upon entities external to the script. This includes interacting with files and calling external commands. Make use of Bash’s vast number of files and directory tests when interacting with files and directories. Perform error checking on the exit status of commands rather than counting on their success and blindly continuing along with the script when an unexpected error occurs.

The extra steps taken to rule out assumptions will increase the script’s robustness, and keep it from being easily derailed and then inflicting unintended and unnecessary damage to a system. A couple of seemingly harmless lines of code, such as the ones that follow, make very risky assumptions about command execution outcome and filename expansion. If the directory change fails, either due to directory permissions or the directory being nonexistent, the subsequent file removal will be performed on a list of unknown files in an unintended directory.

$ cd $TMPDIR
$ rm *

Lastly, while well-intentioned administrators may employ good practices when authoring their scripts, not all will always agree on what constitutes good practices. Administrators should do themselves and others a favor and always apply their practices consistently through the entirety of their scripts. They should also be considerate and understanding of individual differences when it comes to programming styles and formatting in scripts authored by others. When modifying others’ scripts, administrators should follow the existing structure, formatting, and practices used by the original author, rather than imposing their own style on a portion of the script and destroying the script’s consistency and thereby ruining its readability and future maintainability.

Debug and verbose modes

If despite best efforts, bugs are introduced into a script, administrators will find Bash’s debug mode extremely useful. To activate the debug mode on a script, add the -x option to the command interpreter in the first line of the script.

#!/bin/bash -x

Another way to run a script in debug mode is to execute the script as an argument to Bash with the -x option.

$ bash -x [SCRIPTNAME]

Bash’s debug mode will print out commands executed by the script prior to their execution. The results of all shell expansion performed will be displayed in the printout. The following example shows the extra output that is displayed when debug mode is activated.

$ cat filesize
#!/bin/bash

DIR=/home/geek/tmp

for FILE in $DIR/*; do
    echo "File $FILE is $(stat --printf='%s' $FILE) bytes."
done
$ ./filesize
File /home/geek/tmp/filea is 133 bytes.
File /home/geek/tmp/fileb is 266 bytes.
File /home/geek/tmp/filec is 399 bytes.
bash -x ./filesize
+ DIR=/home/geek/tmp
+ for FILE in '$DIR/*'
++ stat --printf=%s /home/geek/tmp/filea
+ echo 'File /home/geek/tmp/filea is 133 bytes.'
File /home/geek/tmp/filea is 133 bytes.
+ for FILE in '$DIR/*'
++ stat --printf=%s /home/geek/tmp/fileb
+ echo 'File /home/geek/tmp/fileb is 266 bytes.'
File /home/geek/tmp/fileb is 266 bytes.
+ for FILE in '$DIR/*'
++ stat --printf=%s /home/geek/tmp/filec
+ echo 'File /home/geek/tmp/filec is 399 bytes.'
File /home/geek/tmp/filec is 399 bytes.

While Bash’s debug mode provides helpful information, the voluminous output may actually become more hindrance than a help for troubleshooting, especially as the lengths of scripts increase. Fortunately, the debug mode can be enabled partially on just a portion of a script, rather than on its entirety. This feature is especially useful when debugging a long script and the source of the problem has been narrowed to a portion of the script.

Debugging can be turned on at a specific point in a script by inserting the command set -x and turned off by inserting the command set +x. The following demonstration shows the previous example script with debugging enabled just for the command line enclosed in the for loop.

$ cat filesize
#!/bin/bash

DIR=/home/geek/tmp

for FILE in $DIR/*; do
    set -x
    echo "File $FILE is $(stat --printf='%s' $FILE) bytes."
    set +x
done
$ ./filesize
++ stat --printf=%s /home/geek/tmp/filea
+ echo 'File /home/geek/tmp/filea is 133 bytes.'
File /home/geek/tmp/filea is 133 bytes.
+ set +x
++ stat --printf=%s /home/geek/tmp/fileb
+ echo 'File /home/geek/tmp/fileb is 266 bytes.'
File /home/geek/tmp/fileb is 266 bytes.
+ set +x
++ stat --printf=%s /home/geek/tmp/filec
+ echo 'File /home/geek/tmp/filec is 399 bytes.'
File /home/geek/tmp/filec is 399 bytes.
+ set +x

In addition to debug mode, Bash also offers a verbose mode, which can be invoked with the -v option. In verbose mode, Bash will print each command to standard out prior to its execution.

$ cat filesize
#!/bin/bash

DIR=/home/geek/tmp

for FILE in $DIR/*; do
    echo "File $FILE is $(stat --printf='%s' $FILE) bytes."
done
$ bash -v ./filesize stat --printf='%s' $FILE) bytes."
stat --printf='%s' $FILE) bytes.
stat --printf='%s' $FILE
File /home/geek/tmp/filea is 133 bytes. stat --printf='%s' $FILE) bytes."
stat --printf='%s' $FILE) bytes.
stat --printf='%s' $FILE
File /home/geek/tmp/fileb is 266 bytes. stat --printf='%s' $FILE) bytes."
stat --printf='%s' $FILE) bytes.
stat --printf='%s' $FILE
File /home/geek/tmp/filec is 399 bytes.

Like the debug feature, the verbose feature can also be turned on and off at specific points in a script by inserting the set -v and set +v lines, respectively.