跟其他高级语言一样,Bash/Shell脚本也可以进行调试(Debug),学会这些调试技巧对编写和完善脚本有不小的帮助。
在本文中,您将学习一些有用的Bash/Shell脚本调试方法:
- 如何使用传统技术
- 如何使用xtrace选项
- 如何使用其他Bash选项
- 如何使用trap
使用的软件要求和约定
类别 | 使用的要求,约定或软件版本 |
---|---|
系统 | 任何GNU /Linux发行版 |
软件 | GNU Bash |
其他 | N/A |
约定 | #-要求linux命令可以直接以root用户身份或通过使用root特权以root特权执行sudo 命令
$-要求linux命令以普通非特权用户身份执行 |
使用传统技术
即使代码错误很明显,调试代码也可能很棘手。程序员传统上利用诸如调试器和编辑器中的语法突出显示之类的工具来协助解决问题。编写Bash脚本没有什么不同。只需突出显示语法即可使您在编写代码时捕获错误。
一些编程语言附带了调试功能,例如gcc和gdb,它们使您可以逐步执行代码,设置断点,在执行时检查这些点处的所有内容的状态,等等。但是,Bash脚本通常不需要像这样的笨拙方法。 对于Shell脚本来说,仅对代码进行解释,而不是将其编译为二进制文件。
传统编程环境中使用了一些对复杂的Bash脚本有用的技术,例如使用断言。即在某个时间点明确声明条件或事物状态的方式,它们可以实现为一个简短的函数,该函数可以显示时间,行号等,或类似如下代码所示的东西:
$ echo "function_name(): value of \$var is ${var}"
如何使用Bash xtrace选项
在编写Shell脚本时,编程逻辑往往会更短,并且通常包含在单个文件中。因此,我们可以使用一些内置调试选项来查看出了什么问题。要提到的第一个选项可能也是最有用的是-xtrace
选项。可以通过使用以下命令调用Bash将其应用于脚本-x
开关。
$ bash -x <scriptname>
-x及与其
相反的-v
,-v显示每行之前的值,而不是之后的值。这两个选项可以组合使用,也就是同时使用-x
和-v,从而
看到变量替换发生前后的语句。
如何使用其他Bash选项
默认情况下,用于调试的Bash选项处于关闭状态,但是一旦使用set命令将其打开,它们将保持打开状态,直到显式关闭为止。如果您不确定启用了哪些选项,则可以检查$-
变量以查看所有变量的当前状态。
$ echo $-
himBHs
$ set -xv && echo $-
himvxBHs
我们可以使用另一个有用的开关来帮助我们找到引用的变量,而无需设置任何值。这是-u开关
,就像-x
和-v
也可以在命令行中使用它,如下面的示例所示:
我们错误地为名为“level”的变量赋予了7这个值,然后试图回显名为“score”的变量,该未定义变量仅导致屏幕完全不打印任何内容。设置-u
开关使我们可以查看特定的错误消息“score: unbound variable”,该错误消息确切指示出了问题所在。
我们可以在简短的Bash脚本中使用这些选项,以向我们提供调试信息,以识别那些不会触发Bash解释器反馈的问题。让我们来看几个例子。
#!/bin/bash
read -p "Path to be added: " $path
if [ "$path" = "/home/mike/bin" ]; then
echo $path >> $PATH
echo "new path: $PATH"
else
echo "did not modify PATH"
fi
在上面的示例中,我们正常运行addpath脚本,但它根本不会修改我们的PATH
。执行时没有给我们任何错误提示。使用-x选项再次运行它时清楚地表明,我们比较的左侧是一个空字符串。$path
是一个空字符串,因为我们在读取语句中不小心在”path”前面加了一个美元符号。
看下一个示例,我们也没有从解释器得到任何错误指示。每行仅打印一个值,而不是两个。这是不会导致脚本停止执行的错误。使用-u开光
,我们立即收到通知,我们的变量j
没有绑定到值。因此,当我们收到从Bash解释器的角度来看不会导致实际错误的错误时,这能帮我们节省时间。
#!/bin/bash
for i in 1 2 3
do
echo $i $j
done
当我们处理更长和更复杂的脚本时,设置-xv
选项,然后运行更复杂的脚本,除了会使生成的输出量增加一倍或两倍,而且需要为多个bash命令行修改代码增加该选项,从而增加混乱。
幸运的是,我们可以通过将它们放置在脚本中来更精确地使用这些选项。我们可以通过将选项添加到shebang行中来设置选项,而不是通过命令行显式调用Bash shell。
#!/bin/bash -x
这将为整个文件设置-x
选项(或者直到脚本执行期间未设置它为止),允许您通过键入文件名而不是将其作为参数传递给Bash命令来简单地运行脚本。但是,使用这种技术,长脚本或具有大量输出的脚本仍然内容多且混乱,因此,让我们看一下使用选项的更具体方法。
对于更具针对性的方法,请仅用可选项包围可疑代码块。此方法可以通过分别使用带有正负的set关键字来实现。
#!/bin/bash
read -p "Path to be added: " $path
set -xv
if [ "$path" = "/home/mike/bin" ]; then
echo $path >> $PATH
echo "new path: $PATH"
else
echo "did not modify PATH"
fi
set +xv
我们仅围绕我们怀疑的代码块以减少输出,从而使我们的任务在调试过程中更加容易查看。请注意,我们仅对包含if-then-else语句的代码块打开选项,然后在可疑块的末尾关闭选项。如果我们无法缩小可疑区域的范围,或者想要在脚本执行过程中的各个时间点评估变量的状态,则可以在一个脚本中多次打开和关闭这些选项。如果我们希望在脚本执行的其余部分继续执行该选项,则无需关闭该选项。
我们还应该提到,有第三方编写的调试器,这些调试器使我们可以逐行逐步执行代码。您可能想研究这些工具,但是大多数人发现它们实际上并不是必需的。
正如经验丰富的程序员所建议的那样,如果您的代码太复杂而无法使用这些选项隔离可疑块,那么真正的问题是应该重构代码。过于复杂的代码意味着可能很难检测到错误,并且维护费时费力。
关于Bash调试选项,最后要提到的一件事是,还存在文件globing选项:-f
。启用此选项后,设置它会关闭globing(扩展通配符以生成文件名)。
#!/bin/bash
echo "ignore fileglobbing option turned off"
ls *
echo "ignore file globbing option set"
set -f
ls *
set +f
如何使用trap帮助调试
如果脚本复杂,还有更多值得考虑的技术,包括使用前面提到的assert函数。另外一种方法是使用trap。 Shell脚本使我们可以捕获(trap)信号并在此时执行某些操作。
您可以在Bash脚本中使用的一个简单但有用的trap示例是捕获EXIT
。
#!/bin/bash
trap 'echo score is $score, status is $status' EXIT
if [ -z ]; then
status="default"
else
status=
fi
score=0
if [ ${USER} = 'superman' ]; then
score=99
elif [ $# -gt 1 ]; then
score=
fi
如您所见,仅将变量的当前值输出到屏幕对于显示逻辑失败的位置很有用。EXIT
信号显然不需要显式exit
产生的陈述;在这种情况下echo
到达脚本末尾时执行语句。
与Bash脚本一起使用的另一个有用的trap是DEBUG
。这是在每个语句之后发生的,因此可以用作蛮力方式在脚本执行的每个步骤中显示变量的值。
#!/bin/bash
trap 'echo "line ${LINENO}: score is $score"' DEBUG
score=0
if [ "${USER}" = "mike" ]; then
let "score += 1"
fi
let "score += 1"
if [ "" = "7" ]; then
score=7
fi
exit 0
结论
当您发现您的Bash脚本未按预期方式运行,并且难以确定问题的原因时,请考虑哪些信息将有助于您确定原因,然后使用最方便的调试工具来帮助您确定问题。