如何在 Linux 上捕获 Bash 脚本中的错误

默认情况下,Linux 上的 Bash 脚本会报告错误但会继续运行。我们向您展示了如何自己处理错误,以便您可以决定下一步需要做什么。
脚本中的错误处理
处理错误是编程的一部分。即使您编写了完美的代码,您仍然会遇到错误情况。随着您安装和卸载软件、创建目录以及执行升级和更新,您计算机上的环境会随着时间的推移而发生变化。
例如,如果目录路径发生变化或文件权限发生变化,过去正常运行的脚本可能会遇到困难。 Bash shell 的默认操作是打印错误消息并继续执行脚本。这是一个危险的默认值。
如果失败的操作对于脚本中稍后发生的某些其他处理或操作至关重要,则该关键操作将不会成功。结果会有多大的灾难性取决于您的脚本试图做什么。
一个更健壮的方案将检测错误并让脚本在需要关闭或尝试修复故障情况时运行。例如,如果目录或文件丢失,让脚本重新创建它们可能会令人满意。
如果脚本遇到无法恢复的问题,它可以关闭。如果脚本必须关闭,它可以有机会执行所需的任何清理工作,例如删除临时文件或将错误情况和关闭原因写入日志文件。
检测退出状态
命令和程序生成一个值,在它们终止时发送到操作系统。这称为他们的退出状态。如果没有错误,它的值为零,如果发生错误,它的值为非零值。
我们可以检查脚本使用的命令的退出状态(也称为返回码),并确定命令是否成功。
在 Bash 中,零等于真。如果命令的响应不是 true,我们就知道发生了问题,我们可以采取适当的措施。
将此脚本复制到编辑器中,并将其保存到名为“bad_command.sh”的文件中。
#!/bin/bash
if ( ! bad_command ); then
echo "bad_command flagged an error."
exit 1
fi
您需要使用 chmod
命令使脚本可执行。这是使任何脚本可执行所需的步骤,因此如果您想在自己的计算机上试用脚本,请记住对每个脚本都执行此操作。在每种情况下替换适当脚本的名称。
chmod +x bad_command.sh

当我们运行脚本时,我们会看到预期的错误消息。
./bad_command.sh

没有“bad_command”这样的命令,也不是脚本中的函数名称。它无法执行,因此响应不为零。如果响应不为零——这里使用感叹号作为逻辑 NOT
运算符——执行 if
语句的主体。
在真实世界的脚本中,这可能会终止脚本(我们的示例会这样做),或者它可能会尝试修复错误情况。
exit 1
行可能看起来是多余的。毕竟,脚本中没有其他内容,无论如何它都会终止。但是使用 exit
命令允许我们将退出状态传递回 shell。如果我们的脚本在第二个脚本中被调用,第二个脚本就会知道这个脚本遇到了错误。
您可以对命令的退出状态使用逻辑 OR
运算符,如果第一个命令的响应非零,则调用脚本中的另一个命令或函数。
command_1 || command_2
这是有效的,因为第一个命令运行 OR
第二个。最左边的命令首先运行。如果成功,则不执行第二个命令。但是,如果第一个命令失败,则执行第二个命令。所以我们可以这样构造代码。这是“逻辑或./sh”。
#!/bin/bash
error_handler()
{
echo "Error: ($?) $1"
exit 1
}
bad_command || error_handler "bad_command failed, Line: ${LINENO}"
我们定义了一个名为 error_handler
的函数。这会打印出失败命令的退出状态,保存在变量 $?
中,以及调用函数时传递给它的一行文本。这保存在变量 $1
中。该函数以 1 的退出状态终止脚本。
该脚本尝试运行 bad_command
,但显然失败了,因此执行逻辑 OR
运算符右侧的命令 ||
。这将调用 error_handler
函数并传递一个字符串,该字符串命名失败的命令,并包含失败命令的行号。
我们将运行脚本以查看错误处理程序消息,然后使用 echo 检查脚本的退出状态。
./logical-or.sh
echo $?

我们的小 error_handler
函数提供尝试运行 bad_command
的退出状态、命令名称和行号。当您调试脚本时,这是有用的信息。
脚本的退出状态是一个。 error_handler
报告的 127 退出状态表示“找不到命令”。如果需要,我们可以将其传递给 exit
命令,以此作为脚本的退出状态。
另一种方法是扩展 error_handler
以检查退出状态的不同可能值并相应地执行不同的操作,使用这种类型的构造:
exit_code=$?
if [ $exit_code -eq 1 ]; then
echo "Operation not permitted"
elif [ $exit_code -eq 2 ]; then
echo "Misuse of shell builtins"
.
.
.
elif [ $status -eq 128 ]; then
echo "Invalid argument"
fi
使用 set 强制退出
如果您知道您希望脚本在出现错误时退出,您可以强制它这样做。这意味着你放弃了任何清理的机会——或者任何进一步的破坏——因为你的脚本一旦检测到错误就会终止。
为此,请使用带有 -e
(错误)选项的 set
命令。这告诉脚本在命令失败或返回大于零的退出代码时退出。此外,使用 -E
选项可确保在 shell 函数中进行错误检测和捕获。
要同时捕获未初始化的变量,请添加 -u
(未设置)选项。要确保在管道序列中检测到错误,请添加 -o pipefail
选项。如果没有这个,管道命令序列的退出状态就是序列中final命令的退出状态。管道序列中间的失败命令将不会被检测到。 -o pipefail
选项必须出现在选项列表中。
添加到脚本顶部的顺序是:
set -Eeuo pipefail
这是一个名为“unset-var.sh”的简短脚本,其中包含一个未设置的变量。
#!/bin/bash
set -Eeou pipefail
echo "$unset_variable"
echo "Do we see this line?"
当我们运行脚本时,unset_variable 被识别为未初始化的变量,脚本终止。
./unset-var.sh

第二个 echo
命令永远不会执行。
使用有错误的陷阱
Bash trap 命令允许您指定在发出特定信号时应调用的命令或函数。通常,这用于捕获信号,例如 SIGINT
,当您按下 Ctrl+C 组合键时会发出该信号。这个脚本是“sigint.sh”。
#!/bin/bash
trap "echo -e '\nTerminated by Ctrl+c'; exit" SIGINT
counter=0
while true
do
echo "Loop number:" $((++counter))
sleep 1
done
trap
命令包含一个 echo
命令和一个 exit
命令。当 SIGINT
被引发时,它将被触发。脚本的其余部分是一个简单的循环。如果您运行脚本并按 Ctrl+C,您将看到来自 trap
定义的消息,脚本将终止。
./sigint.sh

我们可以将 trap
与 ERR
信号一起使用,以在错误发生时捕获它们。然后可以将这些提供给命令或功能。这是“trap.sh”。我们正在向一个名为 error_handler
的函数发送错误通知。
#!/bin/bash
trap 'error_handler $? $LINENO' ERR
error_handler() {
echo "Error: ($1) occurred on $2"
}
main() {
echo "Inside main() function"
bad_command
second
third
exit $?
}
second() {
echo "After call to main()"
echo "Inside second() function"
}
third() {
echo "Inside third() function"
}
main
大部分脚本位于 main
函数内,它调用 second
和 third
函数。当遇到错误时——在这种情况下,因为 bad_command
不存在——trap
语句将错误定向到 error_handler
函数。它将失败命令的退出状态和行号传递给 error_handler
函数。
./trap.sh

我们的 error_handler
函数只是将错误的详细信息列出到终端窗口。如果需要,您可以向函数添加 exit
命令以终止脚本。或者您可以使用一系列 if/elif/fi
语句来针对不同的错误执行不同的操作。
可能可以修复一些错误,其他错误可能需要脚本停止。
最后的提示
捕获错误通常意味着预先排除可能出错的事情,并在它们出现时编写代码来处理这些意外事件。除此之外,还要确保脚本的执行流程和内部逻辑正确无误。
如果您使用此命令运行脚本,Bash 将在脚本执行时向您显示跟踪输出:
bash -x your-script.sh
Bash 在终端窗口中写入跟踪输出。它显示每个命令及其参数(如果有的话)。这发生在命令展开之后但执行之前。
它可以极大地帮助追踪难以捉摸的错误。