3.28. Debugging

The Bash shell contains no debugger, nor even any debugging-specific commands or constructs. Syntax errors or outright typos in the script generate cryptic error messages that are often of no help in debugging a non-functional script.


Example 3-160. test23, a buggy script

   1 #!/bin/bash
   2 
   3 a=37
   4 
   5 if [$a -gt 27 ]
   6 then
   7   echo $a
   8 fi  
   9 
  10 exit 0

Output from script:

 ./test23: [37: command not found

What's wrong with the above script (hint: after the if)?

What if the script executes, but does not work as expected? This is the all too familiar logic error.


Example 3-161. test24, another buggy script

   1 #!/bin/bash
   2 
   3 # This is supposed to delete all filenames
   4 # containing embedded spaces in current directory,
   5 # but doesn't.  Why not?
   6 
   7 
   8 badname=`ls | grep ' '`
   9 
  10 # echo "$badname"
  11 
  12 rm "$badname"
  13 
  14 exit 0

Try to find out what's wrong with Example 3-161 by uncommenting the echo "$badname" line. Echo statements are useful for seeing whether what you expect is actually what you get.

In this particular case, rm "$badname" will not give the desired results because $badname should not be quoted. Placing it in quotes ensures that rm has only one argument (it will match only one filename). A partial fix is to remove to quotes from $badname and to reset $IFS to contain only a newline, IFS=$'\n'. However, there are simpler ways of going about it.

   1 # Correct methods of deleting filenames containing spaces.
   2 rm *\ *
   3 rm *" "*
   4 rm *' '*
   5 # Thank you. S.C.

Summarizing the symptoms of a buggy script,

  1. It bombs with an error message syntax error, or

  2. It runs, but does not work as expected (logic error)

  3. It runs, works as expected, but has nasty side effects (logic bomb).

Tools for debugging non-working scripts include

  1. echo statements at critical points in the script to trace the variables, and otherwise give a snapshot of what is going on.

  2. using the tee filter to check processes or data flows at critical points.

  3. setting option flags -n -v -x

    sh -n scriptname checks for syntax errors without actually running the script. This is the equivalent of inserting set -n or set -o noexec into the script. Note that certain types of syntax errors can slip past this check.

    sh -v scriptname echoes each command before executing it. This is the equivalent of inserting set -v or set -o verbose in the script.

    sh -x scriptname echoes the result each command, but in an abbreviated manner. This is the equivalent of inserting set -x or set -o xtrace in the script.

    Inserting set -u or set -o nounset in the script runs it, but gives an unbound variable error message at each attempt to use an undeclared variable.

  4. trapping at exit

    The exit command in a script triggers a signal 0, terminating the process, that is, the script itself. [1] It is often useful to trap the exit, forcing a "printout" of variables, for example. The trap must be the first command in the script.

trap

Specifies an action on receipt of a signal; also useful for debugging.

Note

A signal is simply a message sent to a process, either by the kernel or another process, telling it to take some specified action (usually to terminate). For example, hitting a Control-C, sends a user interrupt, an INT signal, to a running program.

   1 trap '' 2
   2 # Ignore interrupt 2 (Control-C), with no action specified. 
   3 
   4 trap 'echo "Control-C disabled."' 2
   5 # Message when Control-C pressed.


Example 3-162. Trapping at exit

   1 #!/bin/bash
   2 
   3 trap 'echo Variable Listing --- a = $a  b = $b' EXIT
   4 # EXIT is the name of the signal generated upon exit from a script.
   5 
   6 a=39
   7 
   8 b=36
   9 
  10 exit 0
  11 # Note that commenting out the 'exit' command makes no difference,
  12 # since the script exits anyhow after running out of commands.


Example 3-163. Cleaning up after Control-C

   1 #!/bin/bash
   2 
   3 # logon.sh
   4 # A quick 'n dirty script to check whether you are on-line yet.
   5 
   6 
   7 TRUE=1
   8 LOGFILE=/var/log/messages
   9 # Note that $LOGFILE must be readable (chmod 644 /var/log/messages).
  10 TEMPFILE=temp.$$
  11 # Create a "unique" temp file name, using process id of the script.
  12 KEYWORD=address
  13 # At logon, the line "remote IP address xxx.xxx.xxx.xxx" appended to /var/log/messages.
  14 ONLINE=22
  15 USER_INTERRUPT=13
  16 
  17 trap 'rm -f $TEMPFILE; exit $USER_INTERRUPT' TERM INT
  18 # Cleans up the temp file if script interrupted by control-c.
  19 
  20 echo
  21 
  22 while [ $TRUE ]  #Endless loop.
  23 do
  24   tail -1 $LOGFILE> $TEMPFILE
  25   # Saves last line of system log file as temp file.
  26   search=`grep $KEYWORD $TEMPFILE`
  27   # Checks for presence of the "IP address" phrase,
  28   # indicating a successful logon.
  29 
  30   if [ ! -z "$search" ] # Quotes necessary because of possible spaces.
  31   then
  32      echo "On-line"
  33      rm -f $TEMPFILE  # Clean up temp file.
  34      exit $ONLINE
  35   else
  36      echo -n "." # -n option to echo suppresses newline,
  37                  # so you get continuous rows of dots.
  38   fi
  39 
  40   sleep 1  
  41 done  
  42 
  43 
  44 # Note: if you change the KEYWORD variable to "Exit",
  45 # this script can be used while on-line to check for an unexpected logoff.
  46 
  47 # Exercise: Change the script, as per the above note,
  48 #           and prettify it.
  49 
  50 exit 0
  51 
  52 
  53 # Nick Drage suggests an alternate method:
  54 
  55 while true
  56   do ifconfig ppp0 | grep UP 1> /dev/null && echo "connected" && exit 0
  57   echo -n "."   # Prints dots (.....) until connected.
  58   sleep 2
  59 done
  60 
  61 # Problem: Hitting Control-C to terminate this process may be insufficient.
  62 #          (Dots may keep on echoing.)
  63 # Exercise: Fix this.
  64 
  65 
  66 
  67 # Stephane Chazelas has yet another alternative:
  68 
  69 CHECK_INTERVAL=1
  70 
  71 while ! tail -1 "$LOGFILE" | grep -q "$KEYWORD"
  72 do echo -n .
  73    sleep $CHECK_INTERVAL
  74 done
  75 echo "On-line"

Note

The DEBUG argument to trap causes a specified action to execute after every command in a script. This permits tracing variables, for example.


Example 3-164. Tracing a variable

   1 #!/bin/bash
   2 
   3 trap 'echo "VARIABLE-TRACE> \$variable = \"$variable\""' DEBUG
   4 # Echoes the value of $variable after every command.
   5 
   6 
   7 variable=29
   8 
   9 echo "Just initialized \"\$variable\" to $variable."
  10 
  11 let "variable *= 3"
  12 echo "Just multiplied \"\$variable\" by 3."
  13 
  14 # The "trap 'commands' DEBUG" construct would be more useful
  15 # in the context of a complex script,
  16 # where placing multiple "echo $variable" statements might be
  17 # clumsy and time-consuming.
  18 
  19 # Thanks, Stephane Chazelas for the pointer.
  20 
  21 exit 0

Note

trap '' SIGNAL (two adjacent apostrophes) disables SIGNAL for the remainder of the script. trap SIGNAL restores the functioning of SIGNAL once more. This is useful to protect a critical portion of a script from an undesirable interrupt.

   1 	trap '' 2  # Signal 2 is Control-C, now disabled.
   2 	command
   3 	command
   4 	command
   5 	trap 2     # Reenables Control-C
   6 	

Notes

[1]

By convention, signal 0 is assigned to exit.