The Bash shell contains no debugger, nor even any debugging-specific commands or constructs. [1] 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 30-1. A buggy script
1 #!/bin/bash 2 # ex74.sh 3 4 # This is a buggy script. 5 6 a=37 7 8 if [$a -gt 27 ] 9 then 10 echo $a 11 fi 12 13 exit 0 |
Output from script:
./ex74.sh: [37: command not found |
Example 30-2. Missing keyword
1 #!/bin/bash 2 # missing-keyword.sh: What error message will this generate? 3 4 for a in 1 2 3 5 do 6 echo "$a" 7 # done # Required keyword 'done' commented out in line 7. 8 9 exit 0 |
Output from script:
missing-keyword.sh: line 10: syntax error: unexpected end of file |
Error messages may disregard comment lines in a script when reporting the line number of a syntax error.
What if the script executes, but does not work as expected? This is the all too familiar logic error.
Example 30-3. test24, another buggy script
1 #!/bin/bash 2 3 # This is supposed to delete all filenames in current directory 4 #+ containing embedded spaces. 5 # It doesn't work. 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 30-3 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,
It bombs with a "syntax error" message, or
It runs, but does not work as expected (logic error).
It runs, works as expected, but has nasty side effects (logic bomb).
Tools for debugging non-working scripts include
echo statements at critical points in the script to trace the variables, and otherwise give a snapshot of what is going on.
using the tee filter to check processes or data flows at critical points.
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.
The -n and -v flags work well together. sh -nv scriptname gives a verbose syntax check.
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.
Using an "assert" function to test a variable or condition at critical points in a script. (This is an idea borrowed from C.)
Example 30-4. Testing a condition with an "assert"
1 #!/bin/bash 2 # assert.sh 3 4 assert () # If condition false, 5 { #+ exit from script with error message. 6 E_PARAM_ERR=98 7 E_ASSERT_FAILED=99 8 9 10 if [ -z "$2" ] # Not enough parameters passed. 11 then 12 return $E_PARAM_ERR # No damage done. 13 fi 14 15 lineno=$2 16 17 if [ ! $1 ] 18 then 19 echo "Assertion failed: \"$1\"" 20 echo "File \"$0\", line $lineno" 21 exit $E_ASSERT_FAILED 22 # else 23 # return 24 # and continue executing script. 25 fi 26 } 27 28 29 a=5 30 b=4 31 condition="$a -lt $b" # Error message and exit from script. 32 # Try setting "condition" to something else, 33 #+ and see what happens. 34 35 assert "$condition" $LINENO 36 # The remainder of the script executes only if the "assert" does not fail. 37 38 39 # Some commands. 40 # ... 41 echo "This statement echoes only if the \"assert\" does not fail." 42 # ... 43 # Some more commands. 44 45 exit 0 |
trapping at exit.
The exit command in a script triggers a signal 0, terminating the process, that is, the script itself. [2] 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.
Specifies an action on receipt of a signal; also useful for debugging.
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 30-5. 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 in any case after running out of commands. |
Example 30-6. Cleaning up after Control-C
1 #!/bin/bash 2 # logon.sh: A quick 'n dirty script to check whether you are on-line yet. 3 4 5 TRUE=1 6 LOGFILE=/var/log/messages 7 # Note that $LOGFILE must be readable (chmod 644 /var/log/messages). 8 TEMPFILE=temp.$$ 9 # Create a "unique" temp file name, using process id of the script. 10 KEYWORD=address 11 # At logon, the line "remote IP address xxx.xxx.xxx.xxx" 12 # appended to /var/log/messages. 13 ONLINE=22 14 USER_INTERRUPT=13 15 CHECK_LINES=100 16 # How many lines in log file to check. 17 18 trap 'rm -f $TEMPFILE; exit $USER_INTERRUPT' TERM INT 19 # Cleans up the temp file if script interrupted by control-c. 20 21 echo 22 23 while [ $TRUE ] #Endless loop. 24 do 25 tail -$CHECK_LINES $LOGFILE> $TEMPFILE 26 # Saves last 100 lines of system log file as temp file. 27 # Necessary, since newer kernels generate many log messages at log on. 28 search=`grep $KEYWORD $TEMPFILE` 29 # Checks for presence of the "IP address" phrase, 30 # indicating a successful logon. 31 32 if [ ! -z "$search" ] # Quotes necessary because of possible spaces. 33 then 34 echo "On-line" 35 rm -f $TEMPFILE # Clean up temp file. 36 exit $ONLINE 37 else 38 echo -n "." # -n option to echo suppresses newline, 39 # so you get continuous rows of dots. 40 fi 41 42 sleep 1 43 done 44 45 46 # Note: if you change the KEYWORD variable to "Exit", 47 # this script can be used while on-line to check for an unexpected logoff. 48 49 # Exercise: Change the script, as per the above note, 50 # and prettify it. 51 52 exit 0 53 54 55 # Nick Drage suggests an alternate method: 56 57 while true 58 do ifconfig ppp0 | grep UP 1> /dev/null && echo "connected" && exit 0 59 echo -n "." # Prints dots (.....) until connected. 60 sleep 2 61 done 62 63 # Problem: Hitting Control-C to terminate this process may be insufficient. 64 # (Dots may keep on echoing.) 65 # Exercise: Fix this. 66 67 68 69 # Stephane Chazelas has yet another alternative: 70 71 CHECK_INTERVAL=1 72 73 while ! tail -1 "$LOGFILE" | grep -q "$KEYWORD" 74 do echo -n . 75 sleep $CHECK_INTERVAL 76 done 77 echo "On-line" 78 79 # Exercise: Discuss the strengths and weaknesses 80 # of each of these various approaches. |
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 |
[1] | Rocky Bernstein's Bash debugger partially makes up for this lack. |
[2] | By convention, signal 0 is assigned to exit. |