我们可以将 $PIPESTATUS 与 tee (或 pee)命令一起使用吗?

我们可以将 $PIPESTATUS 与 tee (或 pee)命令一起使用吗?

在我的 bash 脚本中,我经常使用管道,并且想知道管道的哪个阶段导致了错误的问题。此类片段的基本结构是:

#!/bin/bash

ProduceCommand 2>/dev/null | ConsumeCommand >/dev/null 2>&1
PipeErrors=("${PIPESTATUS[@]}")
[[ "${PipeErrors[0]}" -eq '0' ]] || { HandleErrorInProduceCommand; }
[[ "${PipeErrors[1]}" -eq '0' ]] || { HandleErrorInConsumeCommand; }

tee现在(有趣的是,这是第一次)我所处的情况是,如果我可以使用或 ,那就太好了pee。但是$PIPESTATUS使用这些命令时会发生什么呢?例如:

#!/bin/bash

ProduceCommand 2>/dev/null | tee >(ConsumeCommand1) >(ConsumeCommand2) >/dev/null 2>&1
PipeErrors=("${PIPESTATUS[@]}")

或者

#!/bin/bash

ProduceCommand 2>/dev/null | pee ConsumeCommand1 ConsumeCommand2 2>/dev/null
PipeErrors=("${PIPESTATUS[@]}")

我相信这两种情况都${PipeErrors[0]}反映了 的错误状态ProduceCommand。此外,假设 分别反映或本身${PipeErrors[1]}的错误状态是合乎逻辑的。teepee

但这至少让我陷入两个理解问题:

  1. teeor的错误状态(返回值)是什么pee?我在手册页中没有找到关于这一点的准确陈述。如果其中一个消耗命令失败,它们是否返回硬编码的错误状态,或者它们是否以某种方式中继消耗命令的错误状态(例如ssh)?如果是前者,我们如何找出哪个消费命令是罪魁祸首?如果是后者,则转发哪种错误状态?仅仅是先失败的命令吗?

  2. AFAIK,bash 或teeorpee命令本身分别在内部使用管道(fifos)将 的输出获取ProduceCommand到消费命令。这意味着我们有一个管道,其(第一个且在本例中是唯一的)接收侧是管道本身。这不应该影响$PipeErrors上面的示例代码,但我真的不确定。

有人可以解释一下吗?

答案1

错误状态(返回值)是什么tee

当它能够将所有数据复制到所有输出文件时,它为 0;如果不能,则为 >0。请参阅规格。这GNU coreutils 实现oftee有额外的选项可以在写入时忽略错误管道(与用于实现的那些一样>(...)):

$ seq 1024 | tee >(false) >/dev/null; echo $?
141
$ seq 1024 | tee -p >(false) >/dev/null; echo $?
0

没有选项可以知道哪个输出失败(如果有)[1]。


但您的问题似乎是关于>(..)进程替换中运行的命令的退出状态是否以任何方式反映在 中PIPESTATUS,或者是否可能以任何方式体现在PIPESTATUS.

答案是

首先,请注意,它们>(...)更像是... &后台命令,而不是...|...管道命令。在这样的片段中:

... | tee >(cmd ...) | ...; echo ${PIPESTATUS[@]}

无法保证cmd在您运行时已经完成echo ${PIPESTATUS[@]}

但它们并不完全一样,...&因为您不能wait获取它们,也无法从中获取它们的状态$!- 除了在某些有限的情况下不是包括它们与tee其他外部命令的使用:

$ bash -c 'echo 1 | tee >(sleep 2; sed s/1/2/); wait; echo DONE'
1
DONE
$
<after two seconds>
2

正如您所看到的,主 shelltee和主 shell 早在来自 的命令之前就已完成>(...)

[1] 类似的命令pee正在运行输出“子命令”本身(并等待它们完成)可以变得更聪明,并在其退出状态中反映出哪些子命令失败了(例如,为第一个子命令设置位 1,为第二个子命令设置位 2,依此类推,最多 8 个子命令),但也没有这样做。

答案2

你总是可以这样做:

{
  {
    ProduceCommand 2>/dev/null 3>&- ||
      HandleErrorInProduceCommand >&3 3>&-
  } |
    tee >(
      ConsumeCommand1 3>&- ||
        HandleErrorInConsumer1 >&3 3>&-
    ) >(
      ConsumeCommand2 3>&- ||
        HandleErrorInConsumer2 >&3 3>&-
    ) > /dev/null
} 3>&1

它处理启动生产者和消费者的每个子 shell 中的错误。

我们将 stdout 复制到 fd 3 上,因此可以恢复错误处理程序的原始 stdout,因为我们不希望它们的输出(如果有)通过管道。

如果您希望错误处理程序在主 shell 进程中运行(例如,它可以退出它),您可以让这些子 shell 通过某些命令替换管道将退出状态传递给父 shell:

 producer_status=-1
consumer1_status=-1
consumer2_status=-1
{
  eval "$(
    {
      {
        ProduceCommand 2>/dev/null 4>&-
        echo "producer_status=$?" >&4
      } | tee >(
        ConsumeCommand1 4>&-
        echo "consumer1_status=$?" >&4
      ) >(
        ConsumeCommand2 4>&-
        echo "consumer2_status=$?" >&4
      )
    } 4>&1 >&3 3>&-
  )"
} 3>&1

[ "$producer_status"  -eq 0 ] || HandleErrorInProduceCommand
[ "$consumer1_status" -eq 0 ] || HandleErrorInConsumer1
[ "$consumer2_status" -eq 0 ] || HandleErrorInConsumer2

这可以避免$PIPESTATUSbashism,或者您可以避免>(...)kshism 并用普通管道替换它们:

{
  ProduceCommand 2>/dev/null |
    {
      tee /dev/fd/4 |
        ConsumeCommand1 4>&-
    } 4>&1 >&3 3>&- |
      ConsumeCommand2 3>&-
} 3>&1
 producer_status=${PIPESTATUS[0]}
consumer1_status=${PIPESTATUS[1]}
consumer2_status=${PIPESTATUS[2]}

[ "$producer_status"  -eq 0 ] || HandleErrorInProduceCommand
[ "$consumer1_status" -eq 0 ] || HandleErrorInConsumer1
[ "$consumer2_status" -eq 0 ] || HandleErrorInConsumer2

或者将这两种方法结合起来,然后获得标准sh语法,并且还可以访问tee的退出状态作为奖励。

 producer_status=-1
      tee_status=-1
consumer1_status=-1
consumer2_status=-1

{
  eval "$(
    {
      {
        ProduceCommand 2>/dev/null 4>&-
        echo "producer_status=$?" >&4
      } 3>&- |
        {
          {
            tee /dev/fd/5 4>&-
            echo "tee_status=$?" >&4
          } |
            ConsumeCommand1 4>&-
          echo "consumer1_status=$?" >&4
        } 5>&1 >&3 3>&- |
        ConsumeCommand2 >&3 3>&- 4>&- 
        echo "consumer2_status=$?" >&4
    } 4>&1
  )"
} 3>&1

[ "$producer_status"  -eq 0 ] || HandleErrorInProduceCommand
[ "$tee_status"       -eq 0 ] || HandleErrorInTee
[ "$consumer1_status" -eq 0 ] || HandleErrorInConsumer1
[ "$consumer2_status" -eq 0 ] || HandleErrorInConsumer2

请注意,tee如果其中一个进程在未读取所有输入的情况下退出,则可能会因 SIGPIPE 而死亡,这意味着另一个进程也可能会丢失某些输入。因此检查其退出状态可能也很重要。

正如 @UncleBilly 已经指出的,通过 的 GNU 实现tee,可以使用该-p选项来解决这个问题(其中tee忽略 SIGPIPE 信号,并在管道损坏时停止尝试向管道写入更多数据)。

对于其他实现,您可以替换tee ...(trap '' PIPE; exec tee ...)获得类似的行为(即使tee没有中止,也可能会出现一些有关损坏管道的错误消息)。

相关内容