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

已发表: 2022-06-25
蓝色背景下笔记本电脑屏幕上的 Linux 终端。
fatmawati achmad zaenuri/Shutterstock.com

Linux setpipefail命令指示 Bash 脚本中发生故障时会发生什么。 除了它应该停止还是应该继续之外,还有更多需要考虑的事情。

相关: Shell 脚本初学者指南:基础

Bash 脚本和错误条件

Bash shell 脚本很棒。 它们写得很快,而且不需要编译。 您需要执行的任何重复或多阶段操作都可以包含在一个方便的脚本中。 而且由于脚本可以调用任何标准的 Linux 实用程序,因此您不受限于 shell 语言本身的功能。

但是当您调用外部实用程序或程序时可能会出现问题。 如果失败,外部实用程序将关闭并向 shell 发送返回码,甚至可能向终端打印错误消息。 但是您的脚本将继续处理。 也许那不是你想要的。 如果在脚本执行的早期发生错误,如果允许脚本的其余部分运行,则可能会导致更严重的问题。

什么是 Bash Shell,为什么它对 Linux 如此重要?
相关什么是 Bash Shell,为什么它对 Linux 如此重要?

您可以在每个外部进程完成时检查它们的返回码,但是当进程通过管道传输到其他进程时,这变得很困难。 返回码将来自管道末端的进程,而不是中间失败的进程。 当然,脚本内部也可能发生错误,例如尝试访问未初始化的变量。

setpipefile命令让您决定发生此类错误时会发生什么。 即使错误发生在管道链的中间,它们也可以让您检测到错误。

以下是如何使用它们。

展示问题

这是一个简单的 Bash 脚本。 它将两行文本回显到终端。 如果将文本复制到编辑器中并将其保存为“script-1.sh”,则可以运行此脚本。

 #!/bin/bash

echo 这将首先发生 
echo 这将发生在第二个

要使其可执行,您需要使用chmod

 chmod +x script-1.sh

如果要在计算机上运行它们,则需要在每个脚本上运行该命令。 让我们运行脚本:

 ./script-1.sh 

运行一个没有错误的简单脚本。

两行文本按预期发送到终端窗口。

让我们稍微修改一下脚本。 我们将要求ls列出不存在的文件的详细信息。 这将失败。 我们将其保存为“script-2.sh”并使其可执行。

 #!/bin/bash

echo 这将首先发生
ls 虚构文件名
echo 这将发生在第二个

当我们运行这个脚本时,我们会看到来自ls的错误消息。

 ./script-2.sh 

运行脚本并生成失败条件。

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

 回声$? 

检查最后执行的脚本的返回码。

报告的零是脚本中第二个回显的返回码。 所以这个场景有两个问题。 首先是脚本有错误但它继续运行。 如果脚本的其余部分期望或依赖于失败的操作实际成功,这可能会导致其他问题。 第二个是如果另一个脚本或进程需要检查这个脚本的成功或失败,它会得到一个错误的读数。

set -e 选项

如果脚本调用的任何进程生成非零返回码, set -e (退出)选项会导致脚本退出。 任何非零都被视为失败。

通过在脚本的开头添加set -e选项,我们可以改变它的行为。 这是“script-3.sh”。

 #!/bin/bash 
设置-e

echo 这将首先发生
ls 虚构文件名
echo 这将发生在第二个

如果我们运行这个脚本,我们将看到set -e的效果。

 ./script-3.sh
 回声$? 

在错误条件下终止脚本,并正确设置返回码。

脚本停止并且发送到 shell 的返回码是一个非零值。

处理管道故障

管道增加了问题的复杂性。 来自管道命令序列的返回码是链中最后一个命令的返回码。 如果链条中间的命令出现故障,我们将回到第一方。 该返回码丢失,脚本将继续处理。

我们可以使用truefalse shell 内置函数查看具有不同返回码的管道命令的效果。 这两个命令只不过分别生成一个零或一的返回码。

 真的
回声$?
 错误的
回声$? 

bash shell tr​​ue 和 false 内置命令。

如果我们通过管道将false转换为true (使用false表示失败的进程),我们会得到true的返回码为零。

 假 | 真的
回声$? 

将虚假变为真实。

Bash 确实有一个名为PIPESTATUS的数组变量,它捕获了管道链中每个程序的所有返回代码。

 假 | 真实 | 假 | 真的
echo "${PIPESTATUS[0]} ${PIPESTATUS[1]} ${PIPESTATUS[2]} ${PIPESTATUS[3]}" 

使用 PIPESTATUS 查看管道链中所有程序的返回码。

PIPESTATUS只保留返回代码,直到下一个程序运行,并且试图确定哪个返回代码与哪个程序一起使用会很快变得非常混乱。

这就是set -o (选项)和pipefail的用武之地。这是“script-4.sh”。 这将尝试将不存在的文件的内容通过管道传输到wc中。

 #!/bin/bash 
设置-e

echo 这将首先发生
猫脚本-99.sh | wc -l
echo 这将发生在第二个

正如我们所料,这失败了。

 ./script-4.sh
 回声$? 

在管道链中运行错误的脚本。

第一个零是wc的输出,告诉我们它没有读取丢失文件的任何行。 第二个零是第二个echo命令的返回码。

我们将添加-o pipefail ,将其保存为“script-5.sh”,并使其可执行。

 #!/bin/bash 
设置-eo pipefail

echo 这将首先发生
猫脚本-99.sh | wc -l
echo 这将发生在第二个

让我们运行它并检查返回码。

 ./script-5.sh
 回声$? 

运行捕获管道链中的错误并正确设置返回码的脚本。

脚本停止,第二个echo命令没有执行。 发送到 shell 的返回码是 1,正确指示失败。

相关:如何在 Linux 上使用 Echo 命令

捕获未初始化的变量

未初始化的变量很难在真实脚本中被发现。 如果我们尝试echo显未初始化变量的值, echo显只会打印一个空行。 它不会引发错误消息。 脚本的其余部分将继续执行。

这是脚本 6.sh。

 #!/bin/bash 
设置-eo pipefail

回声“$notset” 
echo "另一个回显命令"

我们将运行它并观察它的行为。

 ./script-6.sh
 回声$? 

运行不捕获未初始化变量的脚本。

脚本会跳过未初始化的变量,并继续执行。 返回码为零。 试图在一个非常长而复杂的脚本中找到这样的错误可能非常困难。

如何在 Bash 中使用变量
相关如何在 Bash 中使用变量

我们可以使用set -u (unset) 选项来捕获此类错误。 我们将把它添加到脚本顶部不断增长的设置选项集合中,将其保存为“script-7.sh”,并使其可执行。

 #!/bin/bash 

设置-eou pipefail

回声“$notset” 

echo "另一个回显命令"

让我们运行脚本:

 ./script-7.sh
 回声$? 

运行一个捕获未初始化变量的脚本。

检测到未初始化的变量,脚本停止,返回码设置为 1。

-u (未设置)选项足够智能,不会被您可以合法地与未初始化变量交互的情况触发

在“script-8.sh”中,脚本检查变量New_Var是否已初始化。 您不希望脚本停在这里,在真实世界的脚本中,您将执行进一步的处理并自己处理情况。

请注意,我们在 set 语句中添加了-u选项作为第二个选项。 -o pipefail选项必须放在最后。

 #!/bin/bash 

设置 -eu 管道故障

如果 [ -z "${New_Var:-}" ]; 然后 

echo "New_Var 没有赋值。" 

菲

在“script-9.sh”中,测试未初始化的变量,如果未初始化,则提供默认值。

 #!/bin/bash
设置 -eu 管道故障

默认值=484
值=${New_Var:-$default_value}
回声“New_Var=$Value”

允许脚本运行到完成。

 ./script-8.sh
 ./script-9.sh 

运行两个脚本,其中未初始化的变量在内部处理,并且 -u 选项不会触发。

用斧头密封

另一个方便使用的选项是set -x (执行和打印)选项。 当您编写脚本时,这可以成为救命稻草。 它在执行命令时打印命令及其参数。

它为您提供了一种快速“粗略且准备就绪”的执行跟踪形式。 隔离逻辑缺陷和发现错误变得非常容易。

我们将 set -x 选项添加到“script-8.sh”,将其保存为“script-10.sh”,并使其可执行。

 #!/bin/bash
设置-euxo pipefail

如果 [ -z "${New_Var:-}" ]; 然后
  echo "New_Var 没有赋值。"
菲

运行它以查看跟踪线。

 ./script-10.sh 

运行一个脚本,将 -x 跟踪行写入终端。

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