2009-08-03 105 views
313

我想在Bash中执行长时间运行的命令,并且都捕获它的退出状态,并且它的输出为teeBash中的管道输出和捕获退出状态

所以我这样做:

command | tee out.txt 
ST=$? 

的问题是,可变ST捕获的tee退出状态,而不是命令。我该如何解决这个问题?

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

+1

[[“$ {PIPESTATUS [@]}”=〜[^ 0 \]]] && echo -e“Match - error found”|| echo -e“不匹配 - 全部好” 这将一次测试数组的所有值,并在返回的任何管道值不为零时给出错误消息。这是一个非常强大的通用解决方案,用于检测管道状况中的错误。 – 2015-03-31 19:08:47

+0

http://unix.stackexchange.com/questions/14270/get-exit-status-of-process-thats-piped-to-another – 2016-11-15 17:22:58

回答

408

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

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

或者另一种选择,其也可以与其他shell(如zsh中)将是使pipefail:

set -o pipefail 
... 

第一个选项确实zsh工作,由于一点点不同的语法。

87

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

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

这是这个问题的唯一答案,也适用于简单的** sh ** Unix shell。谢谢! – JamesThomasMoon1979 2014-09-30 04:08:13

+0

为什么这是愚蠢的? – 2016-03-02 02:14:15

+1

@DaveKennedy:愚蠢如“显而易见,不需要复杂的bash语法知识” – EFraim 2016-03-02 18:18:37

33

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

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

此解决方案不使用bash特定功能或临时文件。奖金:最后退出状态实际上是退出状态,而不是文件中的某个字符串。

现状:

someprog | filter 

要从someprog退出状态,并从filter输出。

这里是我的解决方案:

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

echo $? 

对于如何工作的详细解释和一些注意事项见my answer for the same question on unix.stackexchange.com

16

通过结合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: $?" 

会给你:

return value: 1

3

PIPESTATUS [@]必须在管道命令返回后立即复制到数组。 任何读取PIPESTATUS [@]将擦除内容。 如果您打算检查所有管道命令的状态,请将其复制到另一个阵列。 “$?”是与“$ {PIPESTATUS [@]}”的最后一个元素相同的值, 并阅读它似乎破坏了“$ {PIPESTATUS [@]}”,但我没有完全验证这一点。

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

如果管道位于子外壳中,这将不起作用。为了解决这个问题,
看到使用bash的set -o pipefail是有帮助的bash pipestatus in backticked command?

123

pipefail:管道的返回值是 最后命令的状态退出与非零状态, 或零,如果没有命令用非零状态

3

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

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的输出),则删除它。

7

,所以我想贡献像lesmana的答案,但我认为我也许是比较简单一点,稍微有利纯的Bourne外壳的解决方案:

# 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. 

我觉得这是最好的从里面解释出 - 命令1将执行并打印在标准输出上的常规输出(文件描述符1),那么一旦它的完成,printf的将其标准输出执行并打印icommand1的退出代码,但标准输出重定向到文件描述符3

虽然command1正在运行,它的stdout被传送到command2(printf的输出永远不会使它成为command2 beca使用我们发送它到文件描述符3而不是1,这是管道读取的内容)。然后我们将command2的输出重定向到文件描述符4,以便它也停留在文件描述符1之外 - 因为我们稍后需要文件描述符1空闲,因为我们会将文件描述符3上的printf输出返回到文件描述符1 - 因为这就是命令替换(反引号)将捕获的内容,并且这将被放入变量中。

神奇的最后一点是第一个exec 4>&1作为一个单独的命令 - 它打开文件描述符4作为外壳的标准输出的副本。命令替换将从其内部命令的角度捕获任何写在标准上的内容 - 但是由于命令替换所涉及的命令2的输出将转到文件描述符4,所以命令替换不会捕获它 - 但是一旦它得到“输出”的命令替换它仍然有效地去脚本的整个文件描述符1.

exec 4>&1必须是一个单独的命令,因为许多常见的shell不喜欢它,当你试图写一个文件描述符在一个命令替换里面,这是在使用替换的“外部”命令中打开的,所以这是最简单的便携方式)

你可以看看它的技术更少,更好玩办法,就像这些命令的输出相互跳跃一样:command1将命令转换为command2,然后printf的输出会跳过命令2,以使命令2不会捕获它,然后命令2的输出会跳过命令替换跳过,就像printf恰好及时地被替换捕获,以便它在变量中结束,并且command2的输出以正常的管道写入标准输出的方式进行。

另外,据我所知,$?仍将包含所述第二命令的返回代码在管,因为变量赋值,命令替换和化合物命令都是有效透明的,以在其内部的命令的返回代码,所以command2的返回状态应该被传播出去 - 而不必定义一个额外的函数,这就是为什么我认为这可能是一个比lesmana提出的更好的解决方案。

每告诫lesmana提到,它可能是命令1将在某个时候最终使用文件描述符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我猜(我可以想象一个程序检查一个文件描述符,看看它是否打开,如果是的话就使用它,或者如果不是则使用不同的行为)。所以后者可能最好记住并用于通用的情况。

1

基于@ brian-s-wilson的回答;这个庆典的辅助功能:这样

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 
2

外面的bash,你可以这样做:

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

这是有用的,例如在忍者脚本中,shell预计为/bin/sh

1

使用外部命令有时可能会更简单明了,而不是深入探讨bash的细节。 pipeline,来自最小进程脚本语言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管道如答案#43972501使用的bash进程取代相同的差异。

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

2

在纯bash中最简单的方法是使用process substitution而不是管道。有几个不同点,但它们可能对您的用例无关紧要:

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

随着进程替换,庆典才刚刚起步的过程,并忘掉它,它甚至不是在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 

正如我所说的,有从管表达的差异。除非它对管道关闭敏感,否则该过程可能永远不会停止运行。特别是,它可能会继续向标准输出写入内容,这可能会令人困惑。

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

与@cODAR的回答不同,它返回第一个命令的原始退出码,不仅成功为0,失败为127。但正如@Chaoran指出的,你可以拨打${PIPESTATUS[0]}。然而,所有内容都放在括号内是很重要的。