本文全面系统地介绍了shell脚本调试技术,包括使用echo, tee,
trap等命令输出关键信息,跟踪变量的值,在脚本中植入调试钩子,使用“-n”选项进行shell脚本的语法检查,使用“-x”选项实现shell脚本
逐条语句的跟踪,巧妙地利用shell的内置变量增强“-x”选项的输出信息等。
一. 前言
我们之所以要进行Shell脚本调试,就是为
了发现引发脚本错误的原因以及在脚本源代码中定位发生错误的行,常用的手段包括分析输出的错误信息,在脚本中加入调试语句,输出调试信息来辅助诊断错误,
利用调试工具等。但与其它高级语言(java,c++等)相比,shell解释器缺乏相应的调试机制和调试工具的支持,其输出的错误信息又往往很不明确,
初学者在调试脚本时,除了知道用echo语句输出一些信息外,别无它法,而仅仅依赖于大量的加入echo语句来诊断错误,确实令人不胜其繁,故常见初学者
抱怨shell脚本太难调试了。
现在我们系统地学习一些重要的shell脚本调试技术,相信对很多shell的初学者有很大帮助的。
阅读下面内容前,要求读者具有基本的shell编程知识。本文实验平台是Bash3.1+Redhat Enterprise Server 4.0,但所介绍的调试技巧应该也同样适用于其它shell和Unix平台。
二. 在shell脚本中输出调试信息
通过在程序中加入调试语句把一些关键地方或出错的地方的相关信息显示出来。
Shell程序员通常使用echo(ksh程序员常使用print)语句输出信息,但仅仅依靠echo语句的输出跟踪信息太麻烦,调试阶段在脚本中加入的大量的echo语句在产品成形时还得再一一删除掉。这样,这里来说下如何方便有效的输出调试信息。
1. 使用trap命令
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
从运行结果中我们可以清晰的看到每执行一条命令之后,相关变量的值的变化。同时,从运行结果中打印出来的行号来分析,可以看到整个脚本的执行轨迹,能够判断出哪些条件分支执行了,哪些条件分支没有执行。
2. 使用tee命令
在
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即可得到正确的结果。
阅读(1843) | 评论(0) | 转发(0) |