使用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.txt
和t.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