Ви можете використовувати комбінацію stdbuf GNU і 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копроком:
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