分类: LINUX
2009-10-04 01:44:37
Bash shell 没有自带调试器, 甚至没有任何调试类型的命令或结构. 脚本里的语法错误或拼写错误会产生含糊的错误信息,通常这些在调试非功能性的脚本时没什么帮助.
例子 29-1. 一个错误的脚本
1 #!/bin/bash 2 # ex74.sh 3 4 # 这是一个错误的脚本. 5 # 哪里有错? 6 7 a=37 8 9 if [$a -gt 27 ] 10 then 11 echo $a 12 fi 13 14 exit 0 |
脚本的输出:
./ex74.sh: [37: command not found |
例子 29-2. 丢失
1 #!/bin/bash 2 # missing-keyword.sh: 会产生什么样的错误信息? 3 4 for a in 1 2 3 5 do 6 echo "$a" 7 # done # 第7行的必需的关键字 'done' 被注释掉了. 8 9 exit 0 |
脚本的输出:
missing-keyword.sh: line 10: syntax error: unexpected end of file |
出错信息可能在报告语法错误的行号时会忽略脚本的注释行.
如果脚本可以执行,但不是你所期望的那样工作怎么办? 这大多是由于常见的逻辑错误产生的.
例子 29-3. test24, 另一个错误脚本
1 #!/bin/bash 2 3 # 这个脚本目的是为了删除当前目录下的所有文件,包括文件名含有空格的文件。 4 # 5 # 但不能工作. 6 # 为什么? 7 8 9 badname=`ls | grep ' '` 10 11 # 试试这个: 12 # echo "$badname" 13 14 rm "$badname" 15 16 exit 0 |
为了找出 的错误可以把echo "$badname" 行的注释去掉. echo 出来的信息对你判断是否脚本以你希望的方式运行时很有帮助.
在这个实际的例子里, rm "$badname" 不会达到想要的结果,因为$badname 没有引用起来. 加上引号以保证rm 命令只有一个参数(这就只能匹配一个文件名). 一个不完善的解决办法是删除A partial fix is to remove to quotes from $badname and to reset $IFS to contain only a newline, IFS=$'\n'. 不过, 存在更简单的办法.
1 # 修正删除包含空格文件名时出错的办法. 2 rm *\ * 3 rm *" "* 4 rm *' '* 5 # Thank you. S.C. |
总结该脚本的症状,
它能运行,运行的和期望的一样, 但有讨厌的副作用 (逻辑炸弹).
用来调试不能工作的脚本的工具包括
语句可用在脚本中的有疑问的点上以跟踪了解变量的值, 并且也可以了解后续脚本的动作.
最好只在调试时才使用echo语句.
|
使用 过滤器来检查临界点的进程或数据流.
设置选项 -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 插入到脚本里并运行它, 就会在每个试图使用没有申明过的变量的地方打印出一个错误信息.
使用一个"assert"(断言) 函数在脚本的临界点上测试变量或条件. (这是从C语言中借用来的.)
例子 29-4 用"assert"测试条件
1 #!/bin/bash 2 # assert.sh 3 4 assert () # 如果条件测试失败, 5 { #+ 则打印错误信息并退出脚本. 6 E_PARAM_ERR=98 7 E_ASSERT_FAILED=99 8 9 10 if [ -z "$2" ] # 没有传递足够的参数. 11 then 12 return $E_PARAM_ERR # 什么也不做就返回. 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 # 返回并继续执行脚本后面的代码. 25 fi 26 } 27 28 29 a=5 30 b=4 31 condition="$a -lt $b" # 会错误信息并从脚本退出. 32 # 把这个“条件”放在某个地方, 33 #+ 然后看看有什么现象. 34 35 assert "$condition" $LINENO 36 # 脚本以下的代码只有当"assert"成功时才会继续执行. 37 38 39 # 其他的命令. 40 # ... 41 echo "This statement echoes only if the \"assert\" does not fail." 42 # ... 43 # 余下的其他命令. 44 45 exit 0 |
用变量和内建的.
捕捉exit.
脚本中的The exit 命令会触发信号0,终结进程,即脚本本身. 这常用来捕捉exit命令做某事, 如强制打印变量值. trap 命令必须是脚本中第一个命令.
捕捉信号
当收到一个信号时指定一个处理动作; 这在调试时也很有用.
信号是发往一个进程的非常简单的信息, 要么是由内核发出要么是由另一个进程, 以告诉接收进程采取一些指定的动作 (一般是中止). 例如, 按Control-C, 发送一个用户中断( 即 INT 信号)到运行中的进程. |
1 trap '' 2 2 # 忽略信号 2 (Control-C), 没有指定处理动作. 3 4 trap 'echo "Control-C disabled."' 2 5 # 当按 Control-C 时显示一行信息. |
例子 29-5. 捕捉 exit
1 #!/bin/bash 2 # 用trap捕捉变量值. 3 4 trap 'echo Variable Listing --- a = $a b = $b' EXIT 5 # EXIT 是脚本中exit命令产生的信号的信号名. 6 # 7 # 由"trap"指定的命令不会被马上执行,只有当发送了一个适应的信号时才会执行。 8 # 9 10 echo "This prints before the \"trap\" --" 11 echo "even though the script. sees the \"trap\" first." 12 echo 13 14 a=39 15 16 b=36 17 18 exit 0 19 # 注意到注释掉上面一行的'exit'命令也没有什么不同, 20 #+ 这是因为执行完所有的命令脚本都会退出. |
例子 29-6. 在Control-C后清除垃圾
1 #!/bin/bash 2 # logon.sh: 简陋的检查你是否还处于连线的脚本. 3 4 umask 177 # 确定临时文件不是全部用户都可读的. 5 6 7 TRUE=1 8 LOGFILE=/var/log/messages 9 # 注意 $LOGFILE 必须是可读的 10 #+ (用 root来做:chmod 644 /var/log/messages). 11 TEMPFILE=temp.$$ 12 # 创建一个"唯一的"临时文件名, 使用脚本的进程ID. 13 # 用 'mktemp' 是另一个可行的办法. 14 # 举例: 15 # TEMPFILE=`mktemp temp.XXXXXX` 16 KEYWORD=address 17 # 上网时, 把"remote IP address xxx.xxx.xxx.xxx"这行 18 # 加到 /var/log/messages. 19 ONLINE=22 20 USER_INTERRUPT=13 21 CHECK_LINES=100 22 # 日志文件中有多少行要检查. 23 24 trap 'rm -f $TEMPFILE; exit $USER_INTERRUPT' TERM INT 25 # 如果脚本被control-c中断了,则清除临时文件. 26 27 echo 28 29 while [ $TRUE ] #死循环. 30 do 31 tail -$CHECK_LINES $LOGFILE> $TEMPFILE 32 # 保存系统日志文件的最后100行到临时文件. 33 # 这是需要的, 因为新版本的内核在登录网络时产生许多日志文件信息. 34 search=`grep $KEYWORD $TEMPFILE` 35 # 检查"IP address" 短语是不是存在, 36 #+ 它指示了一次成功的网络登录. 37 38 if [ ! -z "$search" ] # 引号是必须的,因为变量可能会有一些空白符. 39 then 40 echo "On-line" 41 rm -f $TEMPFILE # 清除临时文件. 42 exit $ONLINE 43 else 44 echo -n "." # -n 选项使echo不会产生新行符, 45 #+ 这样你可以从该行的继续打印. 46 fi 47 48 sleep 1 49 done 50 51 52 # 注: 如果你更改KEYWORD变量的值为"Exit", 53 #+ 这个脚本就能用来在网络登录后检查掉线 54 # 55 56 # 练习: 修改脚本,像上面所说的那样,并修正得更好 57 # 58 59 exit 0 60 61 62 # Nick Drage 建议用另一种方法: 63 64 while true 65 do ifconfig ppp0 | grep UP 1> /dev/null && echo "connected" && exit 0 66 echo -n "." # 在连接上之前打印点 (.....). 67 sleep 2 68 done 69 70 # 问题: 用 Control-C来终止这个进程可能是不够的. 71 #+ (点可能会继续被打印.) 72 # 练习: 修复这个问题. 73 74 75 76 # Stephane Chazelas 也提出了另一个办法: 77 78 CHECK_INTERVAL=1 79 80 while ! tail -1 "$LOGFILE" | grep -q "$KEYWORD" 81 do echo -n . 82 sleep $CHECK_INTERVAL 83 done 84 echo "On-line" 85 86 # 练习: 讨论这几个方法的优缺点. 87 # |
trap 的DEBUG参数在每个命令执行完后都会引起一个指定的执行动作,例如,这可用来跟踪变量。. 例子 29-7. 跟踪变量
|
当然, trap 命令除了调试还有其他的用处.
例子 29-8. 运行多进程 (在多处理器的机器里)
1 #!/bin/bash 2 # parent.sh 3 # 在多处理器的机器里运行多进程. 4 # 作者: Tedman Eng 5 6 # 这是要介绍的两个脚本的第一个, 7 #+ 这两个脚本都在要在相同的工作目录下. 8 9 10 11 12 LIMIT=$1 # 要启动的进程总数 13 NUMPROC=4 # 当前进程数 (forks?) 14 PROCID=1 # 启动的进程ID 15 echo "My PID is $$" 16 17 function start_thread() { 18 if [ $PROCID -le $LIMIT ] ; then 19 ./child.sh $PROCID& 20 let "PROCID++" 21 else 22 echo "Limit reached." 23 wait 24 exit 25 fi 26 } 27 28 while [ "$NUMPROC" -gt 0 ]; do 29 start_thread; 30 let "NUMPROC--" 31 done 32 33 34 while true 35 do 36 37 trap "start_thread" SIGRTMIN 38 39 done 40 41 exit 0 42 43 44 45 # ======== 下面是第二个脚本 ======== 46 47 48 #!/bin/bash 49 # child.sh 50 # 在多处理器的机器里运行多进程. 51 # 这个脚本由parent.sh脚本调用(即上面的脚本). 52 # 作者: Tedman Eng 53 54 temp=$RANDOM 55 index=$1 56 shift 57 let "temp %= 5" 58 let "temp += 4" 59 echo "Starting $index Time:$temp" "$@" 60 sleep ${temp} 61 echo "Ending $index" 62 kill -s SIGRTMIN $PPID 63 64 exit 0 65 66 67 # ======================= 脚本作者注 ======================= # 68 # 这不是完全没有bug的脚本. 69 # 我运行LIMIT = 500 ,在过了开头的一二百个循环后, 70 #+ 这些进程有一个消失了! 71 # 不能确定是不是因为捕捉信号产生碰撞还是其他的原因. 72 # 一但信号捕捉到,在下一个信号设置之前, 73 #+ 会有一个短暂的时间来执行信号处理程序, 74 #+ 这段时间内很可能会丢失一个信号捕捉,因此失去生成一个子进程的机会. 75 76 # 毫无疑问会有人能找出这个bug的原因,并且修复它 77 #+ . . . 在将来的某个时候. 78 79 80 81 # ===================================================================== # 82 83 84 85 # ----------------------------------------------------------------------# 86 87 88 89 ################################################################# 90 # 下面的脚本由Vernia Damiano原创. 91 # 不幸地是, 它不能正确工作. 92 ################################################################# 93 94 #!/bin/bash 95 96 # 必须以最少一个整数参数来调用这个脚本 97 #+ (这个整数是协作进程的数目). 98 # 所有的其他参数被传给要启动的进程. 99 100 101 INDICE=8 # 要启动的进程数目 102 TEMPO=5 # 每个进程最大的睡眼时间 103 E_BADARGS=65 # 没有参数传给脚本的错误值. 104 105 if [ $# -eq 0 ] # 检查是否至少传了一个参数给脚本. 106 then 107 echo "Usage: `basename $0` number_of_processes [passed params]" 108 exit $E_BADARGS 109 fi 110 111 NUMPROC=$1 # 协作进程的数目 112 shift 113 PARAMETRI=( "$@" ) # 每个进程的参数 114 115 function avvia() { 116 local temp 117 local index 118 temp=$RANDOM 119 index=$1 120 shift 121 let "temp %= $TEMPO" 122 let "temp += 1" 123 echo "Starting $index Time:$temp" "$@" 124 sleep ${temp} 125 echo "Ending $index" 126 kill -s SIGRTMIN $$ 127 } 128 129 function parti() { 130 if [ $INDICE -gt 0 ] ; then 131 avvia $INDICE "${PARAMETRI[@]}" & 132 let "INDICE--" 133 else 134 trap : SIGRTMIN 135 fi 136 } 137 138 trap parti SIGRTMIN 139 140 while [ "$NUMPROC" -gt 0 ]; do 141 parti; 142 let "NUMPROC--" 143 done 144 145 wait 146 trap - SIGRTMIN 147 148 exit $? 149 150 : < |
trap '' SIGNAL (两个引号引空) 在脚本中禁用了 SIGNAL 信号的动作(即忽略了). trap SIGNAL 则恢复了 SIGNAL 信号前次的处理动作. 这在保护脚本的某些临界点的位置不受意外的中断影响时很有用. |
1 trap '' 2 # 信号 2是 Control-C, 现在被忽略了. 2 command 3 command 4 command 5 trap 2 # 再启用Control-C 6 |
Bash的 增加了下面的特殊变量用于调试.
|