一类常见的错误和语法有关,即语法错误。语法错误包括输错了某些Shell语法元素。如果碰到此类错误,Shell会停止执行脚本。
[sysadmin@ansible bin]$ cat error
#!/bin/bash
#演示语法错误
number=1
if [ $number = 1 ]; then
echo "Number is equal to 1.
else
echo "Number is not equal to 1."
fi
[sysadmin@ansible bin]$ error
/home/sysadmin/bin/error: line 9: unexpected EOF while looking for matching `"'
/home/sysadmin/bin/error: line 11: syntax error: unexpected end of file
值得注意的是,错误信息中所报告的行号并非缺少引号的第7行位置,而是第9行。因为系统把第9行的第一个双引号当作是第7行引号的闭合了。而第9行第2个引号则因为未闭合,而报错。
另一种常见错误是没有把符合或命令(如if或while)写完整。让我们来看一看如果去掉if命令中的test命令之后的分号会出现什么情况:
[sysadmin@ansible bin]$ cat error
#!/bin/bash
#演示语法错误
number=1
if [ $number = 1 ] then
echo "Number is equal to 1."
else
echo "Number is not equal to 1."
fi
[sysadmin@ansible bin]$ error
/home/sysadmin/bin/error: line 8: syntax error near unexpected value `else'
/home/sysadmin/bin/error: line 8: `else'
本例是缺少了if命令之后的分号,但错误信息缺指向了实际问题之后的一个错误。
脚本可能会出现间歇性地出现错误,有时候脚本执行正常。有时候又会因为扩展结果而出错。将number的值修改为空,再来演示一下。
[sysadmin@ansible bin]$ cat error
#!/bin/bash
#演示语法错误
number=
if [ $number = 1 ]; then
echo "Number is equal to 1."
else
echo "Number is not equal to 1."
fi
[sysadmin@ansible bin]$ error
/home/sysadmin/bin/error: line 6: [: =: unary operator expected
Number is not equal to 1.
问题在于test命令内的number变量的扩展,当执行下列命令时[ $number = 1 ],$number经过扩展后,结果为空,test命令变成了
[ = 1 ]
这种形式显然是不合法的,因而产生了错误。=是一个二元操作符(要求操作符两侧都要有值),但现在少了一第一个值,所以test命令只能寄望于一元操作符(例如,-z)。由于test执行失败,if命令得到的非0退出状态,因此执行了第二个echo命令。给test命令的第一个参数加上引号就能解决这个问题.
[ "$number" = 1 ]
这样一来,参数数量就没错了。除了空串,双引号还可用于某个值会被扩展成多单词字符串的情况,因为文件名中是可以包含空格的。
始终坚持将变量和命令替换放入双引号中,除非需要单词分割。把这句话作为一条规则记住。
和语法错误不同,逻辑错误不会妨碍脚本执行,脚本照样可以执行,但因为逻辑有问题,无法产生理想的结果。常见的包括以下几种:
编程时的各种假设很重要,意味着要仔细评估程序的退出状态以及脚本中用到的命令。下面举一个例子
cd $dir_name
rm *
看上述代码,只要dir_name变量的目录存在,以上两行代码就没有什么本质性的错误。但是,如果目录不存在呢?这种情形下,cd命令执行失败,脚本接着执行下一行,删除当前工作目录中的所有文件。这可完全不是期望的结果。由于设计的问题,销毁了服务器中一部分重要文件。下面看几个改进措施
cd "$dir_name" && rm *
按照上述方法,如果cd命令执行失败,rm命令并不会执行。脚本有所改进,但存在变量dir_name不存在或为空的可能性,这会导致用户主目录内的文件全部被删除。可以通过检查dir_name是否存在来解决这个问题。
[[ -d "$dir_name" ]] && cd "$dir_name" && rm *
通常要加入终止脚本的逻辑,并在发生上述情况时报告错误:
#删除目录$dir_name中的文件
if [[ ! -d "$dir_name" ]]; then
echo "No such directory: '$dir_name'" >&2
exit 1
fi
if ! cd "$dir_name" ; then
echo "Cannot cd to '$dir_name'" >&2
exit 1
fi
if ! rm *; then
echo "File deletion failed.Check results" >&2
exit 1
fi
事实上,Linux中只有两个字符不能出现在文件名中,一个是/,因为该字符用于分割路径名中的各个部分;另一个是空字符,该字符用于在内部标示字符串结束。除此之外的所有字符都是合法的,其中包括空格符、制表符、换行符、前导连字符,回车符等。
尤其是前导连字符,例如,把文件命名为-rf完全没有任何问题。想想如果将该文件名作为参数传给rm会有什么后果。
为了防范这个问题,把文件检测脚本中的rm命令由:
rm *
修改为
rm ./*
这就避免了以连字符开头的文件名被误解为命令选项。作为通用规则,始终坚持在通配符(如和?)之前加上./,以免命令误解,例如.pdf和???.mp3
可移植的文件名,有一个叫做POSIX可移植文件名字符集的标准,最大程度上提高文件名跨系统使用的可能。标准非常建单,它允许的字符仅包括大写字母A-Z,小写字母a-z,数字0-9,点号,连字符-,下划线。此外,还建议文件名不要以连字符开头。
一个良好的编程习惯,如果程序需要接受输入,那么必须能够应对所有的输入内容。通常意味着一定要核实输入,保证只对有效输入做进一步处理。比如前面在讲read命令时,见过类似的例子。
[[ $REPLY =~ ^[0-3]$ ]]
测试非常具体,如果用户输入的字符串是在0~3范围内的数字,则返回退出状态0值,其他输入概不接受。这种测试有时候写起来有难度,但要想得到高质量的脚本,努力是必不可少的。
如果脚本要作生产之用,也就是说,会重复用于重要任务或被多个用户使用,在开发的时候程序员可就要加倍小心了。
测试是包括脚本在内的所有软件开发中的重要步骤,下面介绍如何利用脏代码核实程序流程,在脚本开发的早期,脏代码是检查工作进展的重要技术手段,如下例:
if [[ -d $dir_name ]]; then
if cd $dir_name; then
echo rm * # 易测试性
else
echo "Cannot cd to '$dir_name'" >&2
exit 1
fi
else
echo "no such directory: '$dir_name'" >&2
exit 1
fi
exit
最重要的改动是在rm命令之前放置了echo命令,使rm命令及其扩展后的命令参数得以显示出来,而不是执行删除操作。在代码片段的末尾,添加了exit命令来结束测试,避免执行脚本的其他部分。
要想执行有效的测试,重要的是开发和应用高质量的测试用例。这要求仔细选择能反映出边角情况的输入数据和操作条件。需要测试以下3种特定条件下的执行情况。
有些脚本中,尤其是长脚本,有时候将其中与问题相关的部分隔离出来还是有必要的,这部分未必就是问题所在,但是往往能帮助我们发现实际原因。一种隔离代码的技术是“注释掉”一部分脚本。例如,我们可以修改文件删除代码片段,确定去掉的部分是否与错误有关。
if [[ -d $dir_name ]]; then
if cd $dir_name; then
echo rm * # 易测试性
else
echo "Cannot cd to '$dir_name'" >&2
exit 1
fi
#else
# echo "no such directory: '$dir_name'" >&2
# exit 1
fi
exit
通过将注释符放置在脚本逻辑片段内的各行之前,就可以阻止这部分代码被执行。再次执行测试,看一看去掉这部分代码对Bug有没有什么影响。
一种跟踪技术是通过在脚本中添加能够显示执行位置的提示信息
echo "preparing to delete files" >&2
if [[ -d $dir_name ]]; then
if cd $dir_name; then
echo "deleting files" >&2
rm *
else
echo "Cannot cd to '$dir_name'" >&2
exit 1
fi
else
echo "no such directory: '$dir_name'" >&2
exit 1
fi
echo "file deletion complete" >&2
我们将信息发送至标准错误,以便与正常输出结果区分开。另外,我们也没有对包含消息的代码进行缩进,这样在需要删除这些行的时候,更容易查找。
bash还通过-x选项和set命令的-x选项提供了另一种跟踪技术。在前文的trouble脚本的第一行加入-x选项,激活整个脚本的跟踪功能:
[sysadmin@ansible bin]$ cat trouble
#!/bin/bash -x
number=1
if [ $number = 1 ]; then
echo "Number is equal to 1."
else
echo "Number is not equal to 1."
fi
[sysadmin@ansible bin]$ trouble
+ number=1
+ '[' 1 = 1 ']'
+ echo 'Number is equal to 1.'
Number is equal to 1.
启用跟踪后,我们可以查看命令经过扩展之后的执行情况。行首的加号表示此行是跟踪显示的结果,以区别于一般的输出结果。加号是用于跟踪输出的默认字符,由Shell变量PS4设定的。修改PS4,加入当前所跟踪代码的行号。注意,单引号用于将扩展推迟到真正使用提示符的时候。
[sysadmin@ansible bin]$ export PS4='$LINENO + '
[sysadmin@ansible bin]$ trouble
3 + number=1
5 + '[' 1 = 1 ']'
6 + echo 'Number is equal to 1.'
Number is equal to 1.
如果只是想对部分脚本进行跟踪,可以使用带有-x选项的set命令:
#!/bin/bash -x
number=1
set -x #打开跟踪
if [ $number = 1 ]; then
echo "Number is equal to 1."
else
echo "Number is not equal to 1."
fi
set +x #关闭跟踪
在跟踪过程中,显示变量的值,以此了解脚本执行时的内部工作状态,往往能派上大用场,可以通过添加echo语句来实现。
#!/bin/bash -x
number=1
echo "number=$number" #调试
set -x #开启跟踪
if [ $number = 1 ]; then
echo "Number is equal to 1."
else
echo "Number is not equal to 1."
fi
set +x #关闭跟踪