如何调试Bash脚本?

173

有没有办法调试Bash脚本?

例如,打印一种执行日志,显示“调用第1行”、“调用第2行”等。


2
这里有一个类似的问题:http://serverfault.com/questions/16204/how-to-make-bash-scripts-print-out-every-command-before-it-executes - Dennis Williamson
这里可能是另一个重复的问题。 - Pablo Bianchi
这里有:Bash性能分析(四个答案)! - F. Hauri - Give Up GitHub
“最有效的调试工具仍然是谨慎的思考,再加上恰到好处的打印语句。” - Brian W. Kernighan, 1979 - undefined
12个回答

205
sh -x script [arg1 ...]
bash -x script [arg1 ...]

这些给出了正在执行的迹象。(请参见答案底部附近的“澄清”部分。)

有时,您需要在脚本内部控制调试。在这种情况下,正如Cheeto提醒我的那样,您可以使用:

set -x

使用此命令打开调试模式。你可以用以下命令关闭调试模式:

set +x

您可以通过分析当前标志 $- 来查找 x 的当前跟踪状态。

此外,shell 通常提供 '-n' 选项用于 'no execution','-v' 选项用于 'verbose' 模式;您可以将它们组合使用以查看 shell 是否认为它可以执行您的脚本 - 如果您在某个地方有一个不平衡的引号,则偶尔会有用。


有争议称Bash中的 '-x' 选项与其他 shell 不同(请参阅评论)。Bash 手册 中说:

  • -x

    打印简单命令、 for 命令、 case 命令、 select 命令和算术 for 命令及其参数或关联的字列表,在扩展之后并在执行之前。 PS4 变量的值被扩展,并且在该命令及其扩展参数之前打印生成的值。

这些看起来没有表明有任何不同的行为。我没有看到手册中其他相关的 '-x' 引用。它没有描述启动序列中的差异。

澄清: 在诸如典型的 Linux 系统之类的系统上,' /bin/sh '是指向 '/bin/bash '(或 Bash 可执行文件所在的位置)的符号链接,这两个命令行实现了使用执行跟踪运行脚本的等效效果。在其他系统上(例如 Solaris 和一些更现代的 Linux 变体),/bin/sh 不是 Bash,并且这两个命令行将给出(稍微)不同的结果。尤其是,'/bin/sh' 会对 Bash 中完全不认识的结构感到困惑。(在 Solaris 上,/bin/sh 是 Bourne shell;在现代 Linux 上,它有时是 Dash - 更小、更严格的 POSIX-only shell。)当像这样通过名称调用时,文件开头的 'shebang' 行('#!/bin/bash ' vs '#!/bin/sh ')对内容的解释方式没有影响。

Bash 手册中有一节关于 Bash POSIX 模式,与长期存在但错误的版本(请参见下面的评论)相反,���模式详细描述了 'Bash 作为 sh 调用' 和 'Bash 作为 bash 调用' 之间的区别。

在调试(Bash)shell 脚本时,使用带有 -x 选项的 shebang 行中命名的 shell 是明智而理性的 - 必要甚至 - 否则,在调试与运行脚本时,可能会获得不同的行为。


6
在启动时和运行时有所区别。这些差异在Bash发行版中都有详细记录。 - TheBonsai
4
这是一个Bash文档的链接:http://www.gnu.org/software/bash/manual/bashref.html#Bash-Startup-Files 如果使用sh作为Bash的名称来调用它,它将尽可能模仿历史版本sh的启动行为,并遵循posix标准。 - thethinman
7
使用PS4提示符提供更多有用信息,例如:export PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'。 (注:PS4是Bash shell中的一个环境变量,它可以控制在命令行中打印的调试信息的格式和内容。这个提示符会在每次执行命令时显示,并包含一些有用的调试信息,如文件名、行号和函数名。以上翻译旨在保留原意并简洁表述) - estani
1
在像典型的Linux系统这样的系统上,'/bin/sh'是指向'/bin/bash'的符号链接。注意:在Ubuntu上,/bin/sh是dash而不是bash。请参见https://wiki.ubuntu.com/DashAsBinSh - Tolli
1
@Tolli:所以Ubuntu不是一个典型的Linux系统,或者说2010年的典型系统在2014年已经不再典型了。或者两者都是... - Jonathan Leffler
显示剩余3条评论

28
我已经使用了以下方法来调试我的脚本。 set -e 如果任何外部程序返回非零退出状态,该脚本将立即停止。如果您的脚本尝试处理所有错误情况,并且无法处理某些情况,则此选项很有用。 set -x 如上所述,是所有调试方法中最有用的。 set -n 如果您想检查脚本是否存在语法错误,则可能也很有用。 strace 也很有用,可以查看正在发生什么。特别适用于您没有自己编写脚本的情况。

1
跟踪脚本(即跟踪执行脚本的shell)是一种奇怪的shell调试方法(但对于有限的问题集可能有效)。 - TheBonsai
1
我承认这很奇怪而且也非常冗长,但是如果你限制strace的输出为几个系统调用,它会变得有用。 - FD Gonthier
1
请注意,如果您还想查找由脚本启动的进程中的错误,则需要使用 strace -f(这会使其更加冗长,但如果将其限制为您感兴趣的系统调用,则仍然很有用)。 - Random832
set -e 是有争议的。 - Charles Duffy

13

Jonathan Leffler的回答是有效和有用的。

但是,我发现“标准”脚本调试方法效率低下、不直观且难以使用。对于习惯了复杂 GUI 调试器并能轻松解决简单问题(甚至是困难问题)的人来说,这些解决方案并不是很令人满意。

我使用 DDD 和 bashdb 的组合。前者执行后者,后者再执行您的脚本。这提供了一个多窗口 UI,可以在上下文中逐步查看代码并查看变量、堆栈等,而无需不断地努力维护头脑中的上下文或重新列出源代码。

DDD 和 BASHDB 中有关于设置的指导。


刚刚通过您的回答发现了ddd。在Ubuntu 12.04.3(64位)中,apt-sources版本无法使用。我不得不从源代码编译和安装才能开始调试我的bash脚本。这里的说明 - http://askubuntu.com/questions/156906/how-do-i-get-the-bash-debugger-to-work-with-ddd 很有帮助。 - chronodekar
是的,那是个问题。我之前使用一些脚本解决了这个问题——'dddbash' 安装/构建 DDD,如果有旧版本则移除,还会安装bashdb等等。(回答已更新此信息) - Stabledog

11

我发现了 shellcheck 工具,或许有些人会觉得它很有趣。

这里有一个小例子:

$ cat test.sh
ARRAY=("hello there" world)

for x in $ARRAY; do
  echo $x
done

$ shellcheck test.sh

In test.sh line 3:
for x in $ARRAY; do
         ^-- SC2128: Expanding an array without an index only gives the first element.

修复这个错误。首先尝试...

$ cat test.sh
ARRAY=("hello there" world)

for x in ${ARRAY[@]}; do
  echo $x
done

$ shellcheck test.sh

In test.sh line 3:
for x in ${ARRAY[@]}; do
         ^-- SC2068: Double quote array expansions, otherwise they're like $* and break on spaces.

让我们再试一次...

$ cat test.sh
ARRAY=("hello there" world)

for x in "${ARRAY[@]}"; do
  echo $x
done

$ shellcheck test.sh

现在找到了!

这只是一个小例子。


1
而且它是在线的 - Nick Westgate
1
幸运的是,这个工具已经发展到了能够找到剩余的错误的地步。 - tripleee

10

您也可以在脚本中写入"set -x"。


4
你可以输入“set +x”来关闭它。 - Jonathan Leffler

4
安装 Visual Studio Code,然后添加 Bash 调试扩展程序,就可以在可视化模式下进行调试。点击此处查看具体操作。

Enter image description here


这比要求Eclipse稍微不那么愚蠢一点。考虑到VS Code的普及程度,我猜这可能会对一些访问者有所帮助;但作为一个答案,它并没有特别启发人的地方。 - undefined

2
set +x = @ECHO OFF, set -x = @ECHO ON.

你可以在标准的 shebang 中添加 -xv 选项,示例如下:

你可以将 -xv 选项添加到标准的 shebang 中,如下所示:

#!/bin/bash -xv

-x:显示执行的命令及其参数。 -v:显示读取的shell输入行。


ltrace是另一个类似于strace的Linux实用程序。然而,ltrace列出了在可执行文件或运行进程中调用的所有库调用。它的名称本身就来自库调用跟踪。

例如:

ltrace ./executable <parameters>
ltrace -p <PID>

Source


2

3
这是一个边缘性的 仅链接答案(也可以参见这里)。你应该扩展你的回答,至少包括实现你所建议的最低限度的信息,并仅将链接用作参考。基本上,Stack Overflow(以及所有 Stack Exchange 网站)上的帖子必须是自包含的。这意味着需要在您的回答中提供足够的信息,以便读者不必离开网站就能理解。目前,这个答案还没有达到这个要求。 - Makyen
这是我在查看了许多答案后找到的第一个真正展示真正调试可能性的答案。标准答案“set +x”完美地匹配了自包含的答案,但几乎故意忽略了真正关于真正调试的问题。我为这个答案鼓掌。 - simbo1905
http://dietrichschroff.blogspot.com/ 页面非常缓慢地完全加载(或超时)。 - Peter Mortensen
第一个链接https://sourceforge.net/projects/shelled/?source=directory会重定向到https://sourceforge.net/projects/dvkit/?source=directory("DVKit"),外表上看起来并没有与“Shelled”有任何关系。需要进行解释。 - Peter Mortensen
这与第一次尝试有何不同? - Peter Mortensen
要求或推荐使用Eclipse是荒谬的。如果你已经安装了它,当然可以使用你已有的;但这个回答的普适性极差。 - undefined

2

BDB目前支持英语和西班牙语。要更改语言,请编辑文件/etc/default/bdb。 - abadjm
这个截图看起来很有趣,但我无法运行它。"bdb.sh: line 32: bdbSTR[1]: unbound variable"。另外,它是否会显示我们在代码中每一步所做的所有设置变量的当前值? - Aquarius Power

1

一些调试Bash脚本的技巧:

使用set -[nvx]

除了

set -x

并且

set +x

用于停止转储。

我想谈谈 set -v,它会将输出减少到较小的未开发输出。

bash <<<$'set -x\nfor i in {0..9};do\n\techo $i\n\tdone\nset +x' 2>&1 >/dev/null|wc -l
21

for arg in x v n nx nv nvx;do echo "- opts: $arg"
    bash 2> >(wc -l|sed s/^/stderr:/) > >(wc -l|sed s/^/stdout:/) <<eof
        set -$arg
        for i in {0..9};do
            echo $i
          done
        set +$arg
        echo Done.
eof
    sleep .02
  done
- opts: x
stdout:11
stderr:21
- opts: v
stdout:11
stderr:4
- opts: n
stdout:0
stderr:0
- opts: nx
stdout:0
stderr:0
- opts: nv
stdout:0
stderr:5
- opts: nvx
stdout:0
stderr:5

动态转储变量或跟踪

为了测试一些变量,我有时会使用以下方法:

bash <(sed '18ideclare >&2 -p var1 var2' myscript.sh) args

用于添加:

declare >&2 -p var1 var2

在第18行运行生成的脚本(带有args),无需编辑即可运行。

当然,这也可以用于添加set [+-][nvx]

bash <(sed '18s/$/\ndeclare -p v1 v2 >\&2/;22s/^/set -x\n/;26s/^/set +x\n/' myscript) args

在第18行之后,将添加declare -p v1 v2 >&2,在第22行之前添加set -x,在第26行之前添加set +x

一个小例子:

bash <(sed '2,3s/$/\ndeclare -p LINENO i v2 >\&2/;5s/^/set -x\n/;7s/^/set +x\n/' <(
        seq -f 'echo $@, $((i=%g))' 1 8)) arg1 arg2
arg1 arg2, 1
arg1 arg2, 2
declare -i LINENO="3"
declare -- i="2"
/dev/fd/63: line 3: declare: v2: not found
arg1 arg2, 3
declare -i LINENO="5"
declare -- i="3"
/dev/fd/63: line 5: declare: v2: not found
arg1 arg2, 4
+ echo arg1 arg2, 5
arg1 arg2, 5
+ echo arg1 arg2, 6
arg1 arg2, 6
+ set +x
arg1 arg2, 7
arg1 arg2, 8

注意: 关注 $LINENO。它会受到即时修改的影响!

(要查看结果脚本而不执行,只需删除 bash <() arg1 arg2)

逐步执行,执行时间

查看关于如何分析Bash脚本的答案:my answer about how to profile Bash scripts


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接