Неверные результаты с заменой процесса и процессом bash?

Используя bash замену процесса, я хочу одновременно запускать две разные команды в файле. В этом примере не обязательно, но представьте, что "cat/usr/share/dict/words" была очень дорогой операцией, такой как распаковка файла размером 50 гб.

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

После этой команды я ожидал, что a.txt будет содержать все слова, начинающиеся с "a", а z.txt содержит все слова, начинающиеся с "z", что и произошло.

Так почему же это не работает с "хвостом" и с какими другими командами это не работает?

Ответ 1

Хорошо, похоже, что когда команда 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

поэтому он получил всего еще 1 итерацию цикла до того, как все вышло (хотя t.txt по-прежнему имеет только 1). Если бы мы тогда сделали

echo "${PIPESTATUS[@]}"

мы видим, что

141 141

который этот вопрос связан с SIGPIPE очень похожим на то, что мы видим здесь.

Составители coreutils добавили это в качестве примера к их tee "gotchas" для будущего потомства.

Для обсуждения с разработчиками о том, как это вписывается в соответствие POSIX, вы можете увидеть отчет (закрытый notabug) в 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