在我的 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]}
的错误状态是合乎逻辑的。tee
pee
但这至少让我陷入两个理解问题:
tee
or的错误状态(返回值)是什么pee
?我在手册页中没有找到关于这一点的准确陈述。如果其中一个消耗命令失败,它们是否返回硬编码的错误状态,或者它们是否以某种方式中继消耗命令的错误状态(例如ssh
)?如果是前者,我们如何找出哪个消费命令是罪魁祸首?如果是后者,则转发哪种错误状态?仅仅是先失败的命令吗?AFAIK,bash 或
tee
orpee
命令本身分别在内部使用管道(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
这可以避免$PIPESTATUS
bashism,或者您可以避免>(...)
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
没有中止,也可能会出现一些有关损坏管道的错误消息)。