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

shutterstock_1278851809

Linuxset和命令规定当 Bash脚本pipefail发生故障时会发生什么。除了它应该停止还是应该继续之外,还有更多需要考虑的事情。

Bash 脚本和错误条件

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

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

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

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

以下是如何使用它们。

展示问题

这是一个简单的 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

1-8

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

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

#!/bin/bash

echo This will happen first
ls imaginary-filename
echo This will happen second

当我们运行这个脚本时,我们会看到来自ls.

./script-2.sh

2-8

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

echo $?

3-10

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

set -e 选项

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

通过在脚本的开头添加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 $?

4-8

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

处理管道故障

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

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

true
echo $?
false
echo $?

5-8

如果我们通过管道false进入true——false代表一个失败的进程——我们得到true的返回码为零。

false | true
echo $?

6-7

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

false | true | false | true
echo "${PIPESTATUS[0]} ${PIPESTATUS[1]} ${PIPESTATUS[2]} ${PIPESTATUS[3]}"

7-7

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 $?

8-7

第一个零是 的输出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 $?

9-6

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

捕获未初始化的变量

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

这是脚本 6.sh。

#!/bin/bash 
set -eo pipefail

echo "$notset" 
echo "Another echo command"

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

./script-6.sh
echo $?

10-5

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

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

#!/bin/bash 

set -eou pipefail

echo "$notset" 

echo "Another echo command"

让我们运行脚本:

./script-7.sh
echo $?

11-4

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

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

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

请注意,我们已将该-u选项添加为 set 语句中的第二个选项。-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

12-3

用斧头密封

另一个方便使用的选项是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

13-1

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

未经允许不得转载:表盘吧 » 如何在 Linux 上的 Bash 脚本中使用 set 和 pipefail