Chinaunix首页 | 论坛 | 博客
  • 博客访问: 118918
  • 博文数量: 121
  • 博客积分: 1400
  • 博客等级: 上尉
  • 技术积分: 1210
  • 用 户 组: 普通用户
  • 注册时间: 2009-10-03 23:43
文章分类

全部博文(121)

文章存档

2011年(1)

2010年(6)

2009年(114)

我的朋友

分类: 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
上面的脚本有什么错误(线索: 注意if的后面)?


例子 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
 	
注意错误信息中说明的错误行不必一定要参考, 但那行是Bash解释器最终认识到是个错误的地方.

出错信息可能在报告语法错误的行号时会忽略脚本的注释行.

如果脚本可以执行,但不是你所期望的那样工作怎么办? 这大多是由于常见的逻辑错误产生的.


例子 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.

总结该脚本的症状,

  1. 终止于一个"syntax error"(语法错误)的信息, 或

  2. 它能运行, 但不是按期望的那样运行(逻辑错误).

  3. 它能运行,运行的和期望的一样, 但有讨厌的副作用 (逻辑炸弹).

用来调试不能工作的脚本的工具包括

  1. 语句可用在脚本中的有疑问的点上以跟踪了解变量的值, 并且也可以了解后续脚本的动作.

    Tip

    最好只在调试时才使用echo语句.

       1 ### debecho (debug-echo), by Stefano Falsetto ###
       2 ### 只有变量 DEBUG 设置了值时才会打印传递进来的变量值. ###
       3 debecho () {
       4   if [ ! -z "$DEBUG" ]; then
       5      echo "$1" >&2
       6      #         ^^^ 打印到标准出错
       7   fi
       8 }
       9 
      10 DEBUG=on
      11 Whatever=whatnot
      12 debecho $Whatever   # whatnot
      13 
      14 DEBUG=
      15 Whatever=notwhat
      16 debecho $Whatever   # (这儿就不会打印了.)

  2. 使用 过滤器来检查临界点的进程或数据流.

  3. 设置选项 -n -v -x

    sh -n scriptname 不会实际运行脚本,而只是检查脚本的语法错误. 这等同于把 set -nset -o noexec 插入脚本中. 注意还是有一些语法错误不能被这种检查找出来.

    sh -v scriptname 在实际执行一个命令前打印出这个命令. 这也等同于在脚本里设置 set -vset -o verbose.

    选项 -n-v 可以一块使用. sh -nv scriptname 会打印详细的语法检查.

    sh -x scriptname 打印每个命令的执行结果, 但只用在某些小的方面. 它等同于脚本中插入 set -xset -o xtrace.

    set -uset -o nounset 插入到脚本里并运行它, 就会在每个试图使用没有申明过的变量的地方打印出一个错误信息.

  4. 使用一个"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

  5. 用变量和内建的.

  6. 捕捉exit.

    脚本中的The exit 命令会触发信号0,终结进程,即脚本本身. 这常用来捕捉exit命令做某事, 如强制打印变量值. trap 命令必须是脚本中第一个命令.

捕捉信号

trap

当收到一个信号时指定一个处理动作; 这在调试时也很有用.

Note

信号是发往一个进程的非常简单的信息, 要么是由内核发出要么是由另一个进程, 以告诉接收进程采取一些指定的动作 (一般是中止). 例如, 按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 #

Note

trapDEBUG参数在每个命令执行完后都会引起一个指定的执行动作,例如,这可用来跟踪变量。.


例子 29-7. 跟踪变量

   1 #!/bin/bash
   2 
   3 trap 'echo "VARIABLE-TRACE> \$variable = \"$variable\""' DEBUG
   4 # 在每个命令行显示变量$variable 的值.
   5 
   6 variable=29
   7 
   8 echo "Just initialized \"\$variable\" to $variable."
   9 
  10 let "variable *= 3"
  11 echo "Just multiplied \"\$variable\" by 3."
  12 
  13 exit $?
  14 
  15 #  "trap 'command1 . . . command2 . . .' DEBUG" 的结构适合复杂脚本的环境
  16 #+ 在这种情况下多次"echo $variable"比较没有技巧并且也耗时.
  17 #
  18 #
  19 
  20 # Thanks, Stephane Chazelas 指出这一点.
  21 
  22 
  23 脚本的输出:
  24 
  25 VARIABLE-TRACE> $variable = ""
  26 VARIABLE-TRACE> $variable = "29"
  27 Just initialized "$variable" to 29.
  28 VARIABLE-TRACE> $variable = "29"
  29 VARIABLE-TRACE> $variable = "87"
  30 Just multiplied "$variable" by 3.
  31 VARIABLE-TRACE> $variable = "87"

当然, 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 : <


Note

trap '' SIGNAL (两个引号引空) 在脚本中禁用了 SIGNAL 信号的动作(即忽略了). trap SIGNAL 则恢复了 SIGNAL 信号前次的处理动作. 这在保护脚本的某些临界点的位置不受意外的中断影响时很有用.

   1 	trap '' 2  # 信号 2是  Control-C, 现在被忽略了.
   2 	command
   3 	command
   4 	command
   5 	trap 2     # 再启用Control-C
   6 	

阅读(1106) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~