首页 > 技术文章 > (029) Linux之shell故障诊断

jplatformx 2015-03-19 18:43 原文

十年运维系列之基础篇 - Linux

作者:曾林 

联系:1494445739@qq.com

网站:www.jplatformx.com

版权:文章未经同意请勿转载


一、引言

  随着脚本的复杂度越来越高,当脚本出现错误或者执行情况和预期不同的时候,就需要看看是哪里出现了问题。本章将讲解一些脚本中常见的错误类型以及几种用于追踪和解除错误的有用技巧。

 

二、语法错误

  语法错误是一种常见的错误类型,其中就包括了shell语句中一些元素的拼写错误。在大多数情况下,shell会拒绝执行含有此种类型错误的脚本。

  在接下来的讨论过程中,我们将使用以下脚本(foo.sh)来演示常见的错误类型。具体如下代码:

#!/bin/bash

# foo: script to demonstrate common errors

number=1

if [[ $number == 1 ]];then
    echo "number is equal to 1." 
else
    echo "number is not equal 1." 
fi

exit

  此脚本经过运行后并没有错误,而且可以正常运行。运行结果如下图所示:

  

1. 引号缺失

  现在修改上述脚本,删除第一个echo命令后实参后的双引号。具体如下图:

#!/bin/bash

# foo: script to demonstrate common errors

number=1

if [[ $number == 1 ]];then
    echo "number is equal to 1.
else
    echo "number is not equal 1." 
fi

exit

  运行后的结果如下图所示:

  此删除动作使脚本产生了两个错误。有趣的是,错误报告指出的行并不是之前所删除双引号所在的行,而是之后的代码。可以看到,当系统读取到所删除双引号的位置之后,bash将继续向下寻找与前双引号对应的引号,这样的行为会一直延续到bash找到目标,也就是在第二个echo命令后的第一个引号处。然后bash就陷入了混乱之中,即if命令的语法结构被破坏了,fi语句现在处于了引号标识(但是又只有一边存在引号)的字符串中。

  这种类型的错误在长脚本中很难发现,而使用带有语法结构突出显示功能的编辑器能够帮助找到这类错误。如果系统配备的是完整版的vim,可使用以下命令启用vim的语法结构突出显示功能。语法如下:

  :syntax on

 

2. 符号缺失

  另一种常见的错误是如if或while这样的复合命令结构不完整。比如我们现在就删除上面脚本中的if命令中的test部分后的分号,看看会发生什么情况。代码如下:

#!/bin/bash

# foo: script to demonstrate common errors

number=1

if [[ $number == 1 ]] then
    echo "number is equal to 1." 
else
    echo "number is not equal 1." 
fi

exit

  执行结果如下图所示:

 

3. 非预期的展开

  有一些脚本错误是间歇出现的。脚本有时候会运行正常,有时候又会因为展开的结果而出错。现在将缺失的分号补回并改变number的值为空,即可以演示这类错误。

#!/bin/bash

# foo.sh: script to demonstrate expand error

number=

if (( $number == 1 )); then
    echo "number is equal to 1"
else
    echo "number is not equal to 1"
fi

exit

  运行修改过的脚本得到的输出结果如下所示:

  也就是一条看起来很深奥的报错信息,外加上脚本第二条echo命令的输出结果。造成问题的原因是test命令中number变量的展开。当命令

[ $number = 1 ]

  expansion情形为number变量为空,就造成了这样的情况。

[ = 1 ]

  等式无效,也就产生了错误。“=”是一个二元操作符(要求符号两边都有值),但是本例中缺少了一个值,所以test命令要求程序改用一元操作符(如-z)。接下来,因为test不成立,if命令接收到了一个非0的退出码,从而执行了第二个echo命令。

  用户可以通过在test命令中使用双引号引用第一个参数方式更正这个错误。

[ "$number" = 1 ]

  现在展开命令,结果是这样的。

[ "" = 1 ]

  "="的前后有了正确数量的参数。除了空字符串需要引用之外,类似包含空格的文件名这样的多字符的字符串也要前后加以引号。

 

三、逻辑错误

  与语法错误不同的是,逻辑错误不会阻碍脚本的运行。因为脚本包含的逻辑问题,脚本尽管可以运行但是不能产生理想的结果。可能发生的逻辑错误有非常多种,但是以下几种逻辑错误是脚本中最常见的。

  • 条件表达错误。编写代码中,很容易写出不正确的if、then和else语句,造成错误的逻辑,例如相反的或者不完整的逻辑表达
  • “从1开始”错误。在使用计数器的循环中,可能会忽略程序需要从0而不是1开始计数才能在正确的点结束循环。这种错误会导致循环次数过多,超过了预期的终点,或循环次数过少,缺少了最后一次的迭代过程。
  • 非预期的情形。大多数此种错误来源于程序运行过程遇到了编写程序人员没有预期到的数据或环境。非预期展开也包括在内,如一个包括了空格的文件名应该作为单个文件名存在,却扩展成为了多个命令的实参。

 

  1. 防御编程

  在编程中核实各项假设是很重要的事情,这意味着我们需要对程序的退出状态以及脚本所用命令进行仔细评估。这里有一个例子,一个管理员写了如下的脚本,这个脚本只包含如下两行代码:

cd $dir_name
rm *

  只要名为dir_name的目录存在,以上两行代码没什么本质性的错误。但是如果目标目录不存在怎么办呢?此情形下cd跳转命令失败,脚本继续执行以下的代码,就会删除当前目录下的所有文件。这应该完全就不是管理员想要的结果!出于这样的设计,这位管理员不幸地删除了服务器的一个重要的部分。

  现在就看看改进此设计的一些方法。第一种可能的方法是使执行rm命令以cd的成功为前提。

cd $dir_name && rm *

  这样以来,如果cd命令跳转失败,rm命令就不会执行。情况有所改善,但是仍然存在$dir_name为空的情况,一旦dir_name为空这样的情况发生,用户的主目录文件就会被删除。因此检查dir_name是否确实包含有效的目录名就可以避免此情况。代码改进如下:

[ -d "$dir_name" ] && cd "$dir_name" && rm *

 

四、测试 

  对任何软件的开发过程来说,包括脚本的开发过程,测试都是一个非常重要的步骤。在开源世界中有这么一种说法——尽早发布,经常发布。这种说法映射出了测试的重要性。通过尽早发布和经常发布新版本,软件就能够得到更多用户的使用和测试。经验表明如果能尽早在开发周期中发现bug,那么剩余的bug会更加容易发现,修补bug的代价也会更小。

 

五、调试

  如果测试可以揭露出脚本存在问题,那么下一步就是调试。问题通常意味着,在某些情况下,脚本的运行结果和预期的效果不同。如果是这样的话,就需要仔细查明脚本实际上是怎样运作的以及为什么会出现这样的情况。寻找bug有时会需要很多的侦查工作。

  设计优良的脚本本身能够提供一些帮助,防御性脚本遇到异常时会向用户反馈有用的信息。但是,解决预料之外的奇怪问题就需要介入其他的测试技术。

 

  1. 找到问题域

  在一些脚本中,尤其是长脚本中,将问题相关的脚本域隔离出来是很有必要的。隔离的部分并不一定是实际问题所在之处,但是通常能提供通往实际原因的线索。其中一种能够用于隔离代码片段的方法是“注释”掉脚本的一部分。

 

      2.  追踪

  追踪是一种用于查看程序实际运行流程的技术。

  一种追踪的技术是通过在脚本中添加通知信息的方式来展示程序执行之处。我们可以在代码片段中添加一些信息。如下代码:

 1 #!/bin/bash
 2 
 3 # foo.sh - trace program flow
 4 
 5 echo "preparing to delete files" >&2 
 6 
 7 if [[ -d "$dir_name" ]]; then
 8     if cd "$dir_name"; then
 9 echo "deleting files" >&2 
10         rm *
11     else
12         echo "cannot cd to '$dir_name'" >&2 
13     fi  
14 else
15     echo "no such directory: '$dir_name'" >&2
16     exit 1
17 fi
18 
19 echo "file deletion complete" >&2 
20 
21 exit

  我们将这些信息发送给标准错误,从而与一般的程序输出区分开来。这些信息所在的行并没有缩进,使这些代码在需要删除的时候易于查找。

  bash也提供了一种追踪的方法,即直接使用-x选项或set命令加-x选项。例如我们可以在shell脚本的第一行命令的末尾加上“-x”选项即可激活对整个脚本追踪活动。如下代码所示:

#!/bin/bash -x

 

# foo.sh - trace program flow

 

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

    fi  

else

    echo "no such directory: '$dir_name'" >&2

    exit 1

fi

 

echo "file deletion complete" >&2

 

exit

  脚本执行的结果如下图所示:

  激活追踪之后,我们就可以看到变量展开的执行情况。行开端的加号表示此行是系统的追踪信息,以区别于一般的输出。加号是追踪信息默认特征,是由shell变量PS4(提示字符串4)设定的,用户可以修改变量值使追踪活动的提示符提供更多的帮助信息。

  要对脚本选定的一部分而不是整个脚本执行追踪,可以使用set命令加-x选项。具体如下代码所示:

#!/bin/bash 

# foo.sh - trace program flow

echo "preparing to delete files" >&2

set -x  # turn on tracing
if [[ -d "$dir_name" ]]; then if cd "$dir_name"; then echo "deleting files" >&2 rm * else echo "cannot cd to '$dir_name'" >&2 fi else echo "no such directory: '$dir_name'" >&2 exit 1 fi echo "file deletion complete" >&2 exit set +x # turn off tracing

  在这里使用set命令加-x激活追踪,set命令加+x解除追踪。这项技术可以用来检验一个问题脚本的多个部分。

 

六、本章结尾语

  本章讨论了脚本开发过程中可能出现的几种问题。当然,没有涉及到的问题还有很多。本章讲述的debug方法已经能够帮助程序员找到大多数常见的bug。调试是一门在实践中成长的艺术,它既包括避免bug(在开发过程中不停测试),也包括找到bug(有效地利用追踪技术)。

 

 

 

 

  

 

推荐阅读