Shell脚本调试技术

一.前言html

shell编程在unix/linux世界中使用得很是普遍,熟练掌握shell编程也是成为一名优秀的unix/linux开发者和系统管理员的必经之路。脚本调试的主要工做就是发现引起脚本错误的缘由以及在脚本源代码中定位发生错误的行,经常使用的手段包括分析输出的错误信息,经过在脚本中加入调试语句,输出调试信息来辅助诊断错误,利用调试工具等。但与其它高级语言相比,shell解释器缺少相应的调试机制和调试工具的支持,其输出的错误信息又每每很不明确,初学者在调试脚本时,除了知道用echo语句输出一些信息外,别无它法,而仅仅依赖于大量的加入echo语句来诊断错误,确实使人不胜其繁,故常见初学者抱怨shell脚本太难调试了。本文将系统地介绍一些重要的shell脚本调试技术,但愿能对shell的初学者有所裨益。
linux

 
 


本文的目标读者是unix/linux环境下的开发人员,测试人员和系统管理员,要求读者具备基本的shell编程知识。本文所使用范例在Bash3.1+Redhat Enterprise Server 4.0下测试经过,但所述调试技巧应也一样适用于其它shell。
经过在程序中加入调试语句把一些关键地方或出错的地方的相关信息显示出来是最多见的调试手段。Shell程序员一般使用echo(ksh程序员常使用print)语句输出信息,但仅仅依赖echo语句的输出跟踪信息很麻烦,调试阶段在脚本中加入的大量的echo语句在产品交付时还得再费力一一删除。针对这个问题,本节主要介绍一些如何方便有效的输出调试信息的方法。
trap命令用于捕获指定的信号并执行预约义的命令。
其基本的语法是:
trap 'command' signal
其中signal是要捕获的信号,command是捕获到指定的信号以后,所要执行的命令。能够用kill –l命令看到系统中所有可用的信号名,捕获信号后所执行的命令能够是任何一条或多条合法的shell语句,也能够是一个函数名。
shell脚本在执行时,会产生三个所谓的“伪信号”,(之因此称之为“伪信号”是由于这三个信号是由shell产生的,而其它的信号是由操做系统产生的),经过使用trap命令捕获这三个“伪信号”并输出相关信息对调试很是有帮助。

表 1. shell伪信号
信号名 什么时候产生
EXIT 从一个函数中退出或整个脚本执行完毕
ERR 当一条命令返回非零状态时(表明命令执行不成功)
DEBUG 脚本中每一条命令执行以前
经过捕获EXIT信号,咱们能够在shell脚本停止执行或从函数中退出时,输出某些想要跟踪的变量的值,并由此来判断脚本的执行状态以及出错缘由,其使用方法是:
trap 'command' EXIT 或 trap 'command' 0
经过捕获ERR信号,咱们能够方便的追踪执行不成功的命令或函数,并输出相关的调试信息,如下是一个捕获ERR信号的示例程序,其中的$LINENO是一个shell的内置变量,表明shell脚本的当前行号。
$ cat -n exp1.sh
1 ERRTRAP()
2 {
3 echo "[LINE:$1] Error: Command or function exited with status $?"
4 }
5 foo()
6 {
7 return 1;
8 }
9 trap 'ERRTRAP $LINENO' ERR
10 abc
11 foo

其输出结果以下:
$ sh exp1.sh
exp1.sh: line 10: abc: command not found
[LINE:10] Error: Command or function exited with status 127
[LINE:11] Error: Command or function exited with status 1

在调试过程当中,为了跟踪某些变量的值,咱们经常须要在shell脚本的许多地方插入相同的echo语句来打印相关变量的值,这种作法显得烦琐而笨拙。而经过捕获DEBUG信号,咱们只须要一条trap语句就能够完成对相关变量的全程跟踪。
如下是一个经过捕获DEBUG信号来跟踪变量的示例程序:
$ cat –n exp2.sh
1 #!/bin/bash
2 trap 'echo “before execute line:$LINENO, a=$a,b=$b,c=$c”' DEBUG
3 a=1
4 if [ "$a" -eq 1 ]
5 then
6 b=2
7 else
8 b=1
9 fi
10 c=3
11 echo "end"

其输出结果以下:
$ sh exp2.sh
before execute line:3, a=,b=,c=
before execute line:4, a=1,b=,c=
before execute line:6, a=1,b=,c=
before execute line:10, a=1,b=2,c=
before execute line:11, a=1,b=2,c=3
end

从运行结果中能够清晰的看到每执行一条命令以后,相关变量的值的变化。同时,从运行结果中打印出来的行号来分析,能够看到整个脚本的执行轨迹,可以判断出哪些条件分支执行了,哪些条件分支没有执行。
在shell脚本中管道以及输入输出重定向使用得很是多,在管道的做用下,一些命令的执行结果直接成为了下一条命令的输入。若是咱们发现由管道链接起来的一批命令的执行结果并不是如预期的那样,就须要逐步检查各条命令的执行结果来判断问题出在哪儿,但由于使用了管道,这些中间结果并不会显示在屏幕上,给调试带来了困难,此时咱们就能够借助于tee命令了。
tee命令会从标准输入读取数据,将其内容输出到标准输出设备,同时又可将内容保存成文件。例若有以下的脚本片断,其做用是获取本机的ip地址:
ipaddr=`/sbin/ifconfig | grep 'inet addr:' | grep -v '127.0.0.1'
| cut -d : -f3 | awk '{print $1}'`
#注意=号后面的整句是用反引号(数字1键的左边那个键)括起来的。
echo $ipaddr

运行这个脚本,实际输出的却不是本机的ip地址,而是广播地址,这时咱们能够借助tee命令,输出某些中间结果,将上述脚本片断修改成:
ipaddr=`/sbin/ifconfig | grep 'inet addr:' | grep -v '127.0.0.1'
| tee temp.txt | cut -d : -f3 | awk '{print $1}'`
echo $ipaddr

以后,将这段脚本再执行一遍,而后查看temp.txt文件的内容:
$ cat temp.txt
inet addr:192.168.0.1 Bcast:192.168.0.255 Mask:255.255.255.0

咱们能够发现中间结果的第二列(列之间以:号分隔)才包含了IP地址,而在上面的脚本中使用cut命令截取了第三列,故咱们只需将脚本中的cut -d : -f3改成cut -d : -f2便可获得正确的结果。
具体到上述的script例子,咱们也许并不须要tee命令的帮助,好比咱们能够分段执行由管道链接起来的各条命令并查看各命令的输出结果来诊断错误,但在一些复杂的shell脚本中,这些由管道链接起来的命令可能又依赖于脚本中定义的一些其它变量,这时咱们想要在提示符下来分段运行各条命令就会很是麻烦了,简单地在管道之间插入一条tee命令来查看中间结果会更方便一些。
在C语言程序中,咱们常用DEBUG宏来控制是否要输出调试信息,在shell脚本中咱们一样可使用这样的机制,以下列代码所示:
if [ “$DEBUG” = “true” ]; then
echo “debugging” #此处能够输出调试信息
fi

这样的代码块一般称之为“调试钩子”或“调试块”。在调试钩子内部能够输出任何您想输出的调试信息,使用调试钩子的好处是它是能够经过DEBUG变量来控制的,在脚本的开发调试阶段,能够先执行export DEBUG=true命令打开调试钩子,使其输出调试信息,而在把脚本交付使用时,也无需再费事把脚本中的调试语句一一删除。
若是在每一处须要输出调试信息的地方均使用if语句来判断DEBUG变量的值,仍是显得比较繁琐,经过定义一个DEBUG函数可使植入调试钩子的过程更简洁方便,以下面代码所示:
$ cat –n exp3.sh
1 DEBUG()
2 {
3 if [ "$DEBUG" = "true" ]; then
4 $@  
5 fi
6 }
7 a=1
8 DEBUG echo "a=$a"
9 if [ "$a" -eq 1 ]
10 then
11 b=2
12 else
13 b=1
14 fi
15 DEBUG echo "b=$b"
16 c=3
17 DEBUG echo "c=$c"

在上面所示的DEBUG函数中,会执行任何传给它的命令,而且这个执行过程是能够经过DEBUG变量的值来控制的,咱们能够把全部跟调试有关的命令都做为DEBUG函数的参数来调用,很是的方便。


 


上一节所述的调试手段是经过修改shell脚本的源代码,令其输出相关的调试信息来定位错误的,那有没有不修改源代码来调试shell脚本的方法呢?答案就是使用shell的执行选项,本节将介绍一些经常使用选项的用法:
-n 只读取shell脚本,但不实际执行
-x 进入跟踪方式,显示所执行的每一条命令
-c "string" 从strings中读取命令
“-n”可用于测试shell脚本是否存在语法错误,但不会实际执行命令。在shell脚本编写完成以后,实际执行以前,首先使用“-n”选项来测试脚本是否存在语法错误是一个很好的习惯。由于某些shell脚本在执行时会对系统环境产生影响,好比生成或移动文件等,若是在实际执行才发现语法错误,您不得不手工作一些系统环境的恢复工做才能继续测试这个脚本。
“-c”选项使shell解释器从一个字符串中而不是从一个文件中读取并执行shell命令。当须要临时测试一小段脚本的执行结果时,可使用这个选项,以下所示:
sh -c 'a=1;b=2;let c=$a+$b;echo "c=$c"'
"-x"选项可用来跟踪脚本的执行,是调试shell脚本的强有力工具。“-x”选项使shell在执行脚本的过程当中把它实际执行的每个命令行显示出来,而且在行首显示一个"+"号。 "+"号后面显示的是通过了变量替换以后的命令行的内容,有助于分析实际执行的是什么命令。 “-x”选项使用起来简单方便,能够轻松对付大多数的shell调试任务,应把其看成首选的调试手段。
若是把本文前面所述的trap ‘command’ DEBUG机制与“-x”选项结合起来,咱们就能够既输出实际执行的每一条命令,又逐行跟踪相关变量的值,对调试至关有帮助。
仍之前面所述的exp2.sh为例,如今加上“-x”选项来执行它:
$ sh –x exp2.sh
+ trap 'echo "before execute line:$LINENO, a=$a,b=$b,c=$c"' DEBUG
++ echo 'before execute line:3, a=,b=,c='
before execute line:3, a=,b=,c=
+ a=1
++ echo 'before execute line:4, a=1,b=,c='
before execute line:4, a=1,b=,c=
+ '[' 1 -eq 1 ']'
++ echo 'before execute line:6, a=1,b=,c='
before execute line:6, a=1,b=,c=
+ b=2
++ echo 'before execute line:10, a=1,b=2,c='
before execute line:10, a=1,b=2,c=
+ c=3
++ echo 'before execute line:11, a=1,b=2,c=3'
before execute line:11, a=1,b=2,c=3
+ echo end
end

在上面的结果中,前面有“+”号的行是shell脚本实际执行的命令,前面有“++”号的行是执行trap机制中指定的命令,其它的行则是输出信息。
shell的执行选项除了能够在启动shell时指定外,亦可在脚本中用set命令来指定。 "set -参数"表示启用某选项,"set +参数"表示关闭某选项。有时候咱们并不须要在启动时用"-x"选项来跟踪全部的命令行,这时咱们能够在脚本中使用set命令,如如下脚本片断所示:
set -x    #启动"-x"选项 
要跟踪的程序段
set +x     #关闭"-x"选项

set命令一样可使用上一节中介绍的调试钩子—DEBUG函数来调用,这样能够避免脚本交付使用时删除这些调试语句的麻烦,如如下脚本片断所示:
DEBUG set -x    #启动"-x"选项 
要跟踪的程序段
DEBUG set +x    #关闭"-x"选项


"-x"执行选项是目前最经常使用的跟踪和调试shell脚本的手段,但其输出的调试信息仅限于进行变量替换以后的每一条实际执行的命令以及行首的一个"+"号提示符,竟然连行号这样的重要信息都没有,对于复杂的shell脚本的调试来讲,仍是很是的不方便。幸运的是,咱们能够巧妙地利用shell内置的一些环境变量来加强"-x"选项的输出信息,下面先介绍几个shell内置的环境变量:
$LINENO
表明shell脚本的当前行号,相似于C语言中的内置宏__LINE__
$FUNCNAME
函数的名字,相似于C语言中的内置宏__func__,但宏__func__只能表明当前所在的函数名,而$FUNCNAME的功能更强大,它是一个数组变量,其中包含了整个调用链上全部的函数的名字,故变量${FUNCNAME[0]}表明shell脚本当前正在执行的函数的名字,而变量${FUNCNAME[1]}则表明调用函数${FUNCNAME[0]}的函数的名字,余者能够依此类推。
$PS4
主提示符变量$PS1和第二级提示符变量$PS2比较常见,但不多有人注意到第四级提示符变量$PS4的做用。咱们知道使用“-x”执行选项将会显示shell脚本中每一条实际执行过的命令,而$PS4的值将被显示在“-x”选项输出的每一条命令的前面。在Bash Shell中,缺省的$PS4的值是"+"号。(如今知道为何使用"-x"选项时,输出的命令前面有一个"+"号了吧?)。
利用$PS4这一特性,经过使用一些内置变量来重定义$PS4的值,咱们就能够加强"-x"选项的输出信息。例如先执行export PS4='+{$LINENO:${FUNCNAME[0]}} ', 而后再使用“-x”选项来执行脚本,就能在每一条实际执行的命令前面显示其行号以及所属的函数名。
如下是一个存在bug的shell脚本的示例,本文将用此脚原本示范如何用“-n”以及加强的“-x”执行选项来调试shell脚本。这个脚本中定义了一个函数isRoot(),用于判断当前用户是否是root用户,若是不是,则停止脚本的执行
$ cat –n exp4.sh
1 #!/bin/bash
2 isRoot()
3 {
4 if [ "$UID" -ne 0 ]
5 return 1
6 else
7 return 0
8 fi
9 }
10 isRoot
11 if ["$?" -ne 0 ]
12 then
13 echo "Must be root to run this script"
14 exit 1
15 else
16 echo "welcome root user"
17 #do something
18 fi

首先执行sh –n exp4.sh来进行语法检查,输出以下:
$ sh –n exp4.sh
exp4.sh: line 6: syntax error near unexpected token `else'
exp4.sh: line 6: ` else'

发现了一个语法错误,经过仔细检查第6行先后的命令,咱们发现是第4行的if语句缺乏then关键字引发的(写惯了C程序的人很容易犯这个错误)。咱们能够把第4行修改成if [ "$UID" -ne 0 ]; then来修正这个错误。再次运行sh –n exp4.sh来进行语法检查,没有再报告错误。接下来就能够实际执行这个脚本了,执行结果以下:
$ sh exp4.sh
exp2.sh: line 11: [1: command not found
welcome root user

尽管脚本没有语法错误了,在执行时却又报告了错误。错误信息还很是奇怪“[1: command not found”。如今咱们能够试试定制$PS4的值,并使用“-x”选项来跟踪:
$ export PS4='+{$LINENO:${FUNCNAME[0]}} '
$ sh –x exp4.sh
+{10:} isRoot
+{4:isRoot} '[' 503 -ne 0 ']'
+{5:isRoot} return 1
+{11:} '[1' -ne 0 ']'
exp4.sh: line 11: [1: command not found
+{16:} echo 'welcome root user'
welcome root user

从输出结果中,咱们能够看到脚本实际被执行的语句,该语句的行号以及所属的函数名也被打印出来,从中能够清楚的分析出脚本的执行轨迹以及所调用的函数的内部执行状况。因为执行时是第11行报错,这是一个if语句,咱们对比分析一下同为if语句的第4行的跟踪结果:
+{4:isRoot} '[' 503 -ne 0 ']'
+{11:} '[1' -ne 0 ']'

可知因为第11行的[号后面缺乏了一个空格,致使[号与紧挨它的变量$?的值1被shell解释器看做了一个总体,并试着把这个总体视为一个命令来执行,故有“[1: command not found”这样的错误提示。只需在[号后面插入一个空格就一切正常了。
shell中还有其它一些对调试有帮助的内置变量,好比在Bash Shell中还有BASH_SOURCE, BASH_SUBSHELL等一批对调试有帮助的内置变量,您能够经过man sh或man bash来查看,而后根据您的调试目的,使用这些内置变量来定制$PS4,从而达到加强“-x”选项的输出信息的目的。




如今让咱们来总结一下调试shell脚本的过程:
首先使用“-n”选项检查语法错误,而后使用“-x”选项跟踪脚本的执行,使用“-x”选项以前,别忘了先定制PS4变量的值来加强“-x”选项的输出信息,至少应该令其输出行号信息(先执行export PS4='+[$LINENO]',更一劳永逸的办法是将这条语句加到您用户主目录的.bash_profile文件中去),这将使你的调试之旅更轻松。也能够利用trap,调试钩子等手段输出关键调试信息,快速缩小排查错误的范围,并在脚本中使用“set -x”及“set +x”对某些代码块进行重点跟踪。这样多种手段齐下,相信您已经能够比较轻松地抓出您的shell脚本中的臭虫了。若是您的脚本足够复杂,还须要更强的调试能力,可使用shell调试器bashdb,这是一个相似于GDB的调试工具,能够完成对shell脚本的断点设置,单步执行,变量观察等许多功能,使用bashdb对阅读和理解复杂的shell脚本也会大有裨益。关于bashdb的安装和使用,不属于本文范围,您可参阅 http://bashdb.sourceforge.net/上的文档并下载试用。
 原文地址 http://www.ibm.com/developerworks/cn/linux/l-cn-shell-debug/index.html