尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Shell脚本——故障诊断

Shell脚本——故障诊断 故障诊断1、语法错误1.1、缺少引号1.2、缺少词法单元1.3、出乎意料的扩展2、逻辑错误2.1、防御式编程2.2、小心文件名2.3、核实输入3、测试4、调试4.1、查找问题区域4.2、跟踪4.3、在执行过程中检查值1、语法错误一类常见错误和语法有关即语法错误。语法错误包括输错了某些Shell语法元素。如果碰到此类错误Shell会停止执行脚本。在后文中我们将使用下面的脚本来演示常见的错误类型#!/bin/bash# 错误演示常见错误的脚本number1if[$number1];thenechoNumber is equal to 1.elseechoNumber is not equal to 1.fi该脚本执行顺利[melinuxbox ~]$ trouble Number is equal to1.1.1、缺少引号让我们来编辑脚本找到第一个echo命令的参数将其结尾的引号去掉#!/bin/bash# 错误演示常见错误的脚本number1if[$number1];thenechoNumber is equal to 1. else echo Number is not equal to1.fi这会导致下列情况[melinuxbox ~]$ trouble /home/me/bin/trouble: line10: unexpected EOFwhilelookingformatching/home/me/bin/trouble: line13: syntax error: unexpected end offile脚本产生了两处错误。值得注意的是错误消息中所报告的行号并非缺少引号的位置而是脚本中相当靠后的地方。如果我们从缺少引号处接着往下看就知道是怎么回事了。Bash会继续寻找闭合引号不找到誓不罢休。它的确找到了就是紧挨着第二个echo命令的那个引号。然后Bash可就“摸不着头脑”了它发现接下来的if命令的语法不对原因在于fi语句此时出现在了一个被引用但未闭合的字符串内部。对于代码量较大的脚本这种错误相当难以查找。使用带有语法高亮特性的编辑器会有所帮助。在多数情况下这种编辑器会用与其他Shell语法元素明显有别的方式显示引用字符串。如果安装了完整版本的Vim可以通过下列命令启用语法高亮:syntax on1.2、缺少词法单元另一种常见错误是没有把符号或命令如if或while写完整。让我们来看一看如果去掉if命令中的test命令之后的分号会出现什么情况#!/bin/bash# 错误演示常见错误的脚本number1if[$number1]thenechoNumber is equal to 1.elseechoNumber is not equal to 1.fi结果如下[melinuxbox ~]$ trouble /home/me/bin/trouble: line9: syntax error near unexpected tokenelse/home/me/bin/trouble: line9:else错误消息这次指向的又是一个出现在实际问题之后的错误。这里的情况着实值得注意。回想一下if接受命令列表评估列表中最后一个命令的退出状态。在我们的程序中这个命令列表原本应该只包含单个命令[​test的同义词​​[命令之后是4个参数$number、1、、​]​。如果去掉了分号单词then也变成了参数这在语法上没毛病。后续的echo命令也是合法的。它会被解释成命令列表中的另一个命令if将评估其退出状态。接下来碰到的是else它出现在此处是不合适的因为Shell将其识别为保留字对Shell具有特殊含义的单词​而非命令名称这正是导致错误消息的原因。1.3、出乎意料的扩展脚本可能会间歇性地出现错误。有时候脚本执行正常有时候又会因为扩展结果而出错。补上之前去掉的分号将number的值修改为空现在让我们来演示一下这种现象#!/bin/bash# 错误演示常见错误的脚本numberif[$number1];thenechoNumber is equal to 1.elseechoNumber is not equal to 1.fi执行修改后的脚本产生下列执行结果[melinuxbox ~]$ trouble /home/me/bin/trouble: line7:[:: unary operator expected Number is not equal to1.我们得到了一条让人摸不着头脑的错误消息接着是第二个echo命令的输出结果。问题在于test命令内的number变量的扩展。当执行下列命令时[$number1]$number经过扩展后结果为空test命令就变成了[1]这种形式显然不合法因而产生了错误。是一个二元操作符要求操作符两侧都要有值​但现在少了第一个值所以test命令只能寄望于一元操作符例如-z​。由于test执行失败if命令得到的是非0退出状态因此执行了第二个echo命令。给test命令的第一个参数加上双引号就能解决这个问题[$number1]经过扩展后结果如下[1]这样一来参数数量就没错了。除了空串双引号还可用于某个值会被扩展成多单词字符串multiword string的情况因为文件名中是可以包含空格的。注意始终坚持将变量和命令替换放入双引号中除非需要单词分割。把这句话作为一条规则记住。2、逻辑错误和语法错误不同逻辑错误不会妨碍脚本执行。脚本照样可以执行但因为逻辑有问题无法产生理想的结果。可能出现的逻辑错误数不胜数不过常见的包括以下几种。条件表达错误。if/then/else表达式很容易写错从而造成逻辑错误错误表达的逻辑与正确表达的逻辑南辕北辙或者压根没表达完整。“差一”​off by one错误。在使用计数器的循环中可能会忽略循环计数需要从0而不是从1开始才能在正确的点完成计数。这种错误要么导致循环超出终点计数过多​要么导致少了最后一次迭代循环提前结束​。非预期情况。大多数逻辑错误是由于程序遇到了程序员没预想到的数据或情况引起的。非预期扩展也包括在内例如包含空格符的文件名被扩展成了多个命令参数而非单个文件名。2.1、防御式编程核实编程时的各种假设很重要这意味要仔细评估程序的退出状态以及脚本中用到的命令。来讲一个真实的故事吧。有位系统管理员写了一个脚本负责一台重要的服务器的维护任务。这个脚本包含了这样两行代码cd$dir_namerm*只要变量dir_name中包含的目录存在以上两行代码就没有什么本质性的错误。但是如果目录不存在呢在这种情形下cd命令执行失败脚本接着执行下一行删除当前工作目录中的所有文件。这可完全不是想要的结果由于这种设计这位系统管理员销毁了服务器中一部分重要文件。来看几个改进措施。首先将变量dir_name标注起来确保其只扩展成一个单词同时仅在cd命令执行成功时才执行rm命令。cd$dir_namerm*按照这种方法如果cd命令执行失败rm命令并不会执行。脚本有所改进但仍存在变量dir_name不存在或为空的可能性这会导致用户主目录内的文件全都被删除。可以通过检查dir_name是否包含已有目录的名称来解决这个问题。[[-d$dir_name]]cd$dir_namerm*通常要加入终止脚本的逻辑并在发生上述情况时报告错误# 删除目录$dir_name中的文件if[[!-d$dir_name]];thenechoNo such directory: $dir_name2exit1fiif!cd$dir_name;thenechoCannot cd to $dir_name2exit1fiif!rm*;thenechoFile deletion failed. Check results2exit1fi其中我们既检查名称对应的目录是否存在也检查cd命令是否执行成功。如果有任意一项失败就向标准错误发送描述性的错误消息并终止脚本返回指示故障的退出状态。2.2、小心文件名文件检测脚本还存在另一个更为晦涩但也非常危险的问题。在很多人看来UNIX以及类UNIX系统在文件名方面有一处严重的设计缺陷。那就是对文件名的组成过于宽容。事实上只有两个字符不能出现在文件名中。一个是/因为该字符用于分隔路径名中的各个部分另一个是空字符该字符用于在内部标示字符串结束。除此之外的所有字符都是合法的其中包括空格符、制表符、换行符、前导连字符leading hyphens​、回车符等。尤其是前导连字符。例如把文件命名为-rf完全没有任何问题。想想如果将该文件名作为参数传给rm会有什么后果。为了防范这个问题我们要把文件检测脚本中的rm命令由rm*修改成rm./*这就避免了以连字符开头的文件名被误解为命令选项。作为通用规则始终坚持在通配符如和​之前加上./以免命令误解例如.pdf和???.mp3。2.3、核实输入一个良好的编程习惯是如果程序需要接受输入那么必须能够应对所有的输入内容。这通常意味着一定要仔细核实输入保证只对有效输入做进一步处理。在第29章讲解read命令时我们曾经见到过类似的例子。脚本中包含了下列测试来核实菜单选项[[$REPLY~^[0-3]$]]测试非常具体。如果用户输入的字符串是在0~3范围内的数字则返回退出状态0值。其他输入概不接受。这种测试有时候写起来有难度但要想得到高质量的脚本努力是必不可少的。3、测试测试是包括脚本在内的所有软件开发中的重要步骤。开源世界有种说法早发布勤发布。按照这种做法软件就能够得到更多的使用和测试。经验表明在开发周期的早期阶段更容易找出Bug这时修复Bug的代价也更低。在前面中我们看到了如何使用桩代码核实程序流程。在脚本开发的最早阶段桩代码是检查工作进展的重要技术手段。回顾一下前文的文件删除问题看一看怎样增强代码的易测试性。因为原始代码的目的是删除文件所以直接进行测试比较危险。不过我们可以修改代码确保测试安全if[[-d$dir_name]];thenifcd$dir_name;thenechorm*# 易测试性elseechocannot cd to $dir_name2exit1fielseechono such directory: $dir_name2exit1fiexit# 易测试性因为错误分支已经输出了有帮助的消息我们就不需要再添加什么了。最重要的改动是在rm命令之前放置了echo命令使rm命令及其扩展后的命令参数得以显示出来而不是执行删除操作。在代码片段的末尾我们添加了exit命令来结束测试避免执行脚本的其他部分。是否需要这么做视脚本设计而定。我们还要添加一些注释标记出与测试相关的改动。测试结束后注释有助于查找和删除做出的改动。测试用例要想执行有效的测试重要的是开发和应用高质量的测试用例。这要求仔细选择能反映出边角情况edge andcorner case的输入数据和操作条件。对于我们的代码片段算不上复杂​需要知道其在以下3种特定条件下的执行情况。dir_name包含的是已存在的目录名。dir_name包含的是不存在的目录名。dir_name为空。通过测试每种条件可实现良好的测试覆盖面。就像设计一样测试也得花时间打磨。不是所有的脚本特性都得测试。确定哪些是重要的才是关键。因为有问题的 代码可能会造成毁灭性的后果所以不管是代码设计还是测试都需要仔细斟酌。4、调试如果测试发现脚本存在问题那么下一步就是调试。​“问题”​通常在某种程度上意味着脚本并没有按照程序员的预期执行。如果是这样的话就需要仔细查明脚本究竟做了什么及其原因。查找Bug有时需要大量时间来推敲。设计优良的脚本本身能够提供一些帮助。脚本应该采用防御式编程以检测异常同时向用户反馈有用的信息。但有时候碰到的问题特别奇怪完全出乎意料这时就得用到更多的技术。4.1、查找问题区域在有些脚本中尤其是那些长脚本有时候将其中与问题相关的部分隔离出来还是有必要的。这部分未必就是问题所在但是往往能帮助我们发现实际原因。一种隔离代码的技术是“注释掉”​commenting out一部分脚本。例如我们可以修改文件删除代码片段确定去掉的部分是否与错误有关if[[-d$dir_name]];thenifcd$dir_name;thenrm*elseechocannot cd to $dir_name2exit1fi# else# echo no such directory: $dir_name 2# exit 1fi通过将注释符放置在脚本逻辑片段内的各行之前就可以阻止这部分代码被执行。再次执行测试看一看去掉的这部分代码对Bug有没有什么影响。4.2、跟踪Bug还经常表现为脚本中异常的逻辑流程。也就是说部分脚本要么没执行要么以错误的顺序或在错误的时刻执行了。跟踪是一种用于查看程序实际执行流程的技术。一种跟踪技术是通过在脚本中添加能够显示执行位置的提示信息。我们可以把这些信息加入代码片段echopreparing to delete files2if[[-d$dir_name]];thenifcd$dir_name;thenechodeleting files2rm*elseechocannot cd to $dir_name2exit1fielseechono such directory: $dir_name2exit1fiechofile deletion complete2我们将信息发送至标准错误以便与正常输出结果区分开。另外我们也没有对包含消息的代码行进行缩进这样在需要删除这些行的时候更容易查找。该脚本执行时就可以看到已经执行了文件删除操作[melinuxbox ~]$ deletion-script preparing to delete files deleting filesfiledeletion complete[melinuxbox ~]$bash还通过-x选项和set命令的-x选项提供了另一种跟踪技术。我们在前文的trouble脚本的第一行加入-x选项激活整个脚本的跟踪功能#!/bin/bash -x# 错误演示常见错误的脚本number1if[$number1];thenechoNumber is equal to 1.elseechoNumber is not equal to 1.fi该脚本的执行结果如下[melinuxbox ~]$ trouble number1[11]echoNumber is equal to 1.Number is equal to1.启用跟踪后我们可以查看命令经过扩展之后的执行情况。行首的加号表示此行是跟踪显示的结果以区别于一般的输出结果。加号是用于跟踪输出的默认字符是由Shell变量PS4Prompt String 4设定的。可以改变PS4变量的值增强提示符的实用性。我们对变量PS4做出修改在其中加入当前所跟踪代码的行号。注意单引号用于将扩展推迟到真正使用提示符的时候[melinuxbox ~]$exportPS4$LINENO [melinuxbox ~]$ trouble5number17[11]8echoNumber is equal to 1.Number is equal to1.如果只是想对部分脚本进行跟踪可以使用带有-x选项的set命令#!/bin/bash# 错误演示常见错误的脚本number1set-x# 打开跟踪if[$number1];thenechoNumber is equal to 1.elseechoNumber is not equal to 1.fisetx# 关闭跟踪我们使用set命令的-x选项启用跟踪使用x选项停止跟踪。这项技术可用于检查问题脚本中的多个部分。4.3、在执行过程中检查值在跟踪过程中显示变量的值以此了解脚本执行时的内部工作状态往往能派上大用场。这可以通过添加echo语句来实现。#!/bin/bash# 错误演示常见错误的脚本number1echonumber$number# 调试set-x# 打开跟踪if[$number1];thenechoNumber is equal to 1.elseechoNumber is not equal to 1.fisetx# 关闭跟踪在这个简单的例子中我们只显示了变量number的值并使用注释标记出了额外添加的行以便于后续的识别和删除。这项技术在观察脚本中的循环和算术运算行为时尤其有用。
返回列表