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

时间:2015-12-17 17:27:30

标签: bash pipe tail tee

使用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包含单词file" 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

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

那么,为什么这不能用" tail"以及其他什么命令不起作用?

1 个答案:

答案 0 :(得分:11)

好吧,似乎发生的事情是,一旦head -1命令完成它退出并导致tee获得SIGPIPE,它会尝试写入命名管道,进程替换设置生成一个EPIPE并且根据man 2 write也会在写入过程中生成SIGPIPE,这会导致tee退出并强制tail -1立即退出,并且左侧的cat也会获得SIGPIPE

如果我们使用head向流程添加更多内容并使输出更具可预测性并且也可以写入stderr而不依赖于tee,我们可以更好地看到这一点:

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

所以在退出所有内容之前,它只会再循环一次(尽管t.txt仍然只有1)。如果我们那么

echo "${PIPESTATUS[@]}"

我们看到了

141 141

this questionSIGPIPE的联系方式与我们在此处看到的方式非常相似。

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

如果与开发人员讨论如何符合POSIX合规性,您可以在http://debbugs.gnu.org/cgi/bugreport.cgi?bug=22195

看到(关闭的notabug)报告

如果您可以访问GNU版本8.24,则他们添加了一些可以帮助您-p--output-error=warn的选项(不在POSIX中)。如果没有它,你可以冒一点风险,但通过陷阱和忽略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