Sams Teach Yourself Shell Programming in 24 Hours
(Publisher: Macmillan Computer Publishing)
Author(s): Sriranga Veeraraghavan
ISBN: 0672314819
Publication Date: 01/01/99

Previous Table of Contents Next


Finding Logical Bugs Using Shell Tracing

As mentioned before, there is at least one logical bug in this script. With the help of shell tracing, you can locate and fix this bug.

Consider the prompt produced by this script:

Make backup (y/n)?

If you do not type a response and press Enter, the script reports an error similar to the following:

./buggy3.sh: [: =: unary operator expected

To determine where this error occurs, it is best to run the entire script in shell tracing mode:

$ /bin/sh -x ./buggy3.sh

The output is similar to the following:

+ YesNo Make backup
+ echo Make backup (y/n)? \c
+ /bin/echo Make backup (y/n)? \c
Make backup (y/n)? + read RESPONSE

+ [ = y ]
./buggy3.sh: [: =: unary operator expected

Here the blank line is the result of pressing Enter instead of typing a response to the prompt, as you can see from the next line that the shell executes:

[ = y ]

This is part of the if statement:

if [ $RESPONSE = "y" ] ; then

Although this can be fixed by changing the if statement

if [ "$RESPONSE" = "y" ] ; then

the correct fix for this problem is to track down the reason why the variable RESPONSE is not set. This variable is set by the function YesNo:

YesNo() {
    echo "$1 (y/n)? \c"
    read RESPONSE
    case $RESPONSE in
        [yY]|[Yy][Ee][Ss]) RESPONSE=y ;;
        [nN]|[Nn][Oo]) RESPONSE=n ;;
    esac
}

There are two problems with this script. The first is that the read command

read RESPONSE

does not set a value for RESPONSE if the user presses Enter without typing some input. Because you cannot change the read command, you need to look for some other method of solving this problem.

This leads you to a logical problem—the case statement is not validating the user input. A simple fix is the following:

YesNo() {
    echo "$1 (y/n)? \c"
    read RESPONSE
    case "$RESPONSE" in
        [yY]|[Yy][Ee][Ss]) RESPONSE=y ;;
        *) RESPONSE=n ;;
    esac
}

Here you treat all responses other than “yes” responses as negative responses, including no response at all.

Using Debugging Hooks

In the previous examples, you were able to deduce the location of a bug by using shell tracing for either the entire script or for part of the script. In the case of enabling tracing for a part of a script, you had to edit the script to insert the debug command:

set -x


In larger scripts, a more common practice is to embed debugging hooks. Debugging hooks are functions that enable shell tracing during functions or critical code sections. They are activated in one of two ways:
  The script is run with a particular command line option (commonly -d or -x).
  The script is run with an environment variable set to true (commonly DEBUG=true or TRACE=true).

Here is a function that enables you to activate and deactivate debugging at will if the variable DEBUG is set to true:

Debug() {
    if [ "$DEBUG" = "true" ] ; then
        if [ "$1" = "on"  -o "$1" = "ON" ] ; then
            set -x
        else
            set +x
        fi
    fi
}

To activate debugging, use the following:

Debug on

To deactivate debugging, use either of the following:

Debug
Debug off

Actually, any argument passed to this function other than on or ON deactivates debugging.

As an example of using this function, modify the functions in the script buggy3.sh to have debugging automatically enabled if the variable DEBUG is set. The modifications are as follows:

#!/bin/sh

Debug() {
    if [ "$DEBUG" = "true" ] ; then
        if [ "$1" = "on"  -o "$1" = "ON" ] ; then
            set -x
        else
            set +x
        fi
    fi
}

Failed() {
    Debug on
    if [ "$1" -ne 0 ] ; then
        echo "Failed. Exiting." ; exit 1 ;
    fi
    echo "Done."
    Debug off
}

YesNo() {
    Debug on
    echo "$1 (y/n)? \c"
    read RESPONSE
    case "$RESPONSE" in
        [yY]|[Yy][Ee][Ss]) RESPONSE=y ;;
        *) RESPONSE=n ;;
    esac
    Debug off
}

YesNo "Make backup"
if [ "$RESPONSE" = "y" ] ; then

    echo "Deleting old backups, please wait… \c"
    rm -r backup > /dev/null 2>&1
    Failed $?

    echo "Making new backups, please wait… \c"
    cp -r docs backup
    Failed $?
fi

The output will be normal if the script executes in either of the following methods:

$ /bin/sh ./buggy3.sh
$ ./buggy3.sh

The output includes shell tracing if the same script executes in either of the following methods:

$ DEBUG=true /bin/sh ./buggy3.sh
$ DEBUG=true ./buggy3.sh

Summary

In the process of developing or maintaining large shell scripts, you need to find and fix bugs that occur in them. In this chapter you looked at the tools provided by the shell to ease the task of debugging shell scripts. Some of the topics you covered are

  Enabling debugging
  Syntax checking using sh -n and sh -nv
  Using shell tracing to find syntax and logic bugs
  Embedding debugging hooks in your shell scripts

By learning the techniques used in debugging shell scripts, you can fix your own scripts as well as maintain scripts written by other programmers.

Questions

1.  What are the three main forms of enabling debugging in a shell script?
2.  Enhance the Debug() function given in this chapter so that the programmer has to press Enter after deactivating the debugging mode.
When you debug scripts that have several dozen functions, this feature enables you to study the debugging output of a particular function in detail before the script proceeds to the next function.


Previous Table of Contents Next