我们掌握了编写脚本的基本理论后,脚本越写越复杂,犯点错误在所难免。当遇到问题的时候不用怕,使用这接下来要讲的调试必杀技帮你找到并解决问题!
1.1.1 空变量问题
举个简单的例子,见代码6:
代码6:
#!/bin/sh
num=1
if [ $num= "1" ]; then
echo "Number is 1"
else
echo "Number is not 1"
fi
这是一个正确的脚本。如果我们把第3行从“num=1”变成“num=”,看看会发生什么?运行改后的脚本,你会得到以下结果:
./tt: line 5: [: =: unary operatorexpected
Number is not 1
你发现了错误信息提示是“line 5: [: =: unary operator expected”。明明我改了第3行,但是为什么提示错误发生在第5行,与“[”这个有关呢?莫着急,我来给你解释一下。
“num=” 语法上是没有错误的。你可以赋给变量为空。这个问题与shell替换文本有关。在第5行,当shell看到“num”变量,就替换它了。如果“num=1”就变成:
if [ 1 = "1" ]; then
可是当“num=”的时候,shell替换就变成:
if [ = "1" ]; then
你能说这不是错误吗?“=”是一个二进制运算符,左右两边都应该有东西。Shell试图告诉我们,只存在一个东西,那就要用unary 运算符(例如“!”),这种运算符支持单项。
为了改正这个错误,修改第5行,把它变成:
if [ "$num" ="1" ]; then
这样shell的替换就会变成:
if [ "" = "1"]; then
这就正确地表达了我们的意图。这个小例子告诉我们要注意当变量为空的情况。
1.1.2 缺引号问题
在代码6中我们去掉第6行的引号,
echo "Number equals 1
这次我们又得到什么了?
./tt: line 6: unexpected EOF whilelooking for matching `"'
./tt: line 8: syntax error:unexpected end of file
在某一行的错误会导致在脚本后面几行发生错误。Shell不断在寻找字符串结束的引号,到文件结束时也没有找全。在很长的脚本文件中遇到这种错误有时候会非常郁闷。所以在你加少量新代码的时候就开始测试,一点一点加,一点一点测,省得到最后再找问题,麻烦就大了。如果在编辑器中采用语法高亮的功能,这种类似缺匹配语法符号的问题就比较容易发现了。
1.1.3 隔离问题
大千世界无奇不有,错误也千奇百怪,找到bug原因有时候可能很困难,下面给你支几招。
注释掉一块代码看看出现的问题“跑”了没有。例如上缺引号的问题,如果把else那部分代码注释掉了,
#else
# echo"Number does not equal 1"
运行后问题还在。我们就可以肯定问题不在else这段代码中,尽管错误提示在这行。
1.1.4 echo-普通中见“伟大”
使用echo命令放在你心存疑虑的地方,是个不错的方法。我们在开发过程中经常发现bug不在我们第一次感觉它在的地方。为了解决这个问题,在你调试程序过程中,可以使用echo命令去证实你的猜想。你可以插入两种信息。
第一种就是在程序的某个点插入一句描述,看看程序是否像我们想象得那样到此一游过。
第二种就是显示计算或测试的变量值。你会发现某段程序失败,经常是因为我们开始假想它是正确的,实际上不正确,导致程序后来失败。
1.1.5 -x跟踪问题本领高
加入“-x”在你运行的脚本第一行:
#!/bin/sh -x
我们再来运行脚本上面的脚本,shell会会打印出每个命令执行的结果。这个技术叫跟踪。这个简单的例子在-x后所得结果如下:
+ num=1
+ '[' 1 = 1 ']'
+ echo 'Number is 1'
Number is 1
你也可以不用在脚本中加入-x,而是用sh -x scriptname执行命令,这两者是等价的。另外还有几种方法运行脚本,如:sh -n scriptname不会运行脚本, 只会检查脚本的语法错误;sh -v scriptname将会在运行脚本之前, 打印出每一个命令。-n和-v可以同时使用,做详细的语法检查。
1.1.6 assert
许多软件在编程思路上借鉴C语言,“assert”(断言)函数就是其中之一。Shell使用“assert”函数在临界点上测试变量或条件。我们认为在运行时该条件应该为真,当遇到假的情况,就不能继续执行脚本,而是立刻对相关错误进行处理。
例如:
assert"$condition" $LINENO
# 脚本以下的代码只有当"assert"成功时才会继续执行。其中$LINENO是用来显示行号的。
1.1.7 caller
caller[framenumber]是内建命令。在函数中里使用caller命令用于在标准输出中打印这个函数的相关信息。如果没有给framenumber或framenumber为0意味着打印最顶层执行的frame信息,此信息包括行号,谁调用了这个函数和文件名。请看代码7这个trycaller脚本:
代码7:
#!/bin/sh
function1()
{
caller 0 # 告诉我相关信息
}
function1
caller 0 #不在函数中的caller不起作用。
执行结果:
bogon:~ #./trycaller
6 main./trycaller
在上面例子中虽然有两个caller,但却只打印了一行信息,是因为caller放在函数中才能起作用,在脚本主体中不起作用。
1.1.8 trap
trap命令用于指定在接收到信号后将要采取的行动。语法是一般是这样的:
trap[COMMANDS] [SIGNALS]
SIGNALS为带有SIG前缀的信号标识,或者干脆就是数字也行。具体这个信号都有啥,本书的第二章就介绍过了。如果你懒得往回翻,可以使用命令“man 7signal”查看。
为了便于理解,我们来看一个实际的例子。代码8这个脚本捕捉“Ctrl+c”组合键发出的信号SIGINT后输出临别赠言,并删除临时文件:
代码8:
#!/bin/sh
control_c()
# 如果用户键入control-c就执行
{
echo -en "\n*** 亲,我要走了。祝幸福!***\n"
rm -f /tmp/tempfile
exit $?
}
# 设置control-c键盘中断
trapcontrol_c SIGINT
# main 循环
whiletrue; do read x; done
trap命令还有两个特别的“信号”,分别是DEBUG和ERR。它们的特别之处就在于其根本就是不是信号,而是trap的模式开关。如果指定为DEBUG模式,那么在每执行一个命令后都会执行COMMANDS;而如果指定为ERR模式,那么只要有命令以非零状态退出就会执行COMMANDS。代码9给出了DEBUG模式的例子,至于ERR模式的例子就当是作业吧!
代码9:
#!/bin/bash
#在每个命令行显示变量$variable的值.
trap'echo "VARIABLE-TRACE> \$variable = \"$variable\""'DEBUG
variable=29
echo"Just initialized \"\$variable\" to $variable."
let"variable *= 3"
echo"Just multiplied \"\$variable\" by 3."
# $? 指的是前一个命令的返回码
exit $?
执行上面的脚本trytrap后,每个命令行上都会显示$variable的值帮助我们跟踪查看。
bogon:~ #./trytrap
VARIABLE-TRACE>$variable = ""
VARIABLE-TRACE>$variable = "29"
Justinitialized "$variable" to 29.
VARIABLE-TRACE>$variable = "29"
VARIABLE-TRACE>$variable = "87"
Justmultiplied "$variable" by 3.
VARIABLE-TRACE>$variable = "87"