WCC logo

CIS120Linux Fundementals

Troubleshooting Bash Scripts

As our scripts become more complex, it's crucial to address what happens when things go wrong. This chapter examines common errors in scripts and techniques to track down and eradicate these issues.

Syntactic Errors

One general class of errors is syntactic. These involve mistyping elements of shell syntax, causing the shell to stop executing the script. Here's an example script to demonstrate common types of errors:

#!/bin/bash
# trouble: script to demonstrate common errors
number=1
if [ $number = 1 ]; then
    echo "Number is equal to 1."
else
    echo "Number is not equal to 1."
fi

As written, this script runs successfully:

[me@linuxbox ~]$ trouble
Number is equal to 1.

Missing Quotes

Let's edit the script by removing the trailing quote from the argument following the first echo command:

#!/bin/bash
# trouble: script to demonstrate common errors
number=1
if [ $number = 1 ]; then
    echo "Number is equal to 1.
else
    echo "Number is not equal to 1."
fi

Running the script results in:

[me@linuxbox ~]$ trouble
/home/me/bin/trouble: line 10: unexpected EOF while looking for matching `"' /home/me/bin/trouble: line 13: syntax error: unexpected end of file

The line numbers reported by the error messages are not where the missing quote was removed but much later in the program. The shell continues looking for the closing quote until it finds one, immediately after the second echo command, confusing subsequent syntax. To avoid this, use an editor with syntax highlighting, like vim with the command :syntax on.

Missing or Unexpected Tokens

Another common mistake is forgetting to complete a compound command, such as if or while. For example, removing the semicolon after the test in the if command:

#!/bin/bash
# trouble: script to demonstrate common errors
number=1
if [ $number = 1 ] then
    echo "Number is equal to 1."
else
    echo "Number is not equal to 1."
fi

Running the script results in:

[me@linuxbox ~]$ trouble
/home/me/bin/trouble: line 9: syntax error near unexpected token `else'
/home/me/bin/trouble: line 9: `else'

The error occurs later than the actual problem. The if command's test list includes the word then due to the missing semicolon, causing syntax errors.

Unanticipated Expansions

Errors can occur intermittently due to the results of an expansion. For example, changing number to an empty variable:

#!/bin/bash
# trouble: script to demonstrate common errors
number=
if [ $number = 1 ]; then
    echo "Number is equal to 1."
else
    echo "Number is not equal to 1."
fi

Running the script results in:

[me@linuxbox ~]$ trouble
/home/me/bin/trouble: line 7: [: =: unary operator expected
Number is not equal to 1.

The test command fails due to the empty number variable, resulting in [ = 1 ], which is invalid. This can be corrected by adding quotes around the first argument in the test command:

[ "$number" = 1 ]

Then, the expansion results in [ "" = 1 ], which is valid.

Logical Errors

Logical errors do not prevent a script from running but cause it to produce incorrect results. Common types include:

  1. Incorrect conditional expressions: Misconfigured if/then/else logic.
  2. “Off by one” errors: Loop counters starting from incorrect values.
  3. Unanticipated situations: Unexpected data or expansions causing failures.

Defensive Programming

Verify assumptions by carefully evaluating the exit status of programs and commands. For example, consider this script:

cd $dir_name
rm *

If dir_name does not exist, the script will delete files in the current working directory. Improve it by quoting dir_name and checking if cd is successful:

cd "$dir_name" && rm *

Further, check that dir_name contains an existing directory:

[[ -d "$dir_name" ]] && cd "$dir_name" && rm *

Terminate the script if the directory does not exist:

# Delete files in directory $dir_name
if [[ ! -d "$dir_name" ]]; then
    echo "No such directory: '$dir_name'" >&2
    exit 1
fi

if ! cd "$dir_name"; then
    echo "Cannot cd to '$dir_name'" >&2
    exit 1
fi

if ! rm *; then
    echo "File deletion failed. Check results" >&2
    exit 1
fi

Watch Out for Filenames

Unix allows nearly any character in filenames, which can cause problems. For example, a filename starting with - can be interpreted as an option. Prevent this by using ./*:

rm ./*

Portable Filenames

To ensure portability, limit filenames to the POSIX Portable Filename Character Set: uppercase and lowercase letters, numerals, period, hyphen, and underscore.

Verifying Input

A program must handle any input it receives. Use specific tests to verify valid input. For example, to verify a menu selection:

[[ $REPLY =~ ^[0-3]$ ]]

This returns a zero exit status only if REPLY is a numeral between 0 and 3.

Design is a Function of Time

Design effort varies with the time available. Quick scripts for single use may need minimal checks, while production scripts require thorough development and testing.

Testing

Testing is crucial in software development. Use stubs to verify program flow and modify scripts to make tests safe. For example:

if [[ -d $dir_name ]]; then
    if cd $dir_name; then
        echo rm * # TESTING
    else
        echo "cannot cd to '$dir_name'" >&2
        exit 1
    fi
else
    echo "no such directory: '$dir_name'" >&2
    exit 1
fi
exit # TESTING

Test Cases

Develop good test cases to reflect edge and corner cases. For example, test the dir_name script with:

  1. dir_name contains an existing directory.
  2. dir_name contains a nonexistent directory.
  3. dir_name is empty.

Debugging

Debugging involves finding out what a script is actually doing. Isolate problem areas by commenting out sections of code:

if [[ -d $dir_name ]]; then
    if cd $dir_name; then
        rm *
    else
        echo "cannot cd to '$dir_name'" >&2
        exit 1
    fi
# else
# echo "no such directory: '$dir_name'" >&2
# exit 1
fi

Tracing

Tracing shows the program's flow. Add informative messages or use the -x option:

#!/bin/bash -x
# trouble: script to demonstrate common errors
number=1
if [ $number = 1 ]; then
    echo "Number is equal to 1."
else
    echo "Number is not equal to 1."
fi

Enable tracing for selected parts with set -x and set +x:

#!/bin/bash
# trouble: script to demonstrate common errors
number=1
set -x # Turn on tracing
if [ $number = 1 ]; then
    echo "Number is equal to 1."
else
    echo "Number is not equal to 1."
fi
set +x # Turn off tracing

Examining Values During Execution

Display variable contents during execution with echo statements:

#!/bin/bash
# trouble: script to demonstrate common errors
number=1
echo "number=$number" # DEBUG
set -x # Turn on tracing
if [ $number = 1 ]; then
    echo "Number is equal to 1."
else
    echo "Number is not equal to 1."
fi
set +x # Turn off tracing

Summing Up

This chapter covered common script problems and debugging techniques. Debugging is a skill developed through experience, involving constant testing and effective use of tracing to find and fix bugs.