Вы можете использовать комбинацию GNU stdbuf и peeот moreutils :
echo "Hello world!" | stdbuf -o 1M pee cmd1 cmd2 cmd3 > output
Писать popen(3)эти 3 командные строки оболочки, а затем freadввод и fwriteвсе три, которые будут буферизироваться до 1М.
Идея состоит в том, чтобы иметь буфер по крайней мере такой же большой, как входные данные. Таким образом, даже несмотря на то, что три команды запускаются одновременно, они будут видеть ввод только при pee pcloseпоследовательном выполнении трех команд.
После каждого pclose, peeпереполнить буфер в команде и ждет его завершения. Это гарантирует, что до тех пор, пока эти cmdxкоманды не начнут выводить что-либо до того, как они получат какие-либо входные данные (и не будут обрабатывать процесс, который может продолжить вывод после возвращения их родителя), выходные данные трех команд не будут чередоваться.
По сути, это немного похоже на использование временного файла в памяти, с тем недостатком, что 3 команды запускаются одновременно.
Чтобы избежать одновременного запуска команд, вы можете написать peeв виде функции оболочки:
pee() (
input=$(cat; echo .)
for i do
printf %s "${input%.}" | eval "$i"
done
)
echo "Hello world!" | pee cmd1 cmd2 cmd3 > out
Но имейте в виду, что оболочки, отличные от тех, zshкоторые не будут использоваться для двоичного ввода с символами NUL.
Это позволяет избежать использования временных файлов, но это означает, что весь ввод хранится в памяти.
В любом случае вам придется хранить входные данные где-то, в памяти или временном файле.
На самом деле, это довольно интересный вопрос, поскольку он показывает нам предел идеи Unix, заключающейся в том, чтобы несколько простых инструментов взаимодействовали с одной задачей.
Здесь мы хотели бы, чтобы несколько инструментов сотрудничали с задачей:
- исходная команда (здесь
echo)
- диспетчерская команда (
tee)
- некоторые команды фильтра (
cmd1, cmd2, cmd3)
- и команда агрегации (
cat).
Было бы хорошо, если бы они все могли работать вместе и выполнять свою тяжелую работу над данными, которые они должны обрабатывать, как только они станут доступны.
В случае одной команды фильтра это легко:
src | tee | cmd1 | cat
Все команды выполняются одновременно, cmd1начинает собирать данные, srcкак только они становятся доступными.
Теперь, используя три команды фильтра, мы можем сделать то же самое: запустить их одновременно и соединить с помощью каналов:
┏━━━┓▁▁▁▁▁▁▁▁▁▁┏━━━━┓▁▁▁▁▁▁▁▁▁▁┏━━━┓
┃ ┃░░░░2░░░░░┃cmd1┃░░░░░5░░░░┃ ┃
┃ ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃
┏━━━┓▁▁▁▁▁▁▁▁▁▁┃ ┃▁▁▁▁▁▁▁▁▁▁┏━━━━┓▁▁▁▁▁▁▁▁▁▁┃ ┃▁▁▁▁▁▁▁▁▁┏━━━┓
┃src┃░░░░1░░░░░┃tee┃░░░░3░░░░░┃cmd2┃░░░░░6░░░░┃cat┃░░░░░░░░░┃out┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃▔▔▔▔▔▔▔▔▔┗━━━┛
┃ ┃▁▁▁▁▁▁▁▁▁▁┏━━━━┓▁▁▁▁▁▁▁▁▁▁┃ ┃
┃ ┃░░░░4░░░░░┃cmd3┃░░░░░7░░░░┃ ┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━┛
Что мы можем сделать относительно легко с именованными каналами :
pee() (
mkfifo tee-cmd1 tee-cmd2 tee-cmd3 cmd1-cat cmd2-cat cmd3-cat
{ tee tee-cmd1 tee-cmd2 tee-cmd3 > /dev/null <&3 3<&- & } 3<&0
eval "$1 < tee-cmd1 1<> cmd1-cat &"
eval "$2 < tee-cmd2 1<> cmd2-cat &"
eval "$3 < tee-cmd3 1<> cmd3-cat &"
exec cat cmd1-cat cmd2-cat cmd3-cat
)
echo abc | pee 'tr a A' 'tr b B' 'tr c C'
(выше, } 3<&0чтобы обойти тот факт, что &перенаправления stdinот /dev/null, и мы используем, <>чтобы избежать открытия каналов для блокировки, пока другой конец ( cat) также не открылся)
Или, чтобы избежать именованных каналов, немного сложнее с zshcoproc:
pee() (
n=0 ci= co= is=() os=()
for cmd do
eval "coproc $cmd $ci $co"
exec {i}<&p {o}>&p
is+=($i) os+=($o)
eval i$n=$i o$n=$o
ci+=" {i$n}<&-" co+=" {o$n}>&-"
((n++))
done
coproc :
read -p
eval tee /dev/fd/$^os $ci "> /dev/null &" exec cat /dev/fd/$^is $co
)
echo abc | pee 'tr a A' 'tr b B' 'tr c C'
Теперь вопрос: как только все программы будут запущены и подключены, будет ли поток данных?
У нас есть два ограничения:
tee передает все свои выходы с одинаковой скоростью, поэтому он может отправлять данные только со скоростью самого медленного канала вывода.
cat начнёт чтение только со второго канала (канал 6 на рисунке выше), когда все данные будут считаны из первого (5).
Это означает, что данные не будут передаваться в канал 6 до cmd1тех пор, пока он не закончится. И, как в случае tr b Bвышеупомянутого, это может означать, что данные также не будут передаваться в трубе 3, что означает, что они не будут течь ни в одном из каналов 2, 3 или 4, так как teeподача происходит с самой низкой скоростью из всех 3.
На практике эти каналы имеют ненулевой размер, поэтому некоторым данным удастся пройти, и по крайней мере в моей системе я смогу заставить их работать до:
yes abc | head -c $((2 * 65536 + 8192)) | pee 'tr a A' 'tr b B' 'tr c C' | uniq -c -c
Помимо этого, с
yes abc | head -c $((2 * 65536 + 8192 + 1)) | pee 'tr a A' 'tr b B' 'tr c C' | uniq -c
У нас тупик, где мы находимся в такой ситуации:
┏━━━┓▁▁▁▁2▁▁▁▁▁┏━━━━┓▁▁▁▁▁5▁▁▁▁┏━━━┓
┃ ┃░░░░░░░░░░┃cmd1┃░░░░░░░░░░┃ ┃
┃ ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃
┏━━━┓▁▁▁▁1▁▁▁▁▁┃ ┃▁▁▁▁3▁▁▁▁▁┏━━━━┓▁▁▁▁▁6▁▁▁▁┃ ┃▁▁▁▁▁▁▁▁▁┏━━━┓
┃src┃██████████┃tee┃██████████┃cmd2┃██████████┃cat┃░░░░░░░░░┃out┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃▔▔▔▔▔▔▔▔▔┗━━━┛
┃ ┃▁▁▁▁4▁▁▁▁▁┏━━━━┓▁▁▁▁▁7▁▁▁▁┃ ┃
┃ ┃██████████┃cmd3┃██████████┃ ┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━┛
Мы заполнили трубы 3 и 6 (по 64 КБ каждая). teeпрочитал этот лишний байт, он покормил его cmd1, но
- теперь он заблокирован записи на канал 3, поскольку он ждет,
cmd2чтобы очистить его
cmd2не может очистить его, потому что он заблокировал запись на канал 6, ожидая, catчтобы очистить его
cat не может очистить его, потому что он ждет, пока не будет больше ввода по каналу 5.
cmd1не могу сказать, catчто больше нет ввода, потому что оно ожидает самого ввода от tee.
- и
teeне могу сказать, cmd1что больше нет ввода, потому что он заблокирован ... и так далее.
У нас есть цикл зависимости и, следовательно, тупик.
Теперь, каково решение? Большие каналы 3 и 4 (достаточно большие, чтобы вместить все srcвыходные данные) сделали бы это. Мы могли бы сделать это, например, вставив pv -qB 1Gмежду teeи cmd2/3где pvможно хранить до 1G данных, ожидающих cmd2и cmd3читать их. Это будет означать две вещи, хотя:
- который использует потенциально много памяти, и, кроме того, дублируя его
- из-за этого все 3 команды не могут взаимодействовать, поскольку
cmd2в действительности обработка данных начинается только после завершения cmd1.
Решением второй проблемы было бы также увеличить трубы 6 и 7. Предполагая это cmd2и cmd3производя столько продукции, сколько они потребляют, это не потребляет больше памяти.
Единственный способ избежать дублирования данных (в первой задаче) состоит в том, чтобы реализовать сохранение данных в самом диспетчере, то есть реализовать вариант, teeкоторый может передавать данные со скоростью самого быстрого вывода (удерживая данные для подачи медленнее в своем собственном темпе). Не совсем тривиально.
Итак, в конце концов, лучшее, что мы можем разумно получить без программирования, это, вероятно, что-то вроде (синтаксис Zsh):
max_hold=1G
pee() (
n=0 ci= co= is=() os=()
for cmd do
if ((n)); then
eval "coproc pv -qB $max_hold $ci $co | $cmd $ci $co | pv -qB $max_hold $ci $co"
else
eval "coproc $cmd $ci $co"
fi
exec {i}<&p {o}>&p
is+=($i) os+=($o)
eval i$n=$i o$n=$o
ci+=" {i$n}<&-" co+=" {o$n}>&-"
((n++))
done
coproc :
read -p
eval tee /dev/fd/$^os $ci "> /dev/null &" exec cat /dev/fd/$^is $co
)
yes abc | head -n 1000000 | pee 'tr a A' 'tr b B' 'tr c C' | uniq -c