32 调试
调试代码要比写代码困难两倍。因此,你写代码时越多的使用奇技淫巧(自做聪明),顾名思义,你越难以调试它。 —Brian Kernighan
Bash shell中不包含内置的debug工具,甚至没有调试专用的命令和结构。当调试非功能脚本,产生语法错误或者有错别字时,往往是无用的错误提示消息。
例子 32-1. 一个错误脚本
1. #!/bin/bash
2. # ex74.sh
4. # 这是一个错误脚本,但是它错在哪?
6. a=37
8. if [$a -gt 27 ]
9. then
10. echo $a
11. fi
13. exit $? # 0! 为什么?
脚本的输出:
1. ./ex74.sh: [37: command not found
上边的脚本究竟哪错了(提示: 注意if的后边)
例子 32-2. 缺少关键字
1. #!/bin/bash
2. # missing-keyword.sh
3. # 这个脚本会提示什么错误信息?
5. for a in 1 2 3
6. do
7. echo "$a"
8. # done #所需关键字'done'在第8行被注释掉.
10. exit 0 # 将不会在这退出!
12. #在命令行执行完此脚本后
13. 输入:echo $?
14. 输出:2
脚本的输出:
1. missing-keyword.sh: line 10: syntax error: unexpected end of file
注意, 其实不必参考错误信息中指出的错误行号. 这行只不过是Bash解释器最终认定错误的地方. 出错信息在报告产生语法错误的行号时, 可能会忽略脚本的注释行. 如果脚本可以执行, 但并不如你所期望的那样工作, 怎么办? 通常情况下, 这都是由常见的逻辑错误所 产生的.
例子 32-3.
1. #!/bin/bash
3. # 这个脚本应该删除在当前目录下所有文件名中含有空格的文件
4. # 它不能正常运行,为什么?
6. badname=`ls | grep ' '`
8. # Try this:
9. # echo "$badname"
11. rm "$badname"
13. exit 0
可以通过把echo “$badname”行的注释符去掉,找出例子 29-3中的错误, 看一下echo出来的信息,是否按你期望的方式运行.
在这种特殊的情况下,rm “$badname”不能得到预期的结果,因为$badname不应该加双引号。加上双引号会让rm只有一个参数(这就只能匹配一个文件名).一种不完善的解决办法是去掉$badname外 面的引号, 并且重新设置$IFS, 让$IFS只包含一个换行符, IFS=$’\n’. 但是, 下面这个方法更简单.
1. # 删除包含空格的文件的正确方法.
2. rm *\ *
3. rm *" "*
4. rm *' '*
5. # 感谢. S.C.
总结一下这个问题脚本的症状:
- 由于”syntax error”(语法错误)使得脚本停止运行,
- 或者脚本能够运行, 但是并不是按照我们所期望的那样运行(逻辑错误).
- 脚本能够按照我们所期望的那样运行, 但是有烦人的副作用(逻辑炸弹).
如果想调试脚本, 可以用以下方式:
- echo语句可以放在脚本中存在疑问的位置上, 观察变量的值, 来了解脚本运行时的情况.
``` 1. ### debecho (debug-echo), by Stefano Falsetto ### 2. ### Will echo passed parameters only if DEBUG is set to a value. ### 3. debecho () { 4. if [ ! -z "$DEBUG" ]; then 5. echo "$1" >&2 6. # ^^^ to stderr 7. fi 8. }
- DEBUG=on
- Whatever=whatnot
-
debecho $Whatever # whatnot
-
DEBUG=
- Whatever=notwhat
-
debecho $Whatever # (Will not echo.) ```
-
使用过滤器tee来检查临界点上的进程或数据流.
- 设置选项-n -v -x
sh -n scriptname不会运行脚本, 只会检查脚本的语法错误. 这等价于把set -n或set -o noexec插入脚本中. 注意, 某些类型的语法错误不会被这种方式检查出来.
sh -v scriptname将会在运行脚本之前, 打印出每一个命令. 这等价于把set -v或set -o verbose插入到脚本中.
选项-n和-v可以同时使用. sh -nv scriptname将会给出详细的语法检查.
sh -x scriptname会打印出每个命令执行的结果, 但只使用缩写形式. 这等价于在脚本中插入set -x或set -o xtrace.
把set -u或set -o nounset插入到脚本中, 并运行它, 就会在每个试图使用未声明变量的地方给出一个unbound variable错误信息.
``` 1. set -u # Or set -o nounset
-
Setting a variable to null will not trigger the error/abort.
-
unset_var=
-
echo $unset_var # Unset (and undeclared) variable.
-
echo "Should not echo!"
-
sh t2.sh
-
t2.sh: line 6: unset_var: unbound variable
``` - 使用“断言”功能在脚本的关键点进行测试的变量或条件。 (这是从C借来的一个想法)
Example 32-4. Testing a condition with an assert
```
# !/bin/bash
# assert.sh
###### #
assert () # If condition false, { #+ exit from script
1. #+ with appropriate error message.
E_PARAM_ERR=98 E_ASSERT_FAILED=99
1. if [ -z "$2" ] # Not enough parameters passed
2. then #+ to assert() function.
3. return $E_PARAM_ERR # No damage done.
4. fi
6. lineno=$2
8. if [ ! $1 ]
9. then
10. echo "Assertion failed: \"$1\""
11. echo "File \"$0\", line $lineno" # Give name of file and line number.
12. exit $E_ASSERT_FAILED
13. # else
14. # return
15. # and continue executing the script.
16. fi
17. } # Insert a similar assert() function into a script you need to debug.
18. #######################################################################
21. a=5
22. b=4
23. condition="$a -lt $b" # Error message and exit from script.
24. # Try setting "condition" to something else
25. #+ and see what happens.
27. assert "$condition" $LINENO
28. # The remainder of the script executes only if the "assert" does not fail.
31. # Some commands.
32. # Some more commands . . .
33. echo "This statement echoes only if the \"assert\" does not fail."
34. # . . .
35. # More commands . . .
37. exit $?
38. ```
- 使用变量$LINENO和内建命令caller.
- 捕获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.
A signal is 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.
A simple instance:
1. trap '' 2
2. # Ignore interrupt 2 (Control-C), with no action specified.
4. trap 'echo "Control-C disabled."' 2
5. # Message when Control-C pressed.
Example 32-5. Trapping at exit
1. #!/bin/bash
2. # Hunting variables with a trap.
4. trap 'echo Variable Listing --- a = $a b = $b' EXIT
5. # EXIT is the name of the signal generated upon exit from a script.
6. #
7. # The command specified by the "trap" doesn't execute until
8. #+ the appropriate signal is sent.
10. echo "This prints before the \"trap\" --"
11. echo "even though the script sees the \"trap\" first."
12. echo
14. a=39
15. b=36
17. exit 0
20. # Note that commenting out the 'exit' command makes no difference,
21. #+ since the script exits in any case after running out of commands.
Example 32-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.
4. umask 177 # Make sure temp files are not world readable.
7. TRUE=1
8. LOGFILE=/var/log/messages
9. # Note that $LOGFILE must be readable
10. #+ (as root, chmod 644 /var/log/messages).
11. TEMPFILE=temp.$$
12. # Create a "unique" temp file name, using process id of the script.
13. # Using 'mktemp' is an alternative.
14. # For example:
15. # TEMPFILE=`mktemp temp.XXXXXX`
16. KEYWORD=address
17. # At logon, the line "remote IP address xxx.xxx.xxx.xxx"
18. # appended to /var/log/messages.
19. ONLINE=22
20. USER_INTERRUPT=13
21. CHECK_LINES=100
22. # How many lines in log file to check.
24. trap 'rm -f $TEMPFILE; exit $USER_INTERRUPT' TERM INT
25. # Cleans up the temp file if script interrupted by control-c.
27. echo
29. while [ $TRUE ] #Endless loop.
30. do
31. tail -n $CHECK_LINES $LOGFILE> $TEMPFILE
32. # Saves last 100 lines of system log file as temp file.
33. # Necessary, since newer kernels generate many log messages at log on.
34. search=`grep $KEYWORD $TEMPFILE`
35. # Checks for presence of the "IP address" phrase,
36. #+ indicating a successful logon.
38. if [ ! -z "$search" ] # Quotes necessary because of possible spaces.
39. then
40. echo "On-line"
41. rm -f $TEMPFILE # Clean up temp file.
42. exit $ONLINE
43. else
44. echo -n "." # The -n option to echo suppresses newline,
45. #+ so you get continuous rows of dots.
46. fi
48. sleep 1
49. done
52. # Note: if you change the KEYWORD variable to "Exit",
53. #+ this script can be used while on-line
54. #+ to check for an unexpected logoff.
56. # Exercise: Change the script, per the above note,
57. # and prettify it.
59. exit 0
62. # Nick Drage suggests an alternate method:
64. while true
65. do ifconfig ppp0 | grep UP 1> /dev/null && echo "connected" && exit 0
66. echo -n "." # Prints dots (.....) until connected.
67. sleep 2
68. done
70. # Problem: Hitting Control-C to terminate this process may be insufficient.
71. #+ (Dots may keep on echoing.)
72. # Exercise: Fix this.
76. # Stephane Chazelas has yet another alternative:
78. CHECK_INTERVAL=1
80. while ! tail -n 1 "$LOGFILE" | grep -q "$KEYWORD"
81. do echo -n .
82. sleep $CHECK_INTERVAL
83. done
84. echo "On-line"
86. # Exercise: Discuss the relative strengths and weaknesses
87. # of each of these various approaches.
88. Example 32-7. A Simple Implementation of a Progress Bar
90. #! /bin/bash
91. # progress-bar2.sh
92. # Author: Graham Ewart (with reformatting by ABS Guide author).
93. # Used in ABS Guide with permission (thanks!).
95. # Invoke this script with bash. It doesn't work with sh.
97. interval=1
98. long_interval=10
100. {
101. trap "exit" SIGUSR1
102. sleep $interval; sleep $interval
103. while true
104. do
105. echo -n '.' # Use dots.
106. sleep $interval
107. done; } & # Start a progress bar as a background process.
109. pid=$!
110. trap "echo !; kill -USR1 $pid; wait $pid" EXIT # To handle ^C.
112. echo -n 'Long-running process '
113. sleep $long_interval
114. echo ' Finished!'
116. kill -USR1 $pid
117. wait $pid # Stop the progress bar.
118. trap EXIT
120. exit $?
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 32-8. Tracing a variable
2. #!/bin/bash
4. trap 'echo "VARIABLE-TRACE> \$variable = \"$variable\""' DEBUG
5. # Echoes the value of $variable after every command.
7. variable=29; line=$LINENO
9. echo " Just initialized \$variable to $variable in line number $line."
11. let "variable *= 3"; line=$LINENO
12. echo " Just multiplied \$variable by 3 in line number $line."
14. exit 0
16. # The "trap 'command1 . . . command2 . . .' DEBUG" construct is
17. #+ more appropriate in the context of a complex script,
18. #+ where inserting multiple "echo $variable" statements might be
19. #+ awkward and time-consuming.
21. # Thanks, Stephane Chazelas for the pointer.
Output of script:
VARIABLE-TRACE> $variable = “” VARIABLE-TRACE> $variable = “29” Just initialized $variable to 29. VARIABLE-TRACE> $variable = “29” VARIABLE-TRACE> $variable = “87” Just multiplied $variable by 3. VARIABLE-TRACE> $variable = “87” Of course, the trap command has other uses aside from debugging, such as disabling certain keystrokes within a script (see Example A-43).
Example 32-9. Running multiple processes (on an SMP box)
2. #!/bin/bash
3. # parent.sh
4. # Running multiple processes on an SMP box.
5. # Author: Tedman Eng
7. # This is the first of two scripts,
8. #+ both of which must be present in the current working directory.
13. LIMIT=$1 # Total number of process to start
14. NUMPROC=4 # Number of concurrent threads (forks?)
15. PROCID=1 # Starting Process ID
16. echo "My PID is $$"
18. function start_thread() {
19. if [ $PROCID -le $LIMIT ] ; then
20. ./child.sh $PROCID&
21. let "PROCID++"
22. else
23. echo "Limit reached."
24. wait
25. exit
26. fi
27. }
29. while [ "$NUMPROC" -gt 0 ]; do
30. start_thread;
31. let "NUMPROC--"
32. done
35. while true
36. do
38. trap "start_thread" SIGRTMIN
40. done
42. exit 0
46. # ======== Second script follows ========
49. #!/bin/bash
50. # child.sh
51. # Running multiple processes on an SMP box.
52. # This script is called by parent.sh.
53. # Author: Tedman Eng
55. temp=$RANDOM
56. index=$1
57. shift
58. let "temp %= 5"
59. let "temp += 4"
60. echo "Starting $index Time:$temp" "$@"
61. sleep ${temp}
62. echo "Ending $index"
63. kill -s SIGRTMIN $PPID
65. exit 0
68. # ======================= SCRIPT AUTHOR'S NOTES ======================= #
69. # It's not completely bug free.
70. # I ran it with limit = 500 and after the first few hundred iterations,
71. #+ one of the concurrent threads disappeared!
72. # Not sure if this is collisions from trap signals or something else.
73. # Once the trap is received, there's a brief moment while executing the
74. #+ trap handler but before the next trap is set. During this time, it may
75. #+ be possible to miss a trap signal, thus miss spawning a child process.
77. # No doubt someone may spot the bug and will be writing
78. #+ . . . in the future.
82. # ===================================================================== #
86. # ----------------------------------------------------------------------#
90. #################################################################
91. # The following is the original script written by Vernia Damiano.
92. # Unfortunately, it doesn't work properly.
93. #################################################################
95. #!/bin/bash
97. # Must call script with at least one integer parameter
98. #+ (number of concurrent processes).
99. # All other parameters are passed through to the processes started.
102. INDICE=8 # Total number of process to start
103. TEMPO=5 # Maximum sleep time per process
104. E_BADARGS=65 # No arg(s) passed to script.
106. if [ $# -eq 0 ] # Check for at least one argument passed to script.
107. then
108. echo "Usage: `basename $0` number_of_processes [passed params]"
109. exit $E_BADARGS
110. fi
112. NUMPROC=$1 # Number of concurrent process
113. shift
114. PARAMETRI=( "$@" ) # Parameters of each process
116. function avvia() {
117. local temp
118. local index
119. temp=$RANDOM
120. index=$1
121. shift
122. let "temp %= $TEMPO"
123. let "temp += 1"
124. echo "Starting $index Time:$temp" "$@"
125. sleep ${temp}
126. echo "Ending $index"
127. kill -s SIGRTMIN $$
128. }
130. function parti() {
131. if [ $INDICE -gt 0 ] ; then
132. avvia $INDICE "${PARAMETRI[@]}" &
133. let "INDICE--"
134. else
135. trap : SIGRTMIN
136. fi
137. }
139. trap parti SIGRTMIN
141. while [ "$NUMPROC" -gt 0 ]; do
142. parti;
143. let "NUMPROC--"
144. done
146. wait
147. trap - SIGRTMIN
149. exit $?
151. : <<SCRIPT_AUTHOR_COMMENTS
152. I had the need to run a program, with specified options, on a number of
153. different files, using a SMP machine. So I thought [I'd] keep running
154. a specified number of processes and start a new one each time . . . one
155. of these terminates.
157. The "wait" instruction does not help, since it waits for a given process
158. or *all* process started in background. So I wrote [this] bash script
159. that can do the job, using the "trap" instruction.
160. --Vernia Damiano
161. SCRIPT_AUTHOR_COMMENTS
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
