使用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"以及其他什么命令不起作用?
答案 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 question与SIGPIPE
的联系方式与我们在此处看到的方式非常相似。
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.txt
和t.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