Shell脚本故障诊断

1. 语法错误

一类常见的错误和语法有关,即语法错误。语法错误包括输错了某些Shell语法元素。如果碰到此类错误,Shell会停止执行脚本。

1.1 缺少引号

[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个引号则因为未闭合,而报错。

1.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命令之后的分号,但错误信息缺指向了实际问题之后的一个错误。

1.3 出乎意料的扩展

脚本可能会出现间歇性地出现错误,有时候脚本执行正常。有时候又会因为扩展结果而出错。将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 ]

这样一来,参数数量就没错了。除了空串,双引号还可用于某个值会被扩展成多单词字符串的情况,因为文件名中是可以包含空格的。

始终坚持将变量和命令替换放入双引号中,除非需要单词分割。把这句话作为一条规则记住。

1.4 逻辑错误

和语法错误不同,逻辑错误不会妨碍脚本执行,脚本照样可以执行,但因为逻辑有问题,无法产生理想的结果。常见的包括以下几种:

  • 条件表达错误 if/then/else表达式很容易写错,从而造成逻辑错误,错误表达的逻辑与正确表达的逻辑南辕北辙,或者压根没表达完整。
  • ”差一“错误,在使用计数器的循环中,可能会忽略循环计数需要从0而不是从1开始,才能在正确的点完成计数。这种错误要么导致循环超出终点(计数过多),要么导致少了最后一次迭代(循环提前结束)。
  • 非预期情况。大多数逻辑错误是由于程序遇到了程序员没预想到的数据或情况引起的。非预期扩展也包括在内,例如包含空格符的文件名被扩展成了多个命令参数,而非单个文件名。

1.5 防御式编程

编程时的各种假设很重要,意味着要仔细评估程序的退出状态以及脚本中用到的命令。下面举一个例子

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

1.6 小心文件名

事实上,Linux中只有两个字符不能出现在文件名中,一个是/,因为该字符用于分割路径名中的各个部分;另一个是空字符,该字符用于在内部标示字符串结束。除此之外的所有字符都是合法的,其中包括空格符、制表符、换行符、前导连字符,回车符等。
尤其是前导连字符,例如,把文件命名为-rf完全没有任何问题。想想如果将该文件名作为参数传给rm会有什么后果。
为了防范这个问题,把文件检测脚本中的rm命令由:

rm *

修改为

rm ./*

这就避免了以连字符开头的文件名被误解为命令选项。作为通用规则,始终坚持在通配符(如和?)之前加上./,以免命令误解,例如.pdf和???.mp3

可移植的文件名,有一个叫做POSIX可移植文件名字符集的标准,最大程度上提高文件名跨系统使用的可能。标准非常建单,它允许的字符仅包括大写字母A-Z,小写字母a-z,数字0-9,点号,连字符-,下划线。此外,还建议文件名不要以连字符开头。

1.7 核实输入

一个良好的编程习惯,如果程序需要接受输入,那么必须能够应对所有的输入内容。通常意味着一定要核实输入,保证只对有效输入做进一步处理。比如前面在讲read命令时,见过类似的例子。

[[ $REPLY =~ ^[0-3]$ ]]

测试非常具体,如果用户输入的字符串是在0~3范围内的数字,则返回退出状态0值,其他输入概不接受。这种测试有时候写起来有难度,但要想得到高质量的脚本,努力是必不可少的。

如果脚本要作生产之用,也就是说,会重复用于重要任务或被多个用户使用,在开发的时候程序员可就要加倍小心了。

1.8 测试

测试是包括脚本在内的所有软件开发中的重要步骤,下面介绍如何利用脏代码核实程序流程,在脚本开发的早期,脏代码是检查工作进展的重要技术手段,如下例:

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命令来结束测试,避免执行脚本的其他部分。

1.9 测试用例

要想执行有效的测试,重要的是开发和应用高质量的测试用例。这要求仔细选择能反映出边角情况的输入数据和操作条件。需要测试以下3种特定条件下的执行情况。

  • dir_name包含的是已存在的目录名
  • dir_name包含的是不存在的目录名
  • dir_name为空

2.调试

2.1 查找问题区域

有些脚本中,尤其是长脚本,有时候将其中与问题相关的部分隔离出来还是有必要的,这部分未必就是问题所在,但是往往能帮助我们发现实际原因。一种隔离代码的技术是“注释掉”一部分脚本。例如,我们可以修改文件删除代码片段,确定去掉的部分是否与错误有关。

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有没有什么影响。

2.2 跟踪

一种跟踪技术是通过在脚本中添加能够显示执行位置的提示信息

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 #关闭跟踪

2.3 在执行过程中检查值

在跟踪过程中,显示变量的值,以此了解脚本执行时的内部工作状态,往往能派上大用场,可以通过添加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 #关闭跟踪

猜你喜欢

转载自blog.csdn.net/weixin_43770382/article/details/128361275