我想在 Bash 中执行一个长时间运行的命令,并捕获它的退出状态和 tee 它的输出。
所以我这样做:
command | tee out.txt
ST=$?
问题是变量 ST 捕获了 tee
而不是命令的退出状态。我该如何解决这个问题?
请注意,命令长时间运行并将输出重定向到文件以稍后查看对我来说不是一个好的解决方案。
有一个名为 $PIPESTATUS
的内部 Bash 变量;它是一个数组,用于保存最后一个前台命令管道中每个命令的退出状态。
<command> | tee out.txt ; test ${PIPESTATUS[0]} -eq 0
或者另一种也适用于其他 shell(如 zsh)的替代方法是启用 pipefail:
set -o pipefail
...
由于语法稍有不同,第一个选项 not 与 zsh
一起使用。
愚蠢的解决方案:通过命名管道(mkfifo)连接它们。然后可以第二次运行该命令。
mkfifo pipe
tee out.txt < pipe &
command > pipe
echo $?
mkfifo
,如果我没记错的话,可能需要 mknod -p
。
mkfifo
似乎更便携pubs.opengroup.org/onlinepubs/9699919799
使用 bash 的 set -o pipefail
很有帮助
pipefail:管道的返回值是最后一个以非零状态退出的命令的状态,如果没有命令以非零状态退出,则为零
( set -o pipefail; command | tee out.txt ); ST=$?
set -o pipefail
,然后执行命令,然后立即执行 set +o pipefail
以取消设置该选项。
-o pipefail
,他会知道管道是否失败,但如果“command”和“tee”都失败,他会收到来自“tee”的退出代码。
有一个数组可以为您提供管道中每个命令的退出状态。
$ 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 特定功能或临时文件即可工作。奖励:最后退出状态实际上是退出状态,而不是文件中的某个字符串。
情况:
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 以获取详细说明以及不带子 shell 和一些警告的替代方案。
通过结合 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
VALUE=$(might_fail | piping)
,它不会在主 shell 中设置 PIPESTATUS,但会设置其错误级别。通过使用:VALUE=$(might_fail | piping; exit ${PIPESTATUS[0]})
我得到我想要的。
command_might_fail | grep -v "line_pattern_to_exclude" || exit ${PIPESTATUS[0]}
如果不是 tee 而是 grep 过滤
所以我想贡献一个像 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.
我认为最好从内到外解释 - 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 作为外部 shell 标准输出的副本。命令替换将从其内部命令的角度捕获标准输出上写入的任何内容 - 但由于 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>&-
请注意,我在示例中使用了复合命令,但子 shell(使用 ( )
而不是 { }
也可以,但可能效率较低。)
命令从启动它们的进程继承文件描述符,因此整个第二行将继承文件描述符 4,而后跟 3>&1
的复合命令将继承文件描述符 3。因此 4>&-
确保内部复合命令不会继承文件描述符 4,而 3>&-
不会继承文件描述符 3,因此 command1 获得了一个“更干净”、更标准的环境。您也可以将内部 4>&-
移到 3>&-
旁边,但我想为什么不尽可能限制它的范围。
我不确定事情直接使用文件描述符 3 和 4 的频率 - 我认为大多数时候程序使用返回未使用的文件描述符的系统调用,但有时代码直接写入文件描述符 3,我猜测(我可以想象一个程序检查文件描述符以查看它是否打开,如果打开则使用它,或者如果不是则相应地表现不同)。所以后者可能最好记住并用于通用情况。
在 Ubuntu 和 Debian 中,您可以apt-get install moreutils
。这包含一个名为 mispipe
的实用程序,它返回管道中第一个命令的退出状态。
(command | tee out.txt; exit ${PIPESTATUS[0]})
与@cODAR 的答案不同,它返回第一个命令的原始退出代码,而不仅仅是 0 表示成功,127 表示失败。但正如@Chaoran 指出的那样,您可以调用 ${PIPESTATUS[0]}
。然而,重要的是所有内容都放在括号中。
在 bash 之外,您可以执行以下操作:
bash -o pipefail -c "command1 | tee output"
例如,在 shell 应为 /bin/sh
的忍者脚本中,这很有用。
在普通 bash 中执行此操作的最简单方法是使用 process substitution 而不是管道。有几个不同之处,但它们对于您的用例可能并不重要:
运行管道时,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
正如我所说,管道表达式存在差异。该过程可能永远不会停止运行,除非它对管道关闭很敏感。特别是,它可能会不断将内容写入您的标准输出,这可能会造成混淆。
PIPESTATUS[@] 必须在管道命令返回后立即复制到数组中。任何对 PIPESTATUS[@] 的读取都会擦除内容。如果您计划检查所有管道命令的状态,请将其复制到另一个数组。 “美元?”与“${PIPESTATUS[@]}”的最后一个元素的值相同,读取它似乎会破坏“${PIPESTATUS[@]}”,但我还没有完全验证这一点。
declare -a PSA
cmd1 | cmd2 | cmd3
PSA=( "${PIPESTATUS[@]}" )
如果管道位于子外壳中,这将不起作用。有关该问题的解决方案,
请参阅bash pipestatus in backticked command?
纯壳解决方案:
% 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 的输出)。
基于@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
有时使用外部命令可能更简单、更清晰,而不是深入研究 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
根本不会退出,除非出现错误。它执行到第二个命令,所以它是执行返回的第二个命令。
不定期副业成功案例分享
exit ${PIPESTATUS[0]}
。