我想在Bash中执行一个长时间运行的命令,并捕获它的退出状态,并输出它的输出。
所以我这样做:
command | tee out.txt
ST=$?
问题是变量ST捕获tee的退出状态,而不是命令的退出状态。我怎么解决这个问题?
注意,该命令长时间运行,将输出重定向到文件以供以后查看,这对我来说不是一个好的解决方案。
我想在Bash中执行一个长时间运行的命令,并捕获它的退出状态,并输出它的输出。
所以我这样做:
command | tee out.txt
ST=$?
问题是变量ST捕获tee的退出状态,而不是命令的退出状态。我怎么解决这个问题?
注意,该命令长时间运行,将输出重定向到文件以供以后查看,这对我来说不是一个好的解决方案。
当前回答
这个解决方案不需要使用bash特定的特性或临时文件。好处:最后,退出状态实际上是一个退出状态,而不是文件中的某个字符串。
情境:
someprog | filter
您需要someprog的退出状态和filter的输出。
以下是我的解决方案:
((((someprog; echo $? >&3) | filter >&4) 3>&1) | (read xs; exit $xs)) 4>&1
echo $?
请参阅我在unix.stackexchange.com上对同一问题的回答,以获得详细的解释和没有子shell和一些注意事项的替代方案。
其他回答
所以我想提供一个类似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之外,您可以执行以下操作:
bash -o pipefail -c "command1 | tee output"
这很有用,例如在ninja脚本中,shell被期望为/bin/sh。
在Ubuntu和Debian中,你可以apt-get install moreutils。这包含一个名为mispipe的实用程序,它返回管道中第一个命令的退出状态。
有一个数组为您提供管道中每个命令的退出状态。
$ 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的细节更简单、更清晰。管道,来自最小的进程脚本语言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
使用管道与原生bash管道的区别与答案#43972501中使用的bash进程替换相同。
*实际上管道不退出,除非有一个错误。它执行到第二个命令中,所以是第二个命令执行返回。