2009年4月7日星期二

使用trap/tee命令/调试钩子调试Shell脚

使用trap/tee命令/调试钩子调试Shell脚本

发布: 2008-3-21 16:14 | 作者: 剑心通明 | 来源: 互联网 | 查看: 151次

BSD爱好者乐园5`Xw4t8q

本文全面系统地介绍了shell脚本调试技术,包括使用echo, tee, trap等命令输出关键信息,跟踪变量的值,在脚本中植入调试钩子,使用"-n"选项进行shell脚本的语法检查, 使用"-x"选项实现shell脚本逐条语句的跟踪,巧妙地利用shell的内置变量增强"-x"选项的输出信息等。

(ES6](pn+T"g

5ys6Qm\e%b y一. 前言
&f [*ix*Rh_EKP     我们之所以要进行Shell脚本调试,就是为了发现引发脚本错误的原因以及在脚本源代码中定位发生错误的行,常用的手段包括分析输出的错误信息,在脚本中 加入调试语句,输出调试信息来辅助诊断错误,利用调试工具等。但与其它高级语言(java,c++等)相比,shell解释器缺乏相应的调试机制和调试工 具的支持,其输出的错误信息又往往很不明确,初学者在调试脚本时,除了知道用echo语句输出一些信息外,别无它法,而仅仅依赖于大量的加入echo语句 来诊断错误,确实令人不胜其繁,故常见初学者抱怨shell脚本太难调试了。
&g1q{;u$E"O6]L    现在我们系统地学习一些重要的shell脚本调试技术,相信对很多shell的初学者有很大帮助的。BSD爱好者乐园/m4\7} gx*b&`&b
    阅读下面内容前,要求读者具有基本的shell编程知识。本文实验平台是Bash3.1+Redhat Enterprise Server 4.0,但所介绍的调试技巧应该也同样适用于其它shell和Unix平台。

!l0J s'f2WE7A4oJBSD爱好者乐园4G~ NA�t

二. 在shell脚本中输出调试信息BSD爱好者乐园.qt Se;F4O$p3k6V

BSD爱好者乐园]~R:n DQ3nV

    通过在程序中加入调试语句把一些关键地方或出错的地方的相关信息显示出来。BSD爱好者乐园rZi4t"q P,^B
    Shell程序员通常使用echo(ksh程序员常使用print)语句输出信息,但仅仅依靠echo语句的输出跟踪信息太麻烦,调试阶段在脚本中加入的大量的echo语句在产品成形时还得再一一删除掉。这样,这里来说下如何方便有效的输出调试信息。

H~ ]xS!i\OBSD爱好者乐园(Q'H Mm \(A1g�u

1. 使用trap命令BSD爱好者乐园A*C6k H kj8D ~ Q

fg)F qT+\ FT`    trap命令用于捕获指定的信号并执行预定义的命令。其基本的语法是:BSD爱好者乐园S7o&} V~2o
trap 'command' signalBSD爱好者乐园V2Qy,[/]VH�c
    其中signal是要捕获的信号,command是捕获到指定的信号之后,所要执行的命令。BSD爱好者乐园9F1~d:B/^.G#X$gB"q
    我们可以用kill –l命令看到系统中全部可用的信号名,捕获信号后所执行的命令可以是任何一条或多条合法的shell语句,也可以是一个函数名。shell脚本在执行时, 会产生三个所谓的"伪信号"(之所以称之为"伪信号"是因为这三个信号是由shell产生的,而其它的信号是由操作系统产生的),通过使用trap命令捕 获这三个"伪信号"并输出相关信息对调试非常有帮助。

VIw"X Jz FS

2Gd3JcO ?P5nu u~表 1. shell伪信号
)M@.`6g6X X信号名  何时产生BSD爱好者乐园6J;|-D7O'X IG
EXIT  从一个函数中退出或整个脚本执行完毕
4f2vb2s!S2e7IQFERR  当一条命令返回非零状态时(代表命令执行不成功)BSD爱好者乐园.s,t4Gf Csns2J/`
DEBUG  脚本中每一条命令执行之前

!gs[(O:dflBSD爱好者乐园-X IK3@$wUn-j

   通过捕获EXIT信号,我们可以在shell脚本中止执行或从函数中退出时,输出某些想要跟踪的变量的值,并由此来判断脚本的执行状态以及出错原因,其使用方法是:BSD爱好者乐园#J!s%q-Z }8o F%lj
trap 'command' EXIT 或者 trap 'command' 0BSD爱好者乐园/R1_Ds{y1e YO'J8|

BSD爱好者乐园 ?&PJhU

   通过捕获ERR信号,我们可以方便的追踪执行不成功的命令或函数,并输出相关的调试信息,以下是一个捕获ERR信号的示例程序,其中的$LINENO是一个shell的内置变量,代表shell脚本的当前行号。BSD爱好者乐园mr"Bx4w c;hM C
   举例:BSD爱好者乐园 q*@ V/C:CR^ I
$ cat -n exp1.shBSD爱好者乐园 Xdd jVA
     1  ERRTRAP()BSD爱好者乐园/c1}B�l!@ F~-t%r
     2  {
*D{5Z/o2Fe     3    echo "[LINE:$1] Error: Command or function exited with status $?"
]%t!u+qZl2t     4  }
i.R5`%W+i?DHH)M     5  foo()BSD爱好者乐园R�\;U.O,fH!B^.]
     6  {
!Xf,D/Ys'd     7    return 1;
G!U#ot#C:gEj     8  }
y `X4D(|'~%m `FQ     9  trap 'ERRTRAP $LINENO' ERRBSD爱好者乐园-A Gva5Us4`+{{
    10  abc
5a/BG8c7NT8E-Dw    11  foo

z1\eou m$~;d

5x'{W6Q3`~/yg其输出结果如下:BSD爱好者乐园8K#PWFQaEp E

BSD爱好者乐园7Bk aX-N$n+]0oM N

$ sh exp1.shBSD爱好者乐园8QKAao.k
exp1.sh: line 10: abc: command not foundBSD爱好者乐园/J,K!}x8L-iJ2a,[N
[LINE:10] Error: Command or function exited with status 127BSD爱好者乐园3r srl,c~\9K
[LINE:11] Error: Command or function exited with status 1

{+j1y$H;`2|PBSD爱好者乐园+K3l,JC Vl@`(cQ

    在调试过程中,为了跟踪某些变量的值,我们常常需要在shell脚本的许多地方插入相同的echo语句来打印相关变量的值,这种做法显得烦琐而笨拙。而通过捕获DEBUG信号,我们只需要一条trap语句就可以完成对相关变量的全程跟踪。

7Z9C"D*dI8x$P;{BSD爱好者乐园5ZeCD'@:RXR

   下面是一个通过捕获DEBUG信号来跟踪变量的示例程序:
'P4c'F t7|!d$ cat –n exp2.sh
}5P,Cfz7[D/L     1  #!/bin/bashBSD爱好者乐园:u:``9Y\&r6`*|
     2  trap 'echo "before execute line:$LINENO, a=$a,b=$b,c=$c"' DEBUG
9W�s.D-]-n/W2N     3  a=1BSD爱好者乐园5k-U0g+r.rQU
     4  if [ "$a" -eq 1 ]
'~W A P/aI#N`     5  thenBSD爱好者乐园H-Y-]RN BV
     6     b=2
*B9ID@-Dn;Gu     7  elseBSD爱好者乐园fP tv$h(B
     8     b=1
Q9Ik1Q0j}     9  fiBSD爱好者乐园J"o{,K#X
    10  c=3
:RvH:N y'L    11  echo "end"BSD爱好者乐园5c:i L5e ~(W@
其输出结果如下:BSD爱好者乐园\(C%S.lB*e,CH
$ sh exp2.sh
aH]S1v8]before execute line:3, a=,b=,c=
1b;LF \Z a{before execute line:4, a=1,b=,c=
j iA6b,b0qi0C2n.`+OGbefore execute line:6, a=1,b=,c=BSD爱好者乐园T hX0[Y dL d
before execute line:10, a=1,b=2,c=BSD爱好者乐园 E8m0\'P F"bvzt
before execute line:11, a=1,b=2,c=3BSD爱好者乐园"P9t'p ]D5}H
end

z+h*]b f3M$b$t:bBSD爱好者乐园t$_.A3tT3{,TBM i

    从运行结果中我们可以清晰的看到每执行一条命令之后,相关变量的值的变化。同时,从运行结果中打印出来的行号来分析,可以看到整个脚本的执行轨迹,能够判断出哪些条件分支执行了,哪些条件分支没有执行。BSD爱好者乐园3B3Q5_r$jD:K.a�P

8Q{d be9q$zU%n:\dE2. 使用tee命令BSD爱好者乐园~`'Ox:PfJ\

BSD爱好者乐园9E)he U;^jx`l,O

    在shell脚本中管道以及输入输出重定向使用得非常多,在管道的作用下,一些命令的执行结果直接成为了下一条命令的输入。如果我们发现由管道连接起来的 一批命令的执行结果并非如预期的那样,就需要逐步检查各条命令的执行结果来判断问题出在哪儿,但因为使用了管道,这些中间结果并不会显示在屏幕上,给调试 带来了困难,这时我们就可以借助于tee命令了。

,o3S"d_^GBSD爱好者乐园wgKMe{

    tee命令会从标准输入读取数据,将其内容输出到标准输出设备,同时又可将内容保存成文件。例如有如下的脚本片段,其作用是获取本机的ip地址:BSD爱好者乐园pY&B r}H(k

BSD爱好者乐园 `RUS#|

ipaddr=`/sbin/ifconfig | grep 'inet addr:' | grep -v '127.0.0.1'
1H }$v~}l:SC.i| cut -d : -f3 | awk '{print $1}'`BSD爱好者乐园"G/f5x-p:D.hS7{
#注意=号后面的整句是用反引号(数字1键的左边那个键)括起来的。
8lu}}^-E gKxoon']echo $ipaddrBSD爱好者乐园U)`@l9t:S1P0h|&a

BSD爱好者乐园5z g8W q,u!A

   运行这个脚本,实际输出的却不是本机的ip地址,而是广播地址,这时我们可以借助tee命令,输出某些中间结果,将上述脚本片段修改为:BSD爱好者乐园]8Y)DtD7yBS

"I g!@c*IU3Iipaddr=`/sbin/ifconfig | grep 'inet addr:' | grep -v '127.0.0.1'BSD爱好者乐园| lc-[tF)r"ga
| tee temp.txt | cut -d : -f3 | awk '{print $1}'`
8W MTK#R_6sXY:gLecho $ipaddr

){Z#ajKBSD爱好者乐园0T8\d)I_

  之后,将这段脚本再执行一遍,然后查看temp.txt文件的内容:

kw5N{j*kBSD爱好者乐园o2]"KAR1\0~-XY

$ cat temp.txtBSD爱好者乐园f/TQ1S5N ~$z1@@
inet addr:192.168.0.1  Bcast:192.168.0.255  Mask:255.255.255.0

fy`5A A lJ}sBSD爱好者乐园y3[0s+^#s!Xb%i _3a

    我们可以发现中间结果的第二列(列之间以:号分隔)才包含了IP地址,而在上面的脚本中使用cut命令截取了第三列,所以我们只需将脚本中的cut -d : -f3改为cut -d : -f2即可得到正确的结果。BSD爱好者乐园V4d-Q b^m%|$w!`2o

BSD爱好者乐园[z\6O#g(Z

    具体到上述的脚本例子,我们也许并不需要tee命令的帮助,比如我们可以分段执行由管道连接起来的各条命令并查看各命令的输出结果来诊断错误,但在一些复 杂的shell脚本中,这些由管道连接起来的命令可能又依赖于脚本中定义的一些其它变量,这时我们想要在提示符下来分段运行各条命令就会非常麻烦了,简单 地在管道之间插入一条tee命令来查看中间结果会更方便一些。BSD爱好者乐园J_,LSw,i!s

BSD爱好者乐园o)Q'O1G4k"o.}!gDtZ

3. 使用"调试钩子"BSD爱好者乐园HU^3I*~:tT4o,Q

-`{Z(lI6l6a&J)l    学过C语言程序的朋友有这个印象吗:我们经常使用DEBUG宏来控制是否要输出调试信息,在shell脚本中我们同样可以使用这样的机制,如下列代码所示:BSD爱好者乐园*`D6]"@+r6AQ#w j s

BSD爱好者乐园8{2C fMD.f

if [ "$DEBUG" = "true" ]; then
0V/uM:C;G6W ymOecho "debugging"  #此处可以输出调试信息
$^:[%D3[6L7H6H-h�m4P}fiBSD爱好者乐园ef5T5C"s%t#D Um

o@@ f6x3Sw    这样的代码块通常称之为"调试钩子"或"调试块"。在调试钩子内部可以输出任何您想输出的调试信息,使用调试钩子的好处是它是可以通过DEBUG变量来控 制的,在脚本的开发调试阶段,可以先执行export DEBUG=true命令打开调试钩子,使其输出调试信息,而在把脚本交付使用时,也无需再费事把脚本中的调试语句一一删除。

g$?P"UjdT

kX$`n,v nG-PJ5Z    如果在每一处需要输出调试信息的地方均使用if语句来判断DEBUG变量的值,还是显得比较繁琐,通过定义一个DEBUG函数可以使植入调试钩子的过程更简洁方便,如下面代码所示:BSD爱好者乐园G1h?.I3_4ry

c$t)T/T4uX2G$ cat –n exp3.shBSD爱好者乐园En:hX N#I
     1  DEBUG()
_+sY&U2AD     2  {
N%S4]J Z     3  if [ "$DEBUG" = "true" ]; then
2}8g:V M {.b\/H     4      $@  BSD爱好者乐园eW-Gx"D
     5  fi
6QU2Y&H!s;g;k-y.Y     6  }BSD爱好者乐园.ql,W l ^{3nv1F�CPE Q
     7  a=1
!YkY0P'Z#v J%[     8  DEBUG echo "a=$a"
G ?C-H~/IU�c_     9  if [ "$a" -eq 1 ]
,A$p.~4qfkM:}?    10  then
Qbni g    11       b=2
4|Q_kz ]Em^    12  elseBSD爱好者乐园Qn2Z"y2uH0Z9[
    13       b=1
ysG5g)s    14  fiBSD爱好者乐园i*W i ?G od
    15  DEBUG echo "b=$b"
uk;o&\-S#`w {u{    16  c=3BSD爱好者乐园4W1dg^T1@ Q
    17  DEBUG echo "c=$c"BSD爱好者乐园`dh9E8NY`:o:lV

F-DSU]f3J    在上面所示的DEBUG函数中,会执行任何传给它的命令,并且这个执行过程是可以通过DEBUG变量的值来控制的,我们可以把所有跟调试有关的命令都作为DEBUG函数的参数来调用,非常的方便。