如何在 Linux 和 Unix 中调试 Bash 脚本

调试可帮助您修复程序中的错误。 在本文中,我们将讨论各种调试方法 bash Linux 和 Unix 操作系统中的脚本。

介绍

在我最初的编程日子里,我花了几个小时试图在我的代码中找到错误,最后,它可能很简单。 你可能也遇到过同样的情况。

知道如何使用正确的调试技术将帮助您快速解决错误。 与 Python、Java 等其他语言不同,没有调试器工具可用于 bash 您可以在其中设置断点、单步执行代码等。

有一些内置功能有助于调试 bash 外壳脚本。 我们将在接下来的部分中详细了解这些功能。

使用调试选项的三种方法

当您想在脚本中启用调试选项时,可以通过三种方式进行。

1. 调用脚本时从终端 shell 启用调试选项。

$ bash [ debugging flags ] scriptname

2. 通过将调试标志传递给脚本中的 shebang 行来启用调试选项。

#!/bin/bash [ debugging flags ]

3. 通过使用启用调试选项 set 来自脚本的命令。

set -o nounset
set -u

Set 命令是做什么用的?

set command 是一个 shell 内置命令,可用于控制 bash 参数和改变 bash 某些方面的行为。

通常你不会从终端运行 set 命令来改变你的 shell 行为。 它将在 shell 脚本中广泛用于调试或启用 bash 严格模式。

$ type -a set
set is a shell builtin

您可以访问 set 命令的帮助部分以了解它支持哪些标志以及每个标志的作用。

$ set --help

调试部分脚本或完整脚本

在了解调试选项之前,您必须了解您可以调试整个脚本或仅调试代码的某个部分。 您必须使用 set 命令来启用和禁用调试选项。

  • set -<debugging-flag> 将启用调试模式。
  • set +<debugging-flag> 将禁用调试模式。

看看下面的代码。 set -x 将为脚本启用 xtrace 模式,并且 set +x 将禁用 xtrace 模式。 介于两者之间的任何东西 set -xset +x 将在 xtrace 调试模式下运行。

您将在接下来的部分中了解 xtrace 模式。 所以对于任何调试标志,你唯一需要记住的是, set - 将启用模式和 set + 将禁用该模式。

#!/bin/bash

set -x
read -p "Pass Dir name : " D_OBJECT
read -p "Pass File name : " F_OBJECT
set +x

touch ${D_OBJECT}/${F_OBJECT}

如果没有定义变量则失败

工作时 变量 bash,缺点是如果我们尝试使用未定义的变量,脚本将不会失败并显示一些错误消息,例如 “变量未定义”. 相反,它将打印一个空字符串。

看看下面的代码,我从用户那里获取输入并将其存储在变量中 $OBJECT. 我试图运行测试运算符(-f-d) 在 $OBJECT1 未定义的变量。

#!/bin/bash

read -p "Please provide the object name :  " OBJECT
if [[ -f $OBJECT1 ]]
then
    echo "$OBJECT is a file"
elif [[ -d $OBJECT1 ]]
then
    echo "$OBJECT is a directory"
fi

当我运行这段代码时,它应该给我一个错误,但它没有,甚至脚本退出并返回代码为零。

脚本成功完成

要覆盖此行为,请使用 -u 使用未定义变量时将引发错误的标志。

我将使用错误的变量名再次运行相同的代码,但这次它会抛出一个 “未绑定变量” 错误。

未绑定变量

您还可以设置 -u 选项使用 set 命令或将其作为参数传递给 shebang。

set -u
set -o nounset

(或者)

#! /bin/bash -u

Xtrace 模式助您一臂之力

这是我调试时广泛使用的模式 bash 逻辑错误的脚本。 Xtrace mode 将逐行显示代码,但会扩展参数。

在上一节中,当我在没有 -u 标志,它已成功完成,但我期待终端中的输出。 现在我可以在 xtrace 模式下运行相同的脚本,并准确查看脚本中出现问题的位置。

看看下面的示例代码。

#!/bin/bash

read -p "Please provide the object name :  " OBJECT
if [[ -f $OBJECT1 ]]
then
    echo "$OBJECT is a file"
elif [[ -d $OBJECT1 ]]
then
    echo "$OBJECT is a directory"
fi

当我运行上面的代码时,它没有返回任何输出。

逻辑错误逻辑错误

要调试此问题,我可以在 跟踪 模式通过传递 -x 旗帜。

在下面的输出中,您可以看到变量已展开并打印出来。 这告诉我有分配给条件语句的空字符串 -f-d. 这样我就可以在逻辑上检查并修复错误。

Xtrace 模式Xtrace 模式

您在输出中看到的加号可以通过设置 PS4 脚本中的变量。 默认情况下,PS4 设置为 (+)。

$ echo $PS4
+
$ PS4=" ==> " bash -x debugging.sh
设置或更改 PS4 变量设置或更改 PS4 变量

您还可以使用 set 命令设置 Xtrace 模式或将其作为参数传递给 shebang。

set -x
set -o xtrace

(或者)

#! /bin/bash -x

同样,在调试时,您可以将 Xtrace 调试日志重定向到文件,而不是将它们打印到终端。

看看下面的代码。 我将文件描述符 6 分配给 .log 文件和 BASH_XTRACEFD="6" 将 xtrace 调试日志重定向到文件描述符 6。

#!/bin/bash


exec 6> redirected_debug.log 
PS4=' ==> ' 
BASH_XTRACEFD="6" 
read -p "Please provide the object name :  " OBJECT
if [[ -f $OBJECT1 ]]
then
    echo "$OBJECT is a file"
elif [[ -d $OBJECT1 ]]
then
    echo "$OBJECT is a directory"
fi

当我运行此代码而不是在终端中打印 xtrace 输出时,它将被重定向到 .log 文件。

$ cat redirected_debug.log 
==> read -p 'Please provide the object name :  ' OBJECT
==> [[ -f '' ]]
==> [[ -d '' ]]

管道退出状态

使用管道时的默认行为是它将采用管道中最后一个运行命令的退出代码。 即使管道中的先前命令失败,它也会运行管道的其余部分。

看看下面 example. 我尝试打开一个不可用的文件并使用 字数 程序。 尽管 cat 命令抛出错误,字数统计程序运行。

如果您尝试使用检查最后一个运行管道命令的退出代码 $?,您将得到零作为来自字数统计程序的退出代码。

$ cat nofile.txt | wc -l
cat: nofile.txt: No such file or directory
0
$ echo $?
0

在脚本中启用 pipefail 时,如果任何命令在管道中抛出非零返回码,则将其视为整个管道的返回码。 您可以通过在脚本中添加以下 set 属性来启用 pipefail。

set -o pipefail
管道 Exit 地位管道 Exit 地位

这种方法仍然存在问题。 通常应该预期的是,如果管道中的任何命令失败,则脚本应该退出而不运行管道中的其余命令。

但不幸的是,即使任何命令失败,管道中的后续命令也会运行。 这是因为管道中的每个命令都在其自己的子 shell 中运行。 shell 将等待管道中的所有进程完成,然后返回结果。

Bash 严格模式

为了消除我们在前几节中看到的所有可能的错误,建议在每个脚本中添加以下选项。

我们已经在上一节详细讨论了所有这些选项。

  • -e flag => Exit 如果任何命令抛出非零退出代码,则脚本。
  • -u flag => 如果使用未定义的变量名,则使脚本失败。
  • pipefail => 如果管道中的任何命令失败,则将考虑整个管道的退出代码。
  • IFS => 内部字段分隔符,将其设置为换行符(n) 和 (t) 将使拆分仅在换行符和制表符中发生。
set -e
set -u
set -o pipefail

或者

set -euo pipefail
IFS=$'nt'

使用 TRAP 捕获信号

陷阱 允许您将信号捕获到您的 bash 脚本并相应地采取一些行动。

想一想您触发脚本但您想使用取消脚本的场景 CTRL+C 击键。 在这种情况下, SIGINT 将发送到您的脚本。 您可以捕获此信号并运行一些命令或功能。

看看下面给出的伪代码。 我创建了一个名为 cleanup 的函数,它将在何时运行 SIGINT 传递给脚本。

trap 'cleanup' TERM INT
function cleanup(){
    echo "Running cleanup since user initiated CTRL + C"
    <some logic>
}

您可以使用陷阱“DEBUG”,它可用于在脚本中重复执行一条语句。 它的行为方式是对于在脚本陷阱中运行的每个语句都将运行相关的函数或语句。

您可以使用以下内容来理解这一点 example.

#!/bin/bash

trap 'printf "${LINENO} ==> DIR_NAME=${D_OBJECT} ; FILE_NAME=${F_OBJECT}; FILE_CREATED=${FILE_C} n"' DEBUG

read -p "Pass Dir name : " D_OBJECT
read -p "Pass File name : " F_OBJECT

touch ${D_OBJECT}/${F_OBJECT} && FILE_C="Yes"
exit 0

这是一个获取用户输入并创建文件和目录的简单程序。 陷阱命令将为脚本中的每个语句运行,并将打印传递的参数和文件创建状态。

查看下面的输出。 对于脚本中的每一行,都会触发陷阱并相应地更新变量。

为每个语句运行 TRAP为每个语句运行 TRAP

在详细模式下,代码将在返回结果之前打印出来。 如果程序在这种情况下需要交互式输入,则将单独打印该行,然后是代码块。

看看下面的程序。 它是一个简单的程序,它从用户那里获取一个对象,并使用 条件语句.

#!/bin/bash

read -p "Please provide the object name :  " OBJECT
if [[ -f $OBJECT ]]
then
  echo "$OBJECT is a file"
elif [[ -d $OBJECT ]]
then
  echo "$OBJECT is a directory"
fi

当我运行上面的代码时,它首先会打印代码,然后等待用户输入,如下所示。

详细模式详细模式

一旦我传递了对象,那么其余的代码将被打印出来,然后是输出。

使用详细模式打印代码使用详细模式打印代码

您还可以使用设置详细模式 set 或在 shebang.

set -v
set -o verbose

(或者)

#! /bin/bash -v

您还可以将详细模式与其他模式结合使用。

set -vx # Verbose and Xtrace Mode
set -uv # Verbose and Unset Mode

语法验证 – noexec 模式

到目前为止,我们已经了解了如何处理脚本中的逻辑错误。 在本节中,让我们讨论一下语法错误。

语法错误在程序中很常见。 您可能错过了报价或未能退出循环等。您可以使用“-n” 旗子被称为 noexec mode 在运行程序之前验证语法。

我将运行下面的代码并验证语法。

#!/bin/bash

TOOLS=( htop peek tilix vagrant shutter )
for TOOL in "${TOOLS[@]" 
do
    echo "--------------INSTALLING: ${TOOL}---------------------------"
    apt install ${TOOL} -y
#done

这个程序有两个错误。 首先,我没能 close “中的花括号for loop“ 其次 done 关键字被注释掉,这应该标志着循环的结束。

当我运行这个程序时,我收到以下错误消息,指出缺少 花括号完成关键字. 有时,错误消息中指向的行号不会包含您应该挖掘以找到实际错误的任何错误。

$ bash -n ./debugging.sh 

./debugging.sh: line 6: unexpected EOF while looking for matching `"'
./debugging.sh: line 8: syntax error: unexpected end of file

需要注意的是,默认情况下,当您运行脚本时, bash 即使不使用 noexec 模式,也会验证语法并抛出这些错误。

或者,您也可以使用 set 命令或 shebang 使用 noexec 模式。

set -n 
set -o noexec

或者,

#! /bin/bash -n

有一些外部工具值得一看,用于调试脚本。 一种这样的工具是 壳牌检查. Shellcheck 还可以与流行的文本编辑器集成,例如 vscode、sublime text、Atom。

结论

在本文中,我向您展示了一些调试方法 bash 脚本。 与其他编程语言不同, bash 除了一些内置选项外,没有调试工具。 有时这些内置的调试选项足以完成工作。

Bash 脚本指南:

  • Bash 脚本——使用 getopts 解析 Bash 脚本中的参数
  • 如何在 Linux 和 Unix 中使用 Zenity 在 Bash 脚本中创建 GUI 对话框
  • Bash 脚本 – 案例陈述
  • Bash 脚本 – 条件语句
  • Bash 脚本 – 字符串操作
  • Bash 脚本 – Printf 命令用示例解释
  • Bash 脚本——用示例解释索引数组
  • Bash 脚本——用示例解释的关联数组
  • Bash 脚本——用例子解释 For 循环
  • Bash 脚本 – While 和 until 循环用示例解释
  • 用示例解释 Bash 重定向
  • Bash 脚本——用例子解释的变量
  • Bash 脚本——用例子解释的函数
  • 用 Linux 中的示例解释 Bash Echo 命令
  • Bash Heredoc 初学者教程