bash进程替换和尾部的结果不正确



使用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包含"argillaceo",大约占文件的5%。

为什么会发生这种情况?看起来要么是"尾巴"进程提前终止,要么是流被混淆了。

运行另一个类似的命令如下所示:

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

在这个命令之后,我希望.txt包含所有以"a"开头的单词,而z.txt包含所有的以"z"开头的词,这正是发生的事情。

那么,为什么这不适用于"tail",以及其他哪些命令不适用呢?

好的,似乎发生的是,一旦head -1命令完成,它就会退出,并导致tee获得一个SIGPIPE,它试图写入命名管道,生成EPIPE并根据man 2 write的进程替换设置也会在写入过程中生成SIGPIPE,这会导致tee退出,并迫使tail -1立即退出,左边的CCD_ 8也得到CCD_。

如果我们在使用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

这个问题以与我们在这里看到的非常相似的方式与CCD_ 15联系在一起。

coreutils的维护者将此作为示例添加到他们的tee"gotchas"中,以供子孙后代使用。

要与开发人员讨论如何将其纳入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,那么这种方法就不太好了。

另一个棘手的选择是在开始前将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

最新更新