Contents

管道进程的退出状态

1. 概述

在Bash中,可能存在我们管道命令并想要检查管道中命令之一的退出状态的情况。一个例子可能是一个长时间运行的进程,我们想在它运行时检查它的输出:

$ long_running_script.sh 2>&1 | tee output_of_script

在这个例子中,我们可能对脚本long_running_script.sh 的退出状态感兴趣。但是运行echo $? 命令在管道命令执行后立即给出tee 命令的退出状态。

在本教程中,我们将讨论获取管道命令的退出状态。

2. 问题分析

首先,让我们从管道命令的退出状态分析开始。我们有以下脚本hello_world.sh

#!/bin/bash
echo "Hello World"
exit $1

这个 Bash 脚本只打印Hello World并以作为脚本参数提供的退出状态退出:

$ hello_world.sh 0
Hello World
$ echo $?
0
$ hello_world.sh 5
Hello World
$ echo $?
5

现在,让我们使用grep命令在该脚本的输出中搜索Hello World,然后检查管道的退出状态:

$ hello_world.sh 5 | grep "Hello World"
Hello World
$ echo $?
0

正如我们所见,虽然hello_world.sh脚本的退出状态是5,但管道命令的退出状态是0。实际上,管道命令的退出状态是管道命令中最后一个命令的退出状态。在我们的示例中,它是grep “Hello World”命令的退出状态。让我们通过在脚本的输出中搜索一个不存在的单词来证明这一点,例如Hello Universe

$ hello_world.sh 5 | grep "Hello World" | grep "Hello Universe"
$ echo $?
1

现在,管道命令的退出状态为1,因为管道中最后一个命令grep “Hello Universe”的退出状态为1

3. PIPESTATUS变量

Bash 中的PIPESTATUS 环境变量可以帮助我们获取管道中每个命令的退出状态。** $PIPESTATUS是一个数组。它存储管道中每个命令的退出状态:**

$ hello_world.sh 5 | grep "Hello World" | grep "Hello Universe"
$ echo ${PIPESTATUS[@]}
5 0 1

**命令echo ${PIPESTATUS[@]}获取数组PIPESTATUS的所有元素。数组中的第一个元素 ( 5 ) 是hello_world.sh 5命令的退出状态。同样,数组中的第二个元素 ( 0 ) 是*grep “Hello World”*命令的退出状态。最后,数组 ( *1 ) 中的第三个元素是grep “Hello Universe”*命令的退出状态。

当然,我们可以通过数组对应的索引来获取我们感兴趣的命令的退出状态。数组中第一个元素的索引是0

$ hello_wold.sh 5 | grep "Hello World" | grep "Hello Universe"
$ echo ${PIPESTATUS[0]} ${PIPESTATUS[1]} ${PIPESTATUS[2]}
5 0 1

我们必须知道,每个命令执行都会更新PIPESTATUS变量

$ hello_world.sh 5 | grep "Hello World" | grep "Hello Universe"
$ echo ${PIPESTATUS[2]}
1
$ echo ${PIPESTATUS[0]}
0

在这种情况下,PIPESTATUS[0]返回的退出状态与我们的预期不同。因为命令echo ${PIPESTATUS[0]}的执行返回了上一个命令的退出状态,即echo ${PIPESTATUS[2]}。这种行为就像echo $? 命令。*$?是上一个命令的退出状态。它的值在每个命令执行中更新。因此echo ${PIPESTATUS[0]}命令给出了与echo $?*相同的退出状态。当我们不使用管道时的命令:

$ hello_world.sh 5
Hello World
$ echo $?
5
$ hello_world.sh 5
Hello World
$ echo ${PIPESTATUS[0]}
5

4.保存每个命令的退出状态

如果 Bash 没有为我们提供PIPESTATUS变量怎么办?好吧,只要有生命,就有希望。在这种情况下,我们可以保存每个命令的退出状态并稍后检查

$ { hello_world.sh 5; echo $? > exit_status_1; } | { grep "Hello World"; echo $? > exit_status_2; } | { grep "Hello Universe"; echo $? > exit_status_3; }
$ cat exit_status_1
5
$ cat exit_status_2
0
$ cat exit_status_3
1

在此示例中,我们*使用*echo $?对每个命令进行分组。命令。因此,我们创建了三个命令组。我们将每个命令的退出状态保存到一个文件中。例如,我们将命令hello_world.sh 5的退出状态保存到文件exit_status_1中。当我们在管道中使用命令组时,每个组的输出被重定向到下一个组的输入。如我们所见,每个命令的退出状态与我们使用PIPESTATUS变量获得的退出状态相同。

5. set命令的pipefail选项

我们已经知道,管道的退出状态是管道中最后一条命令的退出状态。例如,正如我们之前看到的,以下管道以退出状态0退出。但是,管道中的第一个命令hello_world.sh 5以退出状态5退出:

$ hello_world.sh 5 | grep "Hello World"
Hello World
$ echo $?
0

有时,我们可能希望管道返回管道中任何以非零值退出的命令的退出状态。为此,我们可以使用set 命令的pipefail 选项。 set命令是一个内置的 Linux 命令。它显示和设置环境变量的名称和值。它还有许多选项可以启用和禁用 shell 中的多种行为。pipefail选项就是其中之一。

默认情况下禁用。在这种情况下,管道的退出状态是管道中最后一个命令的退出状态。但是,如果我们启用它,则管道的退出状态是管道中最右边的命令的退出状态,该命令以非零退出状态退出。set –o pipefail命令启用此行为,而set +o pipefail命令禁用此行为:

$ set -o pipefail
$ hello_world.sh 5 | grep "Hello World"
Hello World
$ echo $?
5
$ set +o pipefail
$ hello_world.sh 5 | grep "Hello World"
Hello World
$ echo $?
0

当我们在上一个示例中使用set –o pipefail命令启用**pipefail选项时,管道的退出状态为5。这是hello_world.sh脚本的退出状态。但是,当我们使用set +o pipefail命令禁用pipefail选项时,管道的退出状态是管道中最后一个命令的退出状态,grep “Hello World”

为了显示管道的退出状态是管道中以非零退出状态退出的最右边命令的退出状态,让我们运行以下命令:

$ set -o pipefail
$ hello_world.sh 5 | grep "Hello Universe" | (grep Hello || true)
$ echo ${PIPESTATUS[@]}
5 1 0
$ hello_world.sh 5 | grep "Hello Universe" | (grep Hello || true)
$ echo $?
1

这里,命令hello_world.sh 5grep “Hello Universe”的退出状态都是非零,分别为51。但是管道的退出状态是1,因为*grep “Hello Universe”*是最右边的具有非零退出状态的命令。