Chinaunix

标题: shell 调试 [打印本页]

作者: harserm    时间: 2009-10-04 01:44
标题: shell 调试

Bash shell 没有自带调试器, 甚至没有任何调试类型的命令或结构.
[1]
脚本里的语法错误或拼写错误会产生含糊的错误信息,通常这些在调试非功能性的脚本时没什么帮助.
例子 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. 丢失
关键字(keyword)

   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
为了找出
例子 29-3
的错误可以把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.
总结该脚本的症状,
  • 终止于一个"syntax error"(语法错误)的信息, 或
  • 它能运行, 但不是按期望的那样运行(逻辑错误).

  • 它能运行,运行的和期望的一样, 但有讨厌的副作用 (逻辑炸弹).
    用来调试不能工作的脚本的工具包括

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

    最好只在调试时才使用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   # (这儿就不会打印了.)

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

  • 设置选项 -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 插入到脚本里并运行它, 就会在每个试图使用没有申明过的变量的地方打印出一个错误信息.

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

  • 用变量
    $LINENO
    和内建的
    caller
    .

  • 捕捉exit.
    脚本中的The exit 命令会触发信号0,终结进程,即脚本本身.
    [2]
    这常用来捕捉exit命令做某事, 如强制打印变量值. trap 命令必须是脚本中第一个命令.
    捕捉信号
    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. 跟踪变量
       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 :

    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的
    版本
    3
    增加了下面的特殊变量用于调试.

  • $BASH_ARGC

  • $BASH_ARGV

  • $BASH_COMMAND

  • $BASH_EXECUTION_STRING

  • $BASH_LINENO

  • $BASH_SOURCE

  • $BASH_SUBSHELL



    本文来自ChinaUnix博客,如果查看原文请点:http://blog.chinaunix.net/u3/104504/showart_2063908.html




    欢迎光临 Chinaunix (http://bbs.chinaunix.net/) Powered by Discuz! X3.2