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

fly*_*ire 400 error-handling bash shell pipe

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

所以我这样做:

command | tee out.txt
ST=$?
Run Code Online (Sandbox Code Playgroud)

问题是变量ST捕获退出状态tee而不是命令.我怎么解决这个问题?

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

cod*_*dar 502

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

<command> | tee out.txt ; test ${PIPESTATUS[0]} -eq 0
Run Code Online (Sandbox Code Playgroud)

或者另一个也适用于其他shell(如zsh)的替代方法是启用pipefail:

set -o pipefail
...
Run Code Online (Sandbox Code Playgroud)

第一个选项并不能一起工作zsh,由于一点点不同的语法.

  • 这里有关于PIPESTATUS和Pipefail的例子有一个很好的解释:http://unix.stackexchange.com/a/73180/7453. (21认同)
  • 注意:$ PIPESTATUS [0]保存管道中第一个命令的退出状态,$ PIPESTATUS [1]保存第二个命令的退出状态,依此类推. (18认同)
  • 当然,我们必须记住这是特定于Bash的:如果我(例如)在我的Android设备上编写脚本以在BusyBox的"sh"实现上运行,或者在某些其他嵌入式平台上使用其他"sh"编写脚本变体,这不起作用. (17认同)
  • 对于那些关注不带引号的变量扩展的人:退出状态始终是无符号的8位整数[在Bash中](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). (4认同)
  • 您也可以使用`exit $ {PIPESTATUS [0]}`。 (2认同)

Fel*_*rez 140

使用bash set -o pipefail是有帮助的

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

  • 如果您不想修改整个脚本的pipefail设置,可以只在本地设置该选项:`(set -o pipefail; command | tee out.txt); ST = $?` (21认同)
  • @Jaan这将运行一个子shell.如果你想避免这种情况,可以执行`set -o pipefail`然后执行命令,然后立即执行`set + o pipefail`取消设置选项. (6认同)
  • 注意:问题发布者不想要管道的“一般退出代码”,他想要“命令”的返回代码。使用 `-o pipefail` 他会知道管道是否失败,但是如果 'command' 和 'tee' 都失败,他会收到来自 'tee' 的退出代码。 (2认同)

EFr*_*aim 119

愚蠢的解决方案:通过命名管道(mkfifo)连接它们.然后命令可以运行第二个.

 mkfifo pipe
 tee out.txt < pipe &
 command > pipe
 echo $?
Run Code Online (Sandbox Code Playgroud)

  • 这是这个问题的唯一答案,也适用于简单的**sh**Unix shell.谢谢! (17认同)
  • 虽然当你拥有bash额外功能的优势时bash的答案更加优雅,但这是更多的跨平台解决方案.这也是一般值得考虑的事情,因为每当你做一个长期运行的命令时,名称管道通常是最灵活的方式.值得注意的是,有些系统没有`mkfifo`,如果我没记错的话可能需要`mknod -p`. (9认同)
  • 为什么这个笨蛋? (3认同)
  • 有时在堆栈溢出时,有些答案你会赞成一百次,这样人们就会停止做其他没有意义的事情,这就是其中之一。谢谢你,先生。 (3认同)
  • `mkfifo` 似乎更便携 https://pubs.opengroup.org/onlinepubs/9699919799/ (3认同)
  • @DaveKennedy:愚蠢的"显而易见,不需要复杂的bash语法知识" (2认同)
  • 如果有人对 `mkfifo` 或 `mknod -p` 有问题:在我的例子中,创建管道文件的正确命令是 `mknod FILE_NAME p`。 (2认同)
  • 谢谢。它也可以与 Ksh 一起使用(实际上 Ksh 是用 Zsh 模拟的)。如何正确关闭管道?通过一个简单的 rm 管道? (2认同)

Ste*_*ini 35

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

$ 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
Run Code Online (Sandbox Code Playgroud)


les*_*ana 25

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

情况:

someprog | filter
Run Code Online (Sandbox Code Playgroud)

你想要退出状态someprog和输出filter.

这是我的解决方案:

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

echo $?
Run Code Online (Sandbox Code Playgroud)

请参阅我在unix.stackexchange.com上的相同问题的答案,以获取详细解释和替代方案,不带子壳和一些注意事项.


par*_*par 19

通过组合子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: $?"
Run Code Online (Sandbox Code Playgroud)

会给你:

return value: 1

  • 谢谢,这允许我使用构造:``VALUE = $(might_fail | piping)``它不会在主shell中设置PIPESTATUS但会设置其errorlevel.通过使用:``VALUE = $(might_fail | piping;退出$ {PIPESTATUS [0]})``我想要我想要的. (4认同)

mtr*_*eur 11

所以我想提供像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.
Run Code Online (Sandbox Code Playgroud)

我认为最好从内到外解释 - 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作为外壳的标准输出的副本.命令替换将从其中的命令的角度捕获在标准输出上写入的任何内容 - 但是因为命令替换涉及到command2的输出将转到文件描述符4,所以命令替换不捕获它 - 但是一旦它得到命令替换的"out"它实际上仍然会转到脚本的整个文件描述符1.

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

您可以用技术性较低且更有趣的方式查看它,就好像命令的输出相互跳跃:command1管道到command2,然后printf的输出跳过命令2,这样命令2就不会捕获它,然后命令2的输出跳过命令替换,就像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>&-
Run Code Online (Sandbox Code Playgroud)

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

命令从启动它们的进程继承文件描述符,因此整个第二行将继承文件描述符4,后面的复合命令3>&1将继承文件描述符3.因此,4>&-确保内部复合命令不会继承文件描述符四,并且3>&-不会继承文件描述符三,因此command1获得"更清洁",更标准的环境.你也可以移动内部4>&-旁边的3>&-,但我想为什么不尽可能地限制它的范围.

我不确定直接使用文件描述符三和四的情况 - 我认为大多数时候程序使用系统调用来返回当前未使用的文件描述符,但有时代码直接写入文件描述符3,猜测(我可以想象一个程序检查一个文件描述符,看看它是否打开,如果是,则使用它,如果不是则表现不同).所以后者可能最好记住并用于通用案例.


jak*_*b-r 7

(command | tee out.txt; exit ${PIPESTATUS[0]})
Run Code Online (Sandbox Code Playgroud)

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


Bry*_*sen 6

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


Ant*_*ama 6

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

bash -o pipefail  -c "command1 | tee output"
Run Code Online (Sandbox Code Playgroud)

例如,这在 ninja 脚本中很有用,其中 shell 应该是/bin/sh.


cla*_*cke 5

在普通 bash 中执行此操作的最简单方法是使用进程替换而不是管道。有一些差异,但它们对于您的用例来说可能并不重要:

  • 运行管道时,bash 会等待所有进程完成。
  • 向 bash 发送 Ctrl-C 会使其杀死管道中的所有进程,而不仅仅是主进程。
  • 选项pipefailPIPESTATUS变量与过程替换无关。
  • 可能更多

通过进程替换,bash 只是启动进程并忘记它,它甚至在jobs.

除了提到的差异之外,consumer < <(producer)本质producer | consumer上是等效的。

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

command > >(tee out.txt)
Run Code Online (Sandbox Code Playgroud)

例子:

$ { 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
Run Code Online (Sandbox Code Playgroud)

正如我所说,与管道表达式存在差异。该进程可能永远不会停止运行,除非它对管道关闭敏感。特别是,它可能会不断向您的标准输出写入内容,这可能会令人困惑。