shell排错
常见语法错误
=当做==
#!/bin/bash
number=1
if [ $number = 1 ]; thenecho "Number is equal to 1."
elseecho "Number is not equal to 1."
fi
echo字符表达式丢失引号
#!/bin/bash
number=1
if [ $number == 1 ]; thenecho "Number is equal to 1.
elseecho "Number is not equal to 1."
fi
丢失或意外的标记
删除 if 命令中测试之后的分号
#!/bin/bash
# trouble: script to demonstrate common errors
number=1
if [ $number = 1 ] thenecho "Number is equal to 1."
elseecho "Number is not equal to 1."
fi
预料不到的展开
有时候这个脚本执行正常,其它时间会失败, 这是因为展开结果造成的。把 number 的数值更改为一个空变量
#!/bin/bash
# trouble: script to demonstrate common errors
number=
if [ $number = 1 ]; thenecho "Number is equal to 1."
elseecho "Number is not equal to 1."
fi
经过展开之后,number 变为空值,结果就是这样:
[ = 1 ]
通过为 test 命令中的第一个参数添加双引号,可以更正这个问题:
[ "$number" = 1 ]
其得到了正确的参数个数。除了代表空字符串之外,引号应该被用于这样的场合,一个要展开 成多单词字符串的数值,及其包含嵌入式空格的文件名。
[ "" = 1 ]
逻辑错误
- 不正确的条件表达式。很容易编写一个错误的 if/then/else 语句,并且执行错误的逻辑。
- “超出一个值”错误。当编写带有计数器的循环语句的时候,为了计数在恰当的点结束,循环语句可能要求从 0 开始计数,而不是从 1 开始,这有可能会被忽视。
- 意外情况。比如说一个包含嵌入式空格的文件名展开成多个命令参数而不是单个的文件名。
防错编程
当编程的时候,验证假设非常重要。
错
cd $dir_name
rm *
对
cd $dir_name && rm *
但是仍然有可能未设置变量 dir_name 或其变量值为空,从而导致删除了用户家目录下面的所有文件。
[[ -d $dir_name ]] && cd $dir_name && rm *
if [[ -d $dir_name ]]; thenif cd $dir_name; thenrm *elseecho "cannot cd to '$dir_name'" >&2exit 1fi
elseecho "no such directory: '$dir_name'" >&2exit 1
fi
这里,我们检验了两种情况,一个名字,看看它是否为一个真正存在的目录,另一个是 cd 命令是否执行成功。 如果任一种情况失败,就会发送一个错误说明信息到标准错误,然后脚本终止执行,并用退出状态 1 表明脚本执行失败。
验证输入
一个程序可以接受输入数据,那么这个程序必须能够应对它所接受的任意数据。
[[ $REPLY =~ ^[0-3]$ ]]
这条测试语句非常明确。只有当用户输入是一个位于 0 到 3 范围内(包括 0 和 3)的数字的时候, 这条语句才返回一个 0 退出状态。而其它任何输入概不接受。
测试
在各类软件开发中(包括脚本),测试是一个重要的环节。在开源世界中有一句谚语,“早发布,常发布”,这句谚语就反映出这个事实(测试的重要性)。 通过提早和经常发布,软件能够得到更多曝光去使用和测试。经验表明如果在开发周期的早期发现 bug,那么这些 bug 就越容易定位,而且越能低成本 的修复。
if [[ -d $dir_name ]]; thenif cd $dir_name; thenecho rm * # TESTINGelseecho "cannot cd to '$dir_name'" >&2exit 1fi
elseecho "no such directory: '$dir_name'" >&2exit 1
fi
exit # TESTING
因为在满足出错条件的情况下代码可以打印出有用信息,所以我们没有必要再添加任何额外信息了。 最重要的改动是仅在 rm 命令之前放置了一个 echo 命令, 为的是把 rm 命令及其展开的参数列表打印出来,而不是执行实际的 rm 命令语句。这个改动可以安全的执行代码。 在这段代码的末尾,我们放置了一个 exit 命令来结束测试,从而防止执行脚本其它部分的代码。 这个需求会因脚本的设计不同而变化。
测试案例
可以通过谨慎地选择输入数据或者运行边缘案例和极端案例来完成。 在我们的代码片段中(是非常简单的代码),我们想要知道在下面的三种具体情况下这段代码是怎样执行的:
- dir_name 包含一个已经存在的目录的名字
- dir_name 包含一个不存在的目录的名字
- dir_name 为空
通过执行以上每一个测试条件,就达到了一个良好的测试覆盖率。
调试
找到问题区域
if [[ -d $dir_name ]]; thenif cd $dir_name; thenrm *elseecho "cannot cd to '$dir_name'" >&2exit 1fi
# else
# echo "no such directory: '$dir_name'" >&2
# exit 1
fi
通过给脚本中的一个逻辑区块内的每条语句的开头添加一个注释符号,我们就阻止了这部分代码的执行。然后可以再次执行测试, 来看看清除的代码是否影响了错误的行为。
追踪
追踪(tracing)的技术。
一种追踪方法涉及到在脚本中添加可以显示程序执行位置的提示性信息。
echo "preparing to delete files" >&2
if [[ -d $dir_name ]]; thenif cd $dir_name; then
echo "deleting files" >&2rm *elseecho "cannot cd to '$dir_name'" >&2exit 1fi
elseecho "no such directory: '$dir_name'" >&2exit 1
fi
echo "file deletion complete" >&2
bash 还提供了一种名为追踪的方法,这种方法可通过 -x 选项和 set 命令加上 -x 选项两种途径实现。
#!/bin/bash -x
number=1
if [ $number = 1 ]; thenecho "Number is equal to 1."
elseecho "Number is not equal to 1."
fi
$ trouble
+ number=1
+ '[' 1 = 1 ']'
+ echo 'Number is equal to 1.'
Number is equal to 1.
追踪生效后,我们看到脚本命令展开后才执行。行首的加号表明追踪的迹象,使其与常规输出结果区分开来。 加号是追踪输出的默认字符。它包含在 PS4(提示符4)shell 变量中。可以调整这个变量值让提示信息更有意义。 这里,我们修改该变量的内容,让其包含脚本中追踪执行到的当前行的行号。注意这里必须使用单引号是为了防止变量展开,直到提示符真正使用的时候,就不需要了。
$ export PS4='$LINENO + '
$ trouble
5 + number=1
7 + '[' 1 = 1 ']'
8 + echo 'Number is equal to 1.'
Number is equal to 1.
可以使用 set 命令加上 -x 选项来启动追踪,+x 选项关闭追踪。这种技术可以用来检查一个有错误的脚本的多个部分。
#!/bin/bash
number=1
set -x # Turn on tracing
if [ $number = 1 ]; thenecho "Number is equal to 1."
elseecho "Number is not equal to 1."
fi
set +x # Turn off tracing
执行时检查数值
伴随着追踪,在脚本执行的时候显示变量的内容,以此知道脚本内部的工作状态,往往是很用的。
#!/bin/bash
number=1
echo "number=$number" # DEBUG
set -x # Turn on tracing
if [ $number = 1 ]; thenecho "Number is equal to 1."
elseecho "Number is not equal to 1."
fi
set +x # Turn off tracing