Bash中的管道输出和捕获退出状态


420

我想执行Bash中长时间运行的命令,都捕获它的退出状态,并且发球它的输出。

所以我这样做:

command | tee out.txt
ST=$?

问题在于变量ST捕获了tee命令而不是命令的退出状态。我该如何解决?

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


1
[[“” $ {PIPESTATUS [@]}“ =〜[^ 0 \]]] && echo -e”匹配-发现错误“ || echo -e“无匹配项-全部良好”这将立即测试数组的所有值,并且如果返回的任何管道值都不为零,则会给出错误消息。这是一种非常健壮的通用解决方案,用于在管道情况下检测错误。
布赖恩·威尔逊

Answers:


519

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

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

或者也可以与其他shell(如zsh)一起使用的另一种替代方法是启用pipefail:

set -o pipefail
...

由于语法略有不同,第一个选项无法使用zsh


21
此处有关于PIPESTATUS和Pipefail的示例的很好的解释:unix.stackexchange.com/a/73180/7453
slm

18
注意:$ PIPESTATUS [0]保存管道中第一个命令的退出状态,$ PIPESTATUS [1]保存第二个命令的退出状态,依此类推。
simpleuser 2013年

18
当然,我们必须记住这是特定于Bash的:例如,如果我要编写脚本以在Android设备上的BusyBox的“ sh”实现上运行,或者在使用其他“ sh”的其他嵌入式平台上运行变体,这是行不通的。
Asfand Qazi 2014年

4
对于那些关心未加引号的变量扩展的人:Bash中的退出状态始终是无符号的8位整数,因此不需要将其引用。通常,这在Unix下也适用,退出状态被明确定义为8位,并且即使是POSIX本身,例如在定义其逻辑否定时,它也假定是未签名的。
Palec

3
您也可以使用exit ${PIPESTATUS[0]}
Chaoran

142

使用bash set -o pipefail很有帮助

pipefail:管道的返回值是最后一个以非零状态退出的命令的状态,如果没有命令以非零状态退出,则返回零


23
如果您不想修改整个脚本的pipefail设置,则只能在本地设置该选项:( set -o pipefail; command | tee out.txt ); ST=$?
Jaan 2015年

7
@Jaan这将运行一个子shell。如果要避免这种情况,可以set -o pipefail先执行该命令,然后执行,然后立即set +o pipefail取消设置该选项。
Linus Arver

2
注意:问题发布者不需要管道的“通用退出代码”,他需要返回“命令”的代码。随着-o pipefail如果管道出现故障,他会知道,但如果两个“命令”和“三通”失败,他会收到来自“三通”的退出代码。
t0r0X

@LinusArver不会清除退出代码,因为它是成功的命令?
carlin.scott

126

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

 mkfifo pipe
 tee out.txt < pipe &
 command > pipe
 echo $?

20
这是该问题中唯一适用于简单sh Unix shell的答案。谢谢!
JamesThomasMoon1979年

3
@DaveKennedy:哑巴如“显而易见,不需要复杂的bash语法知识”
EFraim

10
当您利用bash的附加功能时,bash的答案更为优雅,但这是跨平台的解决方案。一般而言,这也是值得考虑的事情,因为任何时候执行长时间运行的命令时,名称管道通常是最灵活的方式。值得注意的是,有些系统没有mkfifomknod -p如果我没记错的话,可能会需要。
哈拉维克

3
有时,在堆栈溢出时,您会回答一百次,因此人们会停止做其他毫无意义的事情,这就是其中之一。谢谢你,先生。
丹·蔡斯

1
如果某人mkfifo或有问题mknod -p:在我的情况下,创建管道文件的正确命令是mknod FILE_NAME p
卡罗尔·吉尔

36

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

$ 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

26

此解决方案无需使用bash特定功能或临时文件即可工作。奖励:最后,退出状态实际上是退出状态,而不是文件中的某些字符串。

情况:

someprog | filter

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

这是我的解决方案:

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

echo $?

在unix.stackexchange.com上,对于相同的问题,请参见我的回答,以获取详细的说明以及不包含subshel​​l和一些警告的替代方法。


20

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

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

这是一个例子:

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

会给你:

return value: 1


4
谢谢,这使我可以使用结构:VALUE=$(might_fail | piping)不会在主shell中设置PIPESTATUS,但会设置其错误级别。通过使用:VALUE=$(might_fail | piping; exit ${PIPESTATUS[0]})我想要我想要的。
vaab 2014年

@vaab,该语法看起来非常好,但是我对“管道”在您的上下文中的含义感到困惑?那是在哪里进行“ tee”或对may_fail输出进行任何处理吗?ty!
AnneTheAgile 2014年

1
我的示例中的@AnneTheAgile'piping'代表您不希望从中看到errlvl的命令。例如:'tee','grep','sed',...的其中之一或任何管道组合。这些管道命令用于格式化或提取主管道较大输出或日志输出中的信息并不少见命令:您对主命令的错误级别(在我的示例中称为“ might_fail”)更加感兴趣,但是如果没有我的构造,整个分配将返回最后一个管道命令的错误errlvl,在这里是没有意义的。这更清楚吗?
vaab 2014年

command_might_fail | grep -v "line_pattern_to_exclude" || exit ${PIPESTATUS[0]}如果不是tee而是grep过滤
user1742529

12

因此,我想提供一个像莱斯曼娜的答案,但我认为我的也许是一个更简单,更有利的纯伯恩壳解决方案:

# 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-因为这是命令替换(反引号)将捕获的内容,因此将其放入变量中。

魔术的最后一点是 exec 4>&1我们作为一个单独的命令进行了操作-它打开文件描述符4作为外部外壳的stdout的副本。从命令内部的角度来看,命令替换将捕获标准上写的所有内容-但由于command2的输出就命令替换而言将进入文件描述符4,因此命令替换不会捕获它-但是一旦替换从命令替换中“退出”后,它实际上仍然是脚本的整体文件描述符1。

(该exec 4>&1命令必须是一个单独的命令,因为当您尝试在命令替换中写入文件描述符时,许多常见的shell都不喜欢它,该命令在使用替换的“外部”命令中打开。因此,这是最简单的便携式方法。)

您可以用一种不太技术性且更有趣的方式来查看它,就像命令的输出彼此跳跃一样:command1通过管道传递到command2,然后printf的输出会跳过命令2,以便command2不会捕获它,然后命令2的输出跳出命令替换,就像printf恰好及时被替换捕获一样,以便它最终出现在变量中,而命令2的输出则以一种很快乐的方式写入标准输出,就像在普通管道中。

而且,据我所知,$?它将仍然在管道中包含第二个命令的返回代码,因为变量分配,命令替换和复合命令对于它们内部的命令的返回代码都是有效的透明的,因此返回状态为command2应该被传播出去-这并且不必定义其他功能,这就是为什么我认为这可能比lesmana提出的解决方案更好。

lesmana指出,在某种程度上,command1可能最终会使用文件描述符3或4,因此,为了更加健壮,您可以这样做:

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

请注意,我在示例中使用了复合命令,但是使用了子外壳程序(使用( )代替{ }也会起作用,尽管可能效率较低。)

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

我不确定事情多久直接使用文件描述符3和4-我认为大多数时候程序都使用syscall来返回当前未使用的文件描述符,但有时代码会直接写入文件描述符3猜测(我可以想象一个程序检查文件描述符以查看它是否打开,如果打开则使用它,或者如果没有打开则相应地表现不同)。因此,可能最好记住后者,并在通用情况下使用。


很好的解释!
selurvedu

6

在Ubuntu和Debian中,您可以apt-get install moreutils。它包含一个名为的实用程序mispipe,该实用程序返回管道中第一个命令的退出状态。


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

与@cODAR的答案不同,这将返回第一个命令的原始退出代码,不仅返回0(成功)和127(失败)。但是正如@Chaoran指出的,您可以致电${PIPESTATUS[0]}。但是,将所有内容放在方括号中非常重要。



3

必须在pipe命令返回后立即将PIPESTATUS [@]复制到数组。 任何读取PIPESTATUS [@]的操作都会擦除其中的内容。如果计划检查所有管道命令的状态,请将其复制到另一个阵列。“ $?” 与“ $ {PIPESTATUS [@]}”的最后一个元素的值相同,并且读取该值似乎会破坏“ $ {PIPESTATUS [@]}”,但我尚未对此进行绝对验证。

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

如果管道在子壳中,则将无法使用。有关该问题的解决方案,
请参见反引号命令中的bash pipestatus?


3

在纯bash中执行此操作的最简单方法是使用进程替代而不是管道。有几个区别,但是对于您的用例来说,它们可能并不重要:

  • 在运行管道时,bash等待直到所有进程完成。
  • 将Ctrl-C发送到bash使其杀死管道的所有进程,而不仅仅是主要进程。
  • pipefail选项和PIPESTATUS变量无关的过程替代。
  • 可能更多

通过流程替换,bash只是启动了流程而忘记了它,它甚至在 jobs

提到分歧放在一边,consumer < <(producer)producer | consumer基本上是等效的。

如果要翻转哪一个是“主要”过程,只需将命令和替换方向翻转到即可producer > >(consumer)。在您的情况下:

command > >(tee out.txt)

例:

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

正如我所说的,管道表达式有一些区别。除非对管道关闭敏感,否则该过程可能永远不会停止运行。特别是,它可能不断将内容写入标准输出,这可能会造成混淆。


1

纯壳解决方案:

% 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

现在,第二个cat替换为false

% 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和stderr,因此,如果发生错误,则可以将其也转储到日志文件中;如果没有错误,则可以将其删除(例如dd的输出)。


1

基于@ brian-s-wilson的答案;这个bash辅助函数:

pipestatus() {
  local S=("${PIPESTATUS[@]}")

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

因此使用:

1:get_bad_things必须成功,但是不产生任何输出;但我们希望看到它确实产生的输出

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

2:所有管道必须成功

thing | something -q | thingy
pipeinfo || return

1

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

$ # 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管道具有相同的差异,与答案中使用的bash进程替代相同#43972501

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.