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

所以我这样做:

command | tee out.txt
ST=$?

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

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


当前回答

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

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

或者另一种也适用于其他shell(如zsh)的选择是启用pipefail:

set -o pipefail
...

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

其他回答

基于@brian-s-wilson的回答;这个bash helper函数:

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

所以我想提供一个类似lesmana的答案,但我认为我的答案可能更简单,更有优势的纯伯恩壳解决方案:

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

While command1 is running, its stdout is being piped to command2 (printf's output never makes it to command2 because we send it to file descriptor 3 instead of 1, which is what the pipe reads). Then we redirect command2's output to file descriptor 4, so that it also stays out of file descriptor 1 - because we want file descriptor 1 free for a little bit later, because we will bring the printf output on file descriptor 3 back down into file descriptor 1 - because that's what the command substitution (the backticks), will capture and that's what will get placed into the variable.

The final bit of magic is that first exec 4>&1 we did as a separate command - it opens file descriptor 4 as a copy of the external shell's stdout. Command substitution will capture whatever is written on standard out from the perspective of the commands inside it - but since command2's output is going to file descriptor 4 as far as the command substitution is concerned, the command substitution doesn't capture it - however once it gets "out" of the command substitution it is effectively still going to the script's overall file descriptor 1.

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

You can look at it in a less technical and more playful way, as if the outputs of the commands are leapfrogging each other: command1 pipes to command2, then the printf's output jumps over command 2 so that command2 doesn't catch it, and then command 2's output jumps over and out of the command substitution just as printf lands just in time to get captured by the substitution so that it ends up in the variable, and command2's output goes on its merry way being written to the standard output, just as in a normal pipe.

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

根据lesmana提到的警告,command1可能在某些时候最终会使用文件描述符3或4,所以为了更健壮,你会这样做:

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

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

Commands inherit file descriptors from the process that launches them, so the entire second line will inherit file descriptor four, and the compound command followed by 3>&1 will inherit the file descriptor three. So the 4>&- makes sure that the inner compound command will not inherit file descriptor four, and the 3>&- will not inherit file descriptor three, so command1 gets a 'cleaner', more standard environment. You could also move the inner 4>&- next to the 3>&-, but I figure why not just limit its scope as much as possible.

我不确定直接使用文件描述符3和4的频率有多高——我认为大多数时候程序使用返回当前未使用的文件描述符的系统调用,但有时代码直接写入文件描述符3,我猜(我可以想象一个程序检查一个文件描述符,看看它是否打开,如果打开就使用它,如果没有打开则相应的行为不同)。因此,后者可能是最好记住并用于通用情况。

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

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

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

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

或者另一种也适用于其他shell(如zsh)的选择是启用pipefail:

set -o pipefail
...

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

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

命令| tee;退出${PIPESTATUS[0]}

这里有一个例子:

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

会给你:

返回值:1