使用bash进程替换和tail命令时结果不正确?

13

使用bash进程替换,我想同时在一个文件上运行两个不同的命令。在这个例子中并不需要,但请想象"cat /usr/share/dict/words"是一项非常昂贵的操作,例如解压缩一个50GB的文件。

cat /usr/share/dict/words | tee >(head -1 > h.txt) >(tail -1 > t.txt) > /dev/null

执行此命令后,我希望h.txt包含单词文件“A”的第一行,而t.txt则包含文件“Zyzzogeton”的最后一行。

然而实际发生的情况是,h.txt包含“A”,但t.txt包含文件中大约5%位置的“argillaceo”。

为什么会这样呢?似乎要么“tail”进程过早终止,要么流被混淆了。

运行另一个类似的命令可以得到预期的结果:

cat /usr/share/dict/words | tee >(grep ^a > a.txt) >(grep ^z > z.txt) > /dev/null

执行这个命令后,我期望a.txt包含以字母"a"开头的所有单词,而z.txt则包含以字母"z"开头的所有单词,确切地说就是这样发生的。

那么为什么对于"tail"命令不起作用呢?还有哪些其他命令也不会起作用呢?


1
我认为这与https://dev59.com/d2855IYBdhLWcg3wMBLV有关,该网站建议替换中的进程在外部命令完成后立即退出,但坦白地说,我尚未能够证明这是当前使用的任何命令的问题。 - Eric Renouf
1个回答

11

好的,看起来发生的情况是,一旦head -1命令完成并退出,这将导致tee收到一个SIGPIPE,它试图写入进程替换设置的命名管道,该替换生成一个EPIPE,根据man 2 write,在写入过程中也会生成SIGPIPE,这会导致tee退出,强制tail -1立即退出,并且左侧的cat也会得到一个SIGPIPE

如果我们使用head添加一些更多的进程并使输出更可预测并且不依赖于tee来写入stderr,就可以更清楚地看到这一点:

for i in {1..30}; do echo "$i"; echo "$i" >&2; sleep 1; done | tee >(head -1 > h.txt; echo "Head done") >(tail -1 > t.txt) >/dev/null

当我运行它时,它给了我输出:

1
Head done
2

在一切退出之前,循环只执行了1次(尽管t.txt仍然只有1)。如果我们接下来执行

echo "${PIPESTATUS[@]}"

我们看到

141 141

这个问题与SIGPIPE有着非常类似的联系,这个问题详细解释了这一点。

coreutils维护者已将此作为示例添加到其tee“陷阱”中以备将来参考。

如果想要了解开发人员关于如何符合POSIX标准的讨论,可以查看http://debbugs.gnu.org/cgi/bugreport.cgi?bug=22195中的(关闭的无效)报告。

如果您有GNU版本8.24的访问权限,则可以使用一些选项(不在POSIX中),例如-p--output-error=warn,它们可以提供帮助。否则,您可以冒一点风险通过捕获并忽略SIGPIPE来得到所需的功能:

trap '' PIPE
for i in {1..30}; do echo "$i"; echo "$i" >&2; sleep 1; done | tee >(head -1 > h.txt; echo "Head done") >(tail -1 > t.txt) >/dev/null
trap - PIPE

h.txtt.txt 中都会产生预期的结果,但如果发生了其他需要正确处理 SIGPIPE 的情况,则使用此方法将没有运气。

另一个hacky选项是在开始之前将t.txt归零,然后不让head进程列表完成,直到其长度为非零:

> t.txt; for i in {1..10}; do echo "$i"; echo "$i" >&2; sleep 1; done | tee >(head -1 > h.txt; echo "Head done"; while [ ! -s t.txt ]; do sleep 1; done) >(tail -1 > t.txt; date) >/dev/null

1
tee 的 POSIX 规范行为是即使其中一个读取器退出,它也会继续运行——因此,如果您看到与此相反的情况,那实际上是一个 bug。 - Charles Duffy
如果对于任何成功打开的文件操作数写入失败,则对于其他成功打开的文件操作数和标准输出的写入将继续,但退出状态应为非零。否则,适用于实用程序描述默认值的默认操作。 - Charles Duffy
@CharlesDuffy 好吧,我猜上面的结果是针对旧版本的,8.5吧,我可以再试一次。此外,我还没有深入了解过这个过程替换是否呈现为关闭文件句柄,或者当该进程结束时它是否实际上会引发SIGPIPE信号,我想在提交错误报告之前还需要做更多的工作。 - Eric Renouf
@CharlesDuffy,我深入挖掘了一下,因为进程替换在使用命名管道时,当tee对一个关闭的管道进行write操作时,它会收到一个EPIPE并接收到一个SIGPIPE。也许这确实违反了您引用的标准。 - Eric Renouf
1
那个关于忽略SIGPIPE的错误报告的回复可能值得传播到这里的答案中。 - Charles Duffy
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接