如何在 Linux 上的 Bash 脚本中使用 set 和 pipefail

Linux set
和 pipefail
命令规定了 Bash 脚本中发生故障时会发生什么。除了它应该停止还是应该继续下去之外,还有更多需要考虑的事情。
Bash 脚本和错误条件
Bash shell 脚本很棒。他们写得很快,不需要编译。您需要执行的任何重复或多阶段操作都可以包装在一个方便的脚本中。因为脚本可以调用任何标准的 Linux 实用程序,所以您不会受限于 shell 语言本身的功能。
但是当您调用外部实用程序或程序时可能会出现问题。如果失败,外部实用程序将关闭并向 shell 发送一个返回码,它甚至可能向终端打印一条错误消息。但是您的脚本将继续处理。也许那不是你想要的。如果在脚本执行的早期发生错误,如果允许脚本的其余部分运行,则可能会导致更严重的问题。
您可以在每个外部进程完成时检查它们的返回码,但是当进程通过管道传输到其他进程时,这会变得很困难。返回代码将来自管道末端的进程,而不是中间失败的进程。当然,脚本中也可能出现错误,例如尝试访问未初始化的变量。
set
和 pipefile
命令可让您决定发生此类错误时的处理方式。即使错误发生在管道链的中间,它们也能让您检测到错误。
以下是如何使用它们。
证明问题
这是一个简单的 Bash 脚本。它向终端回显两行文本。如果将文本复制到编辑器中并将其另存为“script-1.sh”,则可以运行此脚本。
#!/bin/bash
echo This will happen first
echo This will happen second
要使其可执行,您需要使用 chmod
:
chmod +x script-1.sh
如果你想在你的计算机上运行它们,你需要在每个脚本上运行该命令。让我们运行脚本:
./script-1.sh

这两行文本按预期发送到终端窗口。
让我们稍微修改一下脚本。我们将要求 ls
列出不存在的文件的详细信息。这将失败。我们将其保存为“script-2.sh”并使其可执行。
#!/bin/bash
echo This will happen first
ls imaginary-filename
echo This will happen second
当我们运行此脚本时,我们会看到来自 ls
的错误消息。
./script-2.sh

虽然 ls
命令失败,但脚本继续运行。即使在脚本执行期间出现错误,从脚本到 shell 的返回码为零,这表明成功。我们可以使用 echo 和 $?
变量来检查这一点,该变量保存发送到 shell 的最后一个返回代码。
echo $?

报告的零是脚本中第二个回显的返回码。所以这个场景有两个问题。首先是脚本有错误但它继续运行。如果脚本的其余部分期望或取决于失败的操作实际上成功了,那么这可能会导致其他问题。第二个是如果另一个脚本或进程需要检查这个脚本的成功或失败,它会得到一个错误的读数。
set -e 选项
set -e
(退出)选项会导致脚本在其调用的任何进程生成非零返回代码时退出。任何非零值都被视为失败。
通过将 set -e
选项添加到脚本的开头,我们可以更改其行为。这是“script-3.sh”。
#!/bin/bash
set -e
echo This will happen first
ls imaginary-filename
echo This will happen second
如果我们运行这个脚本,我们将看到 set -e
的效果。
./script-3.sh
echo $?

脚本暂停,发送到 shell 的返回码是一个非零值。
处理管道故障
管道增加了问题的复杂性。来自管道命令序列的返回码是链中最后一个命令的返回码。如果链条中间的命令出现故障,我们将回到原点。该返回码丢失,脚本将继续处理。
我们可以使用 true
和 false
shell 内置命令查看具有不同返回码的管道命令的效果。这两个命令只不过分别生成零或一的返回代码。
true
echo $?
false
echo $?

如果我们将 false
传递给 true
——false
代表一个失败的进程——我们得到 true
的返回码零。
false | true
echo $?

Bash 确实有一个名为 PIPESTATUS
的数组变量,它捕获管道链中每个程序的所有返回代码。
false | true | false | true
echo "${PIPESTATUS[0]} ${PIPESTATUS[1]} ${PIPESTATUS[2]} ${PIPESTATUS[3]}"

PIPESTATUS
只保存返回码,直到下一个程序运行,并且试图确定哪个返回码与哪个程序一起运行会很快变得非常混乱。
这就是 set -o
(选项)和 pipefail
发挥作用的地方。这就是“script-4.sh”。这将尝试将不存在的文件内容通过管道传输到 wc
中。
#!/bin/bash
set -e
echo This will happen first
cat script-99.sh | wc -l
echo This will happen second
正如我们所料,这失败了。
./script-4.sh
echo $?

第一个零是 wc
的输出,告诉我们它没有读取丢失文件的任何行。第二个零是第二个 echo
命令的返回码。
我们将添加 -o pipefail
,将其保存为“script-5.sh”,并使其可执行。
#!/bin/bash
set -eo pipefail
echo This will happen first
cat script-99.sh | wc -l
echo This will happen second
让我们运行它并检查返回码。
./script-5.sh
echo $?

脚本停止,第二个 echo
命令没有执行。发送到 shell 的返回码是 1,正确指示失败。
捕获未初始化的变量
在真实世界的脚本中很难发现未初始化的变量。如果我们尝试 echo
未初始化变量的值,echo
只会打印一个空行。它不会引发错误消息。脚本的其余部分将继续执行。
这是 script-6.sh。
#!/bin/bash
set -eo pipefail
echo "$notset"
echo "Another echo command"
我们将运行它并观察它的行为。
./script-6.sh
echo $?

脚本越过未初始化的变量,并继续执行。返回代码为零。试图在一个非常长且复杂的脚本中找到这样的错误可能非常困难。
我们可以使用 set -u
(unset) 选项捕获这种类型的错误。我们将把它添加到脚本顶部不断增长的设置选项集合中,将其保存为“script-7.sh”,并使其可执行。
#!/bin/bash
set -eou pipefail
echo "$notset"
echo "Another echo command"
让我们运行脚本:
./script-7.sh
echo $?

检测到未初始化的变量,脚本停止,返回代码设置为 1。
-u
(取消设置)选项足够智能,不会被您可以与未初始化变量合法交互的情况触发。
在“script-8.sh”中,脚本检查变量 New_Var
是否被初始化。您不希望脚本就此停止,在真实世界的脚本中,您将执行进一步的处理并自行处理情况。
请注意,我们在 set 语句中添加了 -u
选项作为 second 选项。 -o pipefail
选项必须放在最后。
#!/bin/bash
set -euo pipefail
if [ -z "${New_Var:-}" ]; then
echo "New_Var has no value assigned to it."
fi
在“script-9.sh”中,测试未初始化的变量,如果未初始化,则提供默认值。
#!/bin/bash
set -euo pipefail
default_value=484
Value=${New_Var:-$default_value}
echo "New_Var=$Value"
允许脚本运行直至完成。
./script-8.sh
./script-9.sh

用 x 密封
另一个方便使用的选项是 set -x
(执行和打印)选项。当您编写脚本时,这可以成为您的救星。它在执行时打印命令及其参数。
它为您提供了一种快速的“粗略和现成”形式的执行跟踪。隔离逻辑缺陷和发现错误变得非常容易。
我们将 set -x 选项添加到“script-8.sh”,将其保存为“script-10.sh”,并使其可执行。
#!/bin/bash
set -euxo pipefail
if [ -z "${New_Var:-}" ]; then
echo "New_Var has no value assigned to it."
fi
运行它以查看跟踪线。
./script-10.sh

在这些简单的示例脚本中发现错误很容易。当您开始编写更多涉及的脚本时,这些选项将证明它们的价值。