Вы можете использовать комбинацию 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
) также не открылся)
Или, чтобы избежать именованных каналов, немного сложнее с zsh
coproc:
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