Bash中的管道输出和捕获退出状态

我想执行Bash中长时间运行的命令,都捕获它的退出状态,并且发球它的输出。

所以我这样做:

command | tee out.txt
ST=$?

问题在于变量ST捕获了tee而不是命令的退出状态。 我该如何解决?

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


#1楼

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

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

#2楼

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

$ 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

#3楼

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

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

或者也可以与其他shell(例如zsh)一起使用的另一种替代方法是启用pipefail:

set -o pipefail
...

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


#4楼

此解决方案无需使用bash特定功能或临时文件即可工作。 奖励:最后,退出状态实际上是退出状态,而不是文件中的某些字符串。

情况:

someprog | filter

要从退出状态someprog和输出filter

这是我的解决方案:

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

echo $?

在unix.stackexchange.com上,对于相同的问题,请参见我的回答,以获取详细的说明以及不包含subshel​​l和一些警告的替代方法。


#5楼

通过将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


#6楼

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

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

如果管道在子壳中,则将无法使用。 为了解决这个问题,
在反引号命令中看到bash pipestatus?


#7楼

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

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


#8楼

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


#9楼

纯壳解决方案:

% 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

现在,第二catfalse代替:

% 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的输出)。


#10楼

因此,我想提供一个像莱斯曼纳的答案,但我认为我的也许是一个更简单,更有利的纯伯恩壳解决方案:

# 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第一个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>&-确保内部复合命令不会继承文件描述符4,而3>&-将不会继承文件描述符3,因此command1获得了一个“更干净”的标准环境。 你也可以移动内4>&-旁边3>&-但我想,为什么不只是限制它的范围尽可能地。

我不确定事情多久直接使用文件描述符3和4-我认为大多数时候程序都使用syscall来返回当前未使用的文件描述符,但有时代码会直接写入文件描述符3猜测(我可以想象一个程序检查文件描述符以查看它是否打开,如果打开则使用它,或者如果没有打开则相应地表现不同)。 因此,可能最好记住后者,并在通用情况下使用。


#11楼

基于@ 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

#12楼

在bash之外,您可以执行以下操作:

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

例如,在忍者脚本中,该外壳应该是/bin/sh这很有用。


#13楼

使用外部命令而不是深入研究bash的细节有时可能更简单明了。 管道 ,从最小的过程脚本语言execline ,以第二个命令的返回代码*退出,就像sh管道一样,但是与sh不同,它允许反转管道的方向,以便我们可以捕获管道的返回代码。生产者进程(以下全部位于sh命令行中,但execline安装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 pipeline具有相同的差异,与答案中使用的bash进程替代相同#43972501

*除非有错误,否则实际上pipeline根本不会退出。 它执行到第二个命令中,因此它是第二个执行返回的命令。


#14楼

在纯bash中执行此操作的最简单方法是使用进程替代而不是管道。 有几个区别,但是对于您的用例来说,它们可能并不重要:

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

通过流程替换,bash只是启动了流程而忘记了它,它甚至在jobs都不可见。

除了提及的差异, consumer < <(producer)producer | consumer 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

正如我所说的,管道表达式有一些区别。 除非对管道关闭敏感,否则该过程可能永远不会停止运行。 特别是,它可能不断将内容写入标准输出,这可能会造成混淆。


#15楼

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

与@cODAR的答案不同,这将返回第一个命令的原始退出代码,不仅返回0(成功)和127(失败)。 但是正如@Chaoran指出的那样,您可以仅调用${PIPESTATUS[0]} 。 但是,将所有内容放在方括号中非常重要。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值