我想执行Bash中长时间运行的命令,都捕获它的退出状态,并且发球它的输出。
所以我这样做:
command | tee out.txt
ST=$?
问题在于变量ST捕获了tee
命令而不是命令的退出状态。我该如何解决?
请注意,该命令运行时间很长,将输出重定向到文件以供以后查看对我来说不是一个好的解决方案。
我想执行Bash中长时间运行的命令,都捕获它的退出状态,并且发球它的输出。
所以我这样做:
command | tee out.txt
ST=$?
问题在于变量ST捕获了tee
命令而不是命令的退出状态。我该如何解决?
请注意,该命令运行时间很长,将输出重定向到文件以供以后查看对我来说不是一个好的解决方案。
Answers:
有一个内部Bash变量称为$PIPESTATUS
;它是一个数组,用于保存最后一个命令前台管道中每个命令的退出状态。
<command> | tee out.txt ; test ${PIPESTATUS[0]} -eq 0
或者也可以与其他shell(如zsh)一起使用的另一种替代方法是启用pipefail:
set -o pipefail
...
由于语法略有不同,第一个选项无法使用zsh
。
exit ${PIPESTATUS[0]}
。
使用bash set -o pipefail
很有帮助
pipefail:管道的返回值是最后一个以非零状态退出的命令的状态,如果没有命令以非零状态退出,则返回零
( set -o pipefail; command | tee out.txt ); ST=$?
set -o pipefail
先执行该命令,然后执行,然后立即set +o pipefail
取消设置该选项。
-o pipefail
如果管道出现故障,他会知道,但如果两个“命令”和“三通”失败,他会收到来自“三通”的退出代码。
哑巴解决方案:通过命名管道(mkfifo)连接它们。然后可以第二次运行该命令。
mkfifo pipe
tee out.txt < pipe &
command > pipe
echo $?
mkfifo
,mknod -p
如果我没记错的话,可能会需要。
mkfifo
或有问题mknod -p
:在我的情况下,创建管道文件的正确命令是mknod FILE_NAME p
。
有一个数组可以为您提供管道中每个命令的退出状态。
$ 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 $?
在unix.stackexchange.com上,对于相同的问题,请参见我的回答,以获取详细的说明以及不包含subshell和一些警告的替代方法。
通过在子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: $?"
会给你:
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过滤
因此,我想提供一个像莱斯曼娜的答案,但我认为我的也许是一个更简单,更有利的纯伯恩壳解决方案:
# 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作为外部外壳的stdout的副本。从命令内部的角度来看,命令替换将捕获标准上写的所有内容-但由于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>&-
请注意,我在示例中使用了复合命令,但是使用了子外壳程序(使用( )
代替{ }
也会起作用,尽管可能效率较低。)
命令从启动它们的进程中继承文件描述符,因此整行第二行将继承文件描述符4,随后的复合命令3>&1
将继承文件描述符3。因此,请4>&-
确保内部复合命令不会继承文件描述符四个,并且3>&-
不会继承文件描述符三个,以便command1获得一个“更干净”的更标准的环境。您还可以将内部移动到4>&-
旁边3>&-
,但我认为为什么不尽可能限制其范围。
我不确定事情多久直接使用文件描述符3和4-我认为大多数时候程序都使用syscall来返回当前未使用的文件描述符,但有时代码会直接写入文件描述符3猜测(我可以想象一个程序检查文件描述符以查看它是否打开,如果打开则使用它,或者如果没有打开则相应地表现不同)。因此,可能最好记住后者,并在通用情况下使用。
在Ubuntu和Debian中,您可以apt-get install moreutils
。它包含一个名为的实用程序mispipe
,该实用程序返回管道中第一个命令的退出状态。
在bash之外,您可以执行以下操作:
bash -o pipefail -c "command1 | tee output"
例如,在预期使用shell的忍者脚本中,这很有用/bin/sh
。
必须在pipe命令返回后立即将PIPESTATUS [@]复制到数组。 任何读取PIPESTATUS [@]的操作都会擦除其中的内容。如果计划检查所有管道命令的状态,请将其复制到另一个阵列。“ $?” 与“ $ {PIPESTATUS [@]}”的最后一个元素的值相同,并且读取该值似乎会破坏“ $ {PIPESTATUS [@]}”,但我尚未对此进行绝对验证。
declare -a PSA
cmd1 | cmd2 | cmd3
PSA=( "${PIPESTATUS[@]}" )
如果管道在子壳中,则将无法使用。有关该问题的解决方案,
请参见反引号命令中的bash pipestatus?
在纯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
正如我所说的,管道表达式有一些区别。除非对管道关闭敏感,否则该过程可能永远不会停止运行。特别是,它可能不断将内容写入标准输出,这可能会造成混淆。
纯壳解决方案:
% 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的细节有时可能更简单明了。管道,从最小的过程脚本语言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管道具有相同的差异,与答案中使用的bash进程替代相同#43972501。
* pipeline
除非有错误,否则实际上根本不会退出。它执行到第二个命令中,因此它是第二个执行返回的命令。