在Bash中管道输出并捕获退出状态

520

我希望在Bash中执行一个长时间运行的命令,并且捕获其退出状态,同时tee输出。

因此我会这样做:

command | tee out.txt
ST=$?
问题在于变量ST捕获了tee的退出状态,而不是命令本身。我该怎么解决这个问题?
请注意,该命令长时间运行,并将输出重定向到文件以便以后查看对我来说并不是一个好的解决方案。

1
[[ "${PIPESTATUS[@]}" =~ [^0\ ] ]] && echo -e "匹配 - 发现错误" || echo -e "无匹配 - 一切正常" 这将一次性测试数组的所有值,并在任何管道返回的值不为零时给出错误消息。这是检测管道情况中的错误的相当强大的通用解决方案。 - Brian S. Wilson
http://unix.stackexchange.com/questions/14270/get-exit-status-of-process-thats-piped-to-another - Ciro Santilli OurBigBook.com
16个回答

617

在Bash中有一个名为$PIPESTATUS的内部变量;它是一个数组,用于保存最后一个前台管道命令中每个命令的退出状态。

<command> | tee out.txt ; test ${PIPESTATUS[0]} -eq 0

或者另一种方法,也适用于其他shell(如zsh),就是启用pipefail:

set -o pipefail
...

由于语法略有不同,第一种选项在zsh中不起作用。


27
这里有一个很好的关于PIPESTATUS和Pipefail的解释,附带例子:http://unix.stackexchange.com/a/73180/7453。 - slm
21
注意:$PIPESTATUS [0] 保存管道中第一个命令的退出状态,$PIPESTATUS [1] 保存第二个命令的退出状态,以此类推。 - simpleuser
20
当然,我们必须记住这只适用于Bash:例如,如果我编写一个脚本在我的Android设备上运行BusyBox的“sh”实现或者在其他嵌入式平台使用某些其他“sh”变体,则此操作将不起作用。 - Asfand Qazi
4
对于那些关心未引用变量扩展的人:在Bash中,退出状态始终为无符号8位整数(参见https://www.gnu.org/software/bash/manual/html_node/Exit-Status.html),因此没有必要对其进行引用。这也适用于Unix系统,因为[退出状态明确定义为8位](http://pubs.opengroup.org/onlinepubs/9699919799/functions/_Exit.html),并且即使是POSIX本身,在定义其逻辑否定时也假定它是无符号的,例如,(http://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_09_02_01)。 - Palec
7
你也可以使用 exit ${PIPESTATUS[0]}。该命令的作用与原文相同,意为退出当前脚本并返回管道中第一个命令的退出状态码。 - Chaoran
显示剩余6条评论

169
笨方法:通过命名管道(mkfifo)将它们连接起来。然后可以运行第二个命令。
 mkfifo pipe
 tee out.txt < pipe &
 command > pipe
 echo $?

编辑:由于我不得不查找这个信息,我正在添加装饰来使用一个随机命名的管道并关闭它。同时将标准输出发送到命名管道。我在一个脚本中需要多个这样的结构。- LM
pipename="pipe-$RANDOM" ; mkfifo "$pipename" ; tee -a "$LOG_FILE" < "$pipename" &
command > "$pipename" 2>&1
echo $?
rm "$pipename"

34
这是这个问题中唯一一个同样适用于简单的 sh Unix shell 的答案。谢谢! - JamesThomasMoon
4
@davekennedy:所说的“愚蠢”,指的是“明显的,不需要深入了解bash语法”。 - EFraim
12
当你拥有bash的额外功能优势时,bash答案可能更加优雅,但这是更跨平台的解决方案。通常情况下,每当执行长时间命令时,命名管道通常是最灵活的方式,因此这也值得我们总体思考。值得注意的是,一些系统没有mkfifo,而是需要使用mknod -p,如果我记得正确的话。 - Haravikk
3
有时在 Stack Overflow 上会有那种你愿意点赞一百次的答案,这样人们就不会再做一些毫无意义的事情了,这就是其中之一。谢谢您,先生。 - Dan Chase
3
"mkfifo" 似乎更加便携可移植。https://pubs.opengroup.org/onlinepubs/9699919799/ - MarcH
显示剩余3条评论

154

使用bash的 set -o pipefail 很有帮助。

pipefail: 管道中最后一个退出状态非零的命令的返回值将作为整个管道的返回值; 如果没有命令退出状态非零,则返回值为零。


28
如果你不想修改整个脚本的pipefail设置,你可以仅在本地设置选项:( set -o pipefail; command | tee out.txt ); ST=$? - Jaan
10
这将会运行一个子shell。如果你想避免这种情况,你可以执行set -o pipefail,然后执行命令,紧接着在命令之后执行set +o pipefail取消该选项。 - Linus Arver
2
注意:问题发布者不想要管道的“一般退出代码”,他想要的是“命令”的返回代码。使用-o pipefail,他将知道管道是否失败,但如果'command'和'tee'都失败,他将收到'tee'的退出代码。 - t0r0X
@LinusArver 那不会清除退出代码吗?因为这是一个成功的命令? - carlin.scott

38

有一个数组,它提供了管道中每个命令的退出状态。

$ cat x| sed 's///'
cat: x: No such file or directory
$ echo $?
0
$ cat x| sed 's///'
cat: x: No such file or directory
$ echo ${PIPESTATUS[*]}
1 0
$ touch x
$ cat x| sed 's'
sed: 1: "s": substitute pattern can not be delimited by newline or backslash
$ echo ${PIPESTATUS[*]}
0 1

32

这个解决方案不使用bash特定功能或临时文件。额外收获:最终退出状态实际上是一个退出状态而不是文件中的某个字符串。

情况:

someprog | filter

您想要从someprog获取退出状态和从filter获取输出结果。

这是我的解决方案:

((((someprog; echo $? >&3) | filter >&4) 3>&1) | (read xs; exit $xs)) 4>&1

echo $?

请查看我的回答,在unix.stackexchange.com上解释了详细内容,并提供了一个不涉及子shell和一些警告的替代方案。


23

通过将 PIPESTATUS[0] 和在子shell中执行的 exit 命令的结果结合起来,您可以直接访问初始命令的返回值:

command | tee ; ( exit ${PIPESTATUS[0]} )

这是一个例子:

# the "false" shell built-in command returns 1
false | tee ; ( exit ${PIPESTATUS[0]} )
echo "return value: $?"

将会给您:

返回值:1


5
谢谢,这让我可以使用语句:VALUE=$(might_fail | piping),它不会在主shell中设置PIPESTATUS,但会设置其错误级别。通过使用:VALUE=$(might_fail | piping; exit ${PIPESTATUS[0]}),我得到了我想要的结果。 - vaab
@vaab,那个语法看起来真的很好,但是我对你所说的“piping”在这个上下文中的意思感到困惑。这只是指在 might_fail 的输出上执行“tee”或其他处理吗?谢谢! - AnneTheAgile
1
@AnneTheAgile,我的示例中的“piping”代表您不想看到其errlvl的命令。例如:任何一个或多个管道组合的'tee'、'grep'、'sed'等。这些管道命令通常用于从主命令的大型输出或日志输出中格式化或提取信息:因此,您更关心主命令(我在示例中称之为'might_fail')的errlevel,但是如果没有我的结构,则整个赋值将返回最后一个管道命令的errlvl,这在这里是无意义的。这样清楚吗? - vaab
1
command_might_fail | grep -v "line_pattern_to_exclude" || exit ${PIPESTATUS[0]},如果不是tee而是grep过滤。 - user1742529

13

所以我想贡献一个回答,类似于lesmana的,但我认为我的解决方案可能更简单,稍微更有优势,是一个纯Bourne shell的解决方案:

# You want to pipe command1 through command2:
exec 4>&1
exitstatus=`{ { command1; printf $? 1>&3; } | command2 1>&4; } 3>&1`
# $exitstatus now has command1's exit status.

我认为这最好从内部向外解释 - command1将执行并在标准输出(stdout)上打印其常规输出,然后一旦完成,printf将执行并在其stdout上打印icommand1的退出码,但是该stdout被重定向到文件描述符3。

当command1正在运行时,它的stdout被传输到command2(printf的输出永远不会传递给command2,因为我们将其发送到文件描述符3而不是1,这是管道读取的内容)。然后我们将command2的输出重定向到文件描述符4,以使它也保持离开文件描述符1 - 因为我们希望稍后留出一点文件描述符1的空间,因为我们将printf输出文件描述符3返回到文件描述符1 - 因为这就是命令替换(反引号)将捕获的内容,并且这就是将放置到变量中的内容。

魔法的最后一部分是我们作为单独命令所做的第一个exec 4>&1 - 它将文件描述符4打开为外部shell的stdout的副本。命令替换将捕获从其内部的命令角度写入标准输出的所有内容 - 但是由于command2的输出就像对于命令替换来说是将流到文件描述符4一样,命令替换不会捕获它 - 但是一旦它“脱离”命令替换,它实际上仍然流到脚本的整体文件描述符1。

exec 4>&1必须是单独的命令,因为许多常见的shell不喜欢您尝试在使用替换的“外部”命令内写入文件描述符。因此,这是最简单的可移植方法。)

你可以用一个不那么技术、更加玩味的方式来看待它,就好像命令的输出相互追逐:command1通过管道传输给command2,然后printf的输出跳过command2以便command2无法捕获,然后command2的输出跳过并从命令替换中出来,正好在printf及时抓住并被替换捕获,以便将其放置到变量中,而command2的输出则沿着正常的管道方式进入标准输出。

另外,据我所知,$?仍然包含管道中第二个命令的返回代码,因为变量赋值,命令替换和复合命令都有效地对其中的命令的返回代码透明,因此command2的返回状态应该被传播出去 - 这也是为什么我认为这可能比lesmana提出的解决方案要好一些。

根据lesmana提到的注意事项,command1可能最终使用文件描述符3或4,因此为了更加健壮,你可以这样做:

exec 4>&1
exitstatus=`{ { command1 3>&-; printf $? 1>&3; } 4>&- | command2 1>&4; } 3>&1`
exec 4>&-

请注意,我的示例中使用了复合命令,但是子shell(使用( )而不是{ }也可以工作,尽管可能效率较低)。

命令会继承从启动它们的进程中继承的文件描述符,因此整个第二行将继承文件描述符四,后跟3>&1的复合命令将继承文件描述符三。因此,4>&- 确保内部复合命令不会继承文件描述符四,3>&- 不会继承文件描述符三,因此command1获得了更“干净”和标准化的环境。您还可以将内部的4>&-移动到3>&-旁边,但我认为最好尽可能限制其范围。

我不确定有多少程序直接使用文件描述符三和四 - 我认为大多数时候程序使用返回未在使用中的文件描述符的系统调用,但有时代码会直接写入文件描述符3,我猜想(我可以想象一个程序检查文件描述符是否打开,如果打开,就使用它,否则根据情况采取不同的行动)。因此,对于通用情况,最好牢记后者并将其用于。


很好的解释! - selurvedu

11
(command | tee out.txt; exit ${PIPESTATUS[0]})

与 @cODAR 的答案不同,这个返回的是第一个命令的原始退出代码,而不仅仅是 0 表示成功和 127 表示失败。但是,正如 @Chaoran 指出的那样,您只需调用 ${PIPESTATUS[0]} 即可。然而,重要的是将所有内容放入括号中。

我喜欢这种方法。我们还可以执行 ( set +e ; command | tee ... ) 来防止脚本在 tee 失败且外部脚本已经设置了 set -e 的情况下退出。 - joeytwiddle
因此,( ... exit ){ ... return }更可取,因为尽管我们的子shell从父shell继承了set -e,但我们的set +e不会影响父shell。 - joeytwiddle

8

在 bash 之外,您可以执行以下操作:

bash -o pipefail  -c "command1 | tee output"

这在忍者脚本中非常有用,因为期望使用的 shell 是 /bin/sh


7
在Ubuntu和Debian中,您可以使用apt-get install moreutils命令进行安装。这个包含了一个被称为mispipe的实用程序,它将返回管道中第一个命令的退出状态。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接