ChatGPT解决这个技术问题 Extra ChatGPT

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

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

所以我这样做:

command | tee out.txt
ST=$?

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

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

[[ "${PIPESTATUS[@]}" =~ [^0\ ] ]] && echo -e "匹配 - 发现错误" || echo -e "No match - all good" 这将一次测试数组的所有值,如果返回的任何管道值不为零,则会给出错误消息。这是一个非常强大的通用解决方案,用于检测管道情况下的错误。

s
sorin

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

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

或者另一种也适用于其他 shell(如 zsh)的替代方法是启用 pipefail:

set -o pipefail
...

由于语法稍有不同,第一个选项 notzsh 一起使用。


这里有一个很好的解释,其中包含 PIPESTATUS 和 Pipefail 的示例:unix.stackexchange.com/a/73180/7453
注意:$PIPESTATUS[0] 保存管道中第一个命令的退出状态,$PIPESTATUS[1] 保存第二个命令的退出状态,依此类推。
当然,我们必须记住这是特定于 Bash 的:如果我要(例如)编写一个脚本以在我的 Android 设备上的 BusyBox 的“sh”实现上运行,或者在使用其他一些“sh”的其他嵌入式平台上运行变体,这是行不通的。
对于那些关心未引用变量扩展的人:退出状态始终为无符号 8 位整数 in Bash,因此无需引用它。这通常在 Unix 下也成立,其中 exit status is defined to be 8-bit explicitly,并且即使 POSIX 本身也假定它是无符号的,例如在定义它的 logical negation 时。
您也可以使用 exit ${PIPESTATUS[0]}
j
jfs

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

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

这是这个问题中唯一适用于简单 sh Unix shell 的答案。谢谢!
@DaveKennedy:就像“很明显,不需要复杂的 bash 语法知识”一样愚蠢
虽然当您利用 bash 的额外功能时,bash 的答案会更加优雅,但这是更跨平台的解决方案。一般来说,这也是值得考虑的事情,因为每当您执行长时间运行的命令时,名称管道通常是最灵活的方式。值得注意的是,有些系统没有 mkfifo,如果我没记错的话,可能需要 mknod -p
有时在堆栈溢出时,有一些答案你会赞成一百次,这样人们就会停止做其他没有意义的事情,这就是其中之一。谢谢你,先生。
M
Michael Anderson

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

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


如果您不想修改整个脚本的 pipefail 设置,您可以仅在本地设置该选项:( set -o pipefail; command | tee out.txt ); ST=$?
@Jaan 这将运行一个子shell。如果您想避免这种情况,您可以先执行 set -o pipefail,然后执行命令,然后立即执行 set +o pipefail 以取消设置该选项。
注意:问题发布者不想要管道的“一般退出代码”,他想要“命令”的返回代码。使用 -o pipefail,他会知道管道是否失败,但如果“command”和“tee”都失败,他会收到来自“tee”的退出代码。
@LinusArver 不会清除退出代码,因为它是一个成功的命令?
L
Lesmana

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

$ 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

L
Lesmana

此解决方案无需使用 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 和一些警告的替代方案。


p
par

通过结合 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]}) 我得到我想要的。
@vaab,该语法看起来非常好,但我对“管道”在您的上下文中的含义感到困惑?这只是一个人会做'tee'或对may_fail的输出进行任何处理的地方吗?泰!
在我的示例中,@AnneTheAgile 'piping' 代表您不想看到 errlvl 的命令。例如:'tee'、'grep'、'sed'、......其中一个或任何管道组合,这些管道命令用于格式化或从主的较大输出或日志输出中提取信息并不少见命令:然后您对主命令的 errlevel 更感兴趣(我在示例中称为“might_fail”),但没有我的构造,整个分配将返回最后一个管道命令的 errlvl,这在这里毫无意义。这更清楚了吗?
command_might_fail | grep -v "line_pattern_to_exclude" || exit ${PIPESTATUS[0]} 如果不是 tee 而是 grep 过滤
m
mtraceur

所以我想贡献一个像 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,我猜测(我可以想象一个程序检查文件描述符以查看它是否打开,如果打开则使用它,或者如果不是则相应地表现不同)。所以后者可能最好记住并用于通用情况。


很好的解释!
B
Bryan Larsen

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


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

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


A
Anthony Scemama

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

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

例如,在 shell 应为 /bin/sh 的忍者脚本中,这很有用。


c
clacke

在普通 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

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


这是我的首选解决方案。
C
Community

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

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

如果管道位于子外壳中,这将不起作用。有关该问题的解决方案,
请参阅bash pipestatus in backticked command?


C
Coroos

纯壳解决方案:

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


S
Sam Liddicott

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

c
clacke

有时使用外部命令可能更简单、更清晰,而不是深入研究 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 根本不会退出,除非出现错误。它执行到第二个命令,所以它是执行返回的第二个命令。