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

Incorrect results with bash process substitution and tail?

使用 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%。

为什么会这样?似乎 "tail" 进程提前终止或流混淆。

运行 另一个类似的命令按预期运行:

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

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

那么,为什么这对 "tail" 不起作用,而对其他哪些命令不起作用?

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

如果我们使用 head 向过程中添加更多内容,并使输出更可预测并写入 stderr 而不依赖 [=17],我们可以更好地看到这一点=]:

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

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

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

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

上查看(已关闭的 notabug)报告

如果您可以访问 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