关于shell:Bash中的管道输出和捕获退出状态

Pipe output and capture exit status in Bash

我想在bash中执行一个长时间运行的命令,并捕获它的退出状态,并测试它的输出。

所以我这样做:

1
2
command | tee out.txt
ST=$?

问题在于,变量st捕获tee的退出状态,而不是命令状态。我怎么解决这个问题?

请注意,该命令运行时间很长,将输出重定向到文件以供以后查看对我来说不是一个好的解决方案。


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

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

或者另一个同样适用于其他外壳(如zsh)的替代方案是启用pipefail:

1
2
set -o pipefail
...

由于语法稍有不同,第一个选项不适用于zsh


使用bash的set -o pipefail有帮助

pipefail: the return value of a pipeline is the status of
the last command to exit with a non-zero status,
or zero if no command exited with a non-zero status


哑解决方案:通过命名管道(mkfifo)连接它们。然后可以再次运行该命令。

1
2
3
4
 mkfifo pipe
 tee out.txt < pipe &
 command > pipe
 echo $?


有一个数组为您提供管道中每个命令的退出状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
$ 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

此解决方案在不使用bash特定功能或临时文件的情况下工作。另外:最后,退出状态实际上是一个退出状态,而不是文件中的某个字符串。

情况:

1
someprog | filter

您需要someprog的退出状态和filter的输出。

这是我的解决方案:

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

echo $?

请参阅我在unix.stackexchange.com上对相同问题的回答,以获得详细的解释和没有子shell和一些警告的替代方案。


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

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

下面是一个例子:

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

会给你:

return value: 1


所以我想提出一个像莱斯马纳那样的答案,但我认为我的答案可能更简单一些,更有利一些,纯粹的Bourne-Shell解决方案:好的。

1
2
3
4
# 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(文件描述符1)上执行并打印其常规输出,然后一旦完成,printf将在stdout上执行并打印icommand1的退出代码,但stdout将重定向到文件描述符3。好的。

当command1运行时,它的stdout被管道传输到command2(printf的输出永远不会使它成为command2,因为我们将它发送到文件描述符3而不是管道读取的1)。然后,我们将command2的输出重定向到文件描述符4,这样它也不会出现在文件描述符1之外-因为我们希望文件描述符1稍后有一点空闲,因为我们将把文件描述符3上的printf输出恢复到文件描述符1中-因为这就是命令替换(backticks)将捕获的内容,而且s什么将被放入变量中。好的。

最后一点神奇的是,第一个exec 4>&1是作为一个单独的命令执行的—它打开文件描述符4作为外部shell stdout的副本。命令替换将从其内部命令的角度捕获标准上写入的内容-但是,由于command2的输出将要对描述符4进行文件替换,因此命令替换不会捕获它-但是,一旦它从命令替换中"退出",它实际上就是我将转到脚本的整体文件描述符1。好的。

(exec 4>&1必须是一个单独的命令,因为许多常见的shell不喜欢在使用替换的"外部"命令中打开的命令替换中写入文件描述符。所以这是最简单的可移植方法。)好的。

你可以用一种不那么技术性和更好玩的方式来看待它,就好像命令的输出是相互跳跃的:command1将管道连接到command2,然后printf的输出跳过command2,这样command2就不会捕获它,然后command2的输出跳过并跳出命令替换,就像printf及时到达以获取由替换捕获,以便它最终进入变量,command2的输出以其愉快的方式写入标准输出,就像在普通管道中一样。好的。

另外,据我所知,$?仍将包含管道中第二个命令的返回代码,因为变量分配、命令替换和复合命令对其内部命令的返回代码都是有效透明的,因此command2的返回状态应该传播出去,而不必定义一个额外的函数,这就是为什么我认为这可能是一个比莱斯曼提出的更好的解决方案。好的。

根据Lesmana提到的警告,command1可能在某个时候会使用文件描述符3或4,所以为了更健壮,您可以这样做:好的。

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

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

命令从启动它们的进程继承文件描述符,因此整个第二行将继承文件描述符4,后面跟着3>&1的复合命令将继承文件描述符3。因此,4>&-确保内部复合命令不会继承文件描述符4,3>&-也不会继承文件描述符3,因此command1得到一个"更干净、更标准的环境"。你也可以把内部的4>&-移到3>&-旁边,但我想为什么不尽可能地限制它的范围。好的。

我不确定文件描述符3和4直接使用的频率-我认为大多数时候程序使用的是系统调用,它们返回的不是当前使用的文件描述符,但有时代码直接写入文件描述符3,我猜(我可以想象一个程序检查文件描述符,看它是否打开,如果打开,使用它,或者behavin如果不是的话,那就不一样了。因此,后者可能最好记住并用于一般情况。好的。好啊。


在Ubuntu和Debian中,您可以使用ecx1(9)。它包含一个名为mispipe的实用程序,该实用程序返回管道中第一个命令的退出状态。


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

与@codar的答案不同,这将返回第一个命令的原始退出代码,成功时不仅返回0,失败时返回127。但正如@chauran所指出的,你可以直接打电话给${PIPESTATUS[0]}。但重要的是,所有的都放在括号中。


pipestatus[@]必须在pipe命令返回后立即复制到数组。任何对pipestatus[@]的读取都将擦除内容。如果计划检查所有管道命令的状态,请将其复制到另一个数组。"$?"与"$PipeStatus[@]"的最后一个元素的值相同,阅读它似乎会破坏"$pipestatus[@]",但我还没有完全验证这一点。

1
2
3
declare -a PSA  
cmd1 | cmd2 | cmd3  
PSA=("${PIPESTATUS[@]}" )

如果管道在子壳中,这将不起作用。为了解决这个问题,在backticked命令中看到bash pipestatus吗?


在普通bash中实现这一点的最简单方法是使用流程替换而不是管道。有几个不同之处,但对于您的用例来说,它们可能并不重要:

  • 运行管道时,bash会等待所有进程完成。
  • 将ctrl-c发送到bash会杀死管道的所有进程,而不仅仅是主进程。
  • pipefail选项和PIPESTATUS变量与过程替换无关。
  • 可能更多

通过过程替换,bash只是启动了过程并忽略了它,它甚至在jobs中都不可见。

除上述差异外,consumer < <(producer)producer | consumer基本上是等效的。

如果要翻转哪个是"主"进程,只需将命令和替换方向翻转到producer > >(consumer)。在你的情况下:

1
command > >(tee out.txt)

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ { echo"hello world"; false; } > >(tee out.txt)
hello world
$ echo $?
1
$ cat out.txt
hello world

$ echo"hello world"> >(tee out.txt)
hello world
$ echo $?
0
$ cat out.txt
hello world

正如我所说,管道表达式有区别。除非该过程对管道关闭敏感,否则它可能永远不会停止运行。尤其是,它可能会一直在给你的性传播疾病写东西,这可能会让人困惑。


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

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

这在忍者脚本中很有用,其中shell应该是/bin/sh


有时使用外部命令可能更简单、更清晰,而不是深入挖掘bash的细节。pipe line,从最小进程脚本语言execline,以第二个命令*的返回代码退出,就像sh管道一样,但与sh不同,它允许反转管道的方向,以便我们捕获生产者进程的返回代码(以下都在sh命令行,b安装了execline的UT):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
$ # using the full execline grammar with the execlineb parser:
$ execlineb -c 'pipeline { echo"hello world" } tee out.txt'
hello world
$ cat out.txt
hello world

$ # for these simple examples, one can forego the parser and just use"" as a separator
$ # traditional order
$ pipeline echo"hello world""" tee out.txt
hello world

$ #"write" order (second command writes rather than reads)
$ pipeline -w tee out.txt"" echo"hello world"
hello world

$ # pipeline execs into the second command, so that's the RC we get
$ pipeline -w tee out.txt"" false; echo $?
1

$ pipeline -w tee out.txt"" true; echo $?
0

$ # output and exit status
$ pipeline -w tee out.txt"" sh -c"echo 'hello world'; exit 42"; echo"RC: $?"
hello world
RC: 42
$ cat out.txt
hello world

使用pipeline与本地bash管道有相同的区别,这与答案43972501中使用的bash流程替换相同。

*实际上,除非出现错误,否则pipeline根本不会退出。它执行到第二个命令中,所以它是执行返回的第二个命令。


基于@brian-s-wilson的答案;此bash helper函数:

1
2
3
4
5
6
7
8
pipestatus() {
  local S=("${PIPESTATUS[@]}")

  if test -n"$*"
  then test"$*" ="${S[*]}"
  else ! [["${S[@]}" =~ [^0\ ] ]]
  fi
}

因此使用:

1:不好的事情必须成功,但它不应该产生输出;但我们希望看到它确实产生的输出

1
2
get_bad_things | grep '^'
pipeinfo 0 1 || return

2:所有管道必须成功

1
2
thing | something -q | thingy
pipeinfo || return

纯壳液:

1
2
3
4
5
6
% rm -f error.flag; echo hello world \
| (cat || echo"First command failed: $?">> error.flag) \
| (cat || echo"Second command failed: $?">> error.flag) \
| (cat || echo"Third command failed: $?">> error.flag) \
; test -s error.flag  && (echo Some command failed: ; cat error.flag)
hello world

现在第二个catfalse取代:

1
2
3
4
5
6
7
8
% rm -f error.flag; echo hello world \
| (cat || echo"First command failed: $?">> error.flag) \
| (false || echo"Second command failed: $?">> error.flag) \
| (cat || echo"Third command failed: $?">> error.flag) \
; test -s error.flag  && (echo Some command failed: ; cat error.flag)
Some command failed:
Second command failed: 1
First command failed: 141

请注意,第一只猫也会失败,因为它的stdout会关闭。在本例中,日志中失败命令的顺序是正确的,但不要依赖于它。

此方法允许为单个命令捕获stdout和stderr,这样您就可以在发生错误时将其转储到日志文件中,或者在没有错误时将其删除(如dd的输出)。