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