Sen GNU stdbuf ve birlikte kullanabilir peegelen moreutils :
echo "Hello world!" | stdbuf -o 1M pee cmd1 cmd2 cmd3 > output
popen(3)o 3 kabuk komut satırları işemek ve daha sonra freadgiriş fwrites ve üçü de, 1M kadar tamponlu olacak.
Fikir, en az girdi kadar büyük bir arabellek bulundurmaktır. Bu şekilde, üç komut aynı anda başlatılmış olsa bile, girdiler yalnızca pee pcloseüç komut sırayla geldiğinde görünecektir .
Her üzerine pclose, peesonlanmasını komut ve bekler tamponunu boşaltır. Bu, bu cmdxkomutlar herhangi bir girdi almadan önce herhangi bir şey çıktılamaya başlamadığı sürece (ve ebeveynleri döndükten sonra çıktı almaya devam edebilecek bir işlemi çatallamadıkça), üç komutun çıktısının araya eklenmiş.
Aslında bu, 3 komutun eşzamanlı olarak başlatılması dezavantajı ile bellekte bir geçici dosya kullanmaya benzer.
Komutları aynı anda başlatmaktan kaçınmak peeiçin kabuk işlevi olarak yazabilirsiniz :
pee() (
input=$(cat; echo .)
for i do
printf %s "${input%.}" | eval "$i"
done
)
echo "Hello world!" | pee cmd1 cmd2 cmd3 > out
Ancak zsh, NUL karakterli ikili girişler için mermilerin dışındaki mermilerin başarısız olacağını unutmayın.
Bu geçici dosyaların kullanılmasını önler, ancak bu, girdinin tamamının bellekte saklandığı anlamına gelir.
Her durumda, girişi bir yerde, bellekte veya geçici bir dosyada saklamanız gerekir.
Aslında, Unix'in birkaç basit aracın tek bir görevle işbirliği yapma fikrinin sınırını gösterdiğinden oldukça ilginç bir soru.
Burada, görev için işbirliği yapan birkaç araç olmasını istiyoruz:
- bir kaynak komutu (burada
echo)
- bir dağıtıcı komutu (
tee)
- Bazı filtre komutları (
cmd1, cmd2, cmd3)
- ve bir toplama komutu (
cat).
Hepsi aynı anda birlikte çalışabilseler ve mümkün olan en kısa sürede işlemek istedikleri veriler üzerinde sıkı çalışabilirlerse güzel olurlar.
Bir filtre komutu söz konusu olduğunda kolaydır:
src | tee | cmd1 | cat
Tüm komutlar eşzamanlı olarak çalıştırılır cmd1, srcen kısa sürede verileri karıştırmaya başlar .
Şimdi, üç filtre komutuyla, hala aynı şeyi yapabiliriz: onları aynı anda başlatın ve borularla bağlayın:
┏━━━┓▁▁▁▁▁▁▁▁▁▁┏━━━━┓▁▁▁▁▁▁▁▁▁▁┏━━━┓
┃ ┃░░░░2░░░░░┃cmd1┃░░░░░5░░░░┃ ┃
┃ ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃
┏━━━┓▁▁▁▁▁▁▁▁▁▁┃ ┃▁▁▁▁▁▁▁▁▁▁┏━━━━┓▁▁▁▁▁▁▁▁▁▁┃ ┃▁▁▁▁▁▁▁▁▁┏━━━┓
┃src┃░░░░1░░░░░┃tee┃░░░░3░░░░░┃cmd2┃░░░░░6░░░░┃cat┃░░░░░░░░░┃out┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃▔▔▔▔▔▔▔▔▔┗━━━┛
┃ ┃▁▁▁▁▁▁▁▁▁▁┏━━━━┓▁▁▁▁▁▁▁▁▁▁┃ ┃
┃ ┃░░░░4░░░░░┃cmd3┃░░░░░7░░░░┃ ┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━┛
Hangi adlandırılmış borular ile nispeten kolay yapabiliriz :
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'
(yukarıda } 3<&0olduğu gerçeğini geçici olarak &yönlendirmeleri stdingelen /dev/null, kullandığımız <>diğer ucuna kadar bloğuna boru açıklığı (önlemek için catde açtı))
Veya adlandırılmış borulardan kaçınmak için, zshcoproc ile biraz daha acı verici :
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'
Şimdi soru şu: Tüm programlar başlatıldıktan ve bağlandıktan sonra veri akışı olacak mı?
İki karşıtlığımız var:
tee tüm çıkışlarını aynı hızda besler, böylece yalnızca en yavaş çıkış borusu hızında veri gönderebilir.
cat sadece ikinci borudan okumaya başlayacaktır (yukarıdaki çizimde boru 6) ilk veri (5) 'ten tüm veriler okunduğunda.
Bunun anlamı, veriler bitene kadar boru 6'da cmd1akmayacaktır. Ve tr b Byukarıdaki gibi, bu da verilerin boru 3'te de akmayacağı anlamına gelebilir, bu da 3 tee, en yavaş hızda beslendiği için 2, 3 veya 4 borularının hiçbirinde akmayacağı anlamına gelir .
Uygulamada bu borular boş olmayan bir boyuta sahiptir, bu nedenle bazı veriler geçmeyi başarabilir ve en azından sistemimde aşağıdakilere kadar çalışabilirim:
yes abc | head -c $((2 * 65536 + 8192)) | pee 'tr a A' 'tr b B' 'tr c C' | uniq -c -c
Bunun ötesinde,
yes abc | head -c $((2 * 65536 + 8192 + 1)) | pee 'tr a A' 'tr b B' 'tr c C' | uniq -c
Bu durumda olduğumuz bir kilitlenme var:
┏━━━┓▁▁▁▁2▁▁▁▁▁┏━━━━┓▁▁▁▁▁5▁▁▁▁┏━━━┓
┃ ┃░░░░░░░░░░┃cmd1┃░░░░░░░░░░┃ ┃
┃ ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃
┏━━━┓▁▁▁▁1▁▁▁▁▁┃ ┃▁▁▁▁3▁▁▁▁▁┏━━━━┓▁▁▁▁▁6▁▁▁▁┃ ┃▁▁▁▁▁▁▁▁▁┏━━━┓
┃src┃██████████┃tee┃██████████┃cmd2┃██████████┃cat┃░░░░░░░░░┃out┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃▔▔▔▔▔▔▔▔▔┗━━━┛
┃ ┃▁▁▁▁4▁▁▁▁▁┏━━━━┓▁▁▁▁▁7▁▁▁▁┃ ┃
┃ ┃██████████┃cmd3┃██████████┃ ┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━┛
3 ve 6 numaralı boruları doldurduk (her biri 64KB). teefazladan baytı okudu, besledi cmd1ama
- şimdi boru 3'e yazılmasını engelledi çünkü
cmd2boşaltmayı bekliyor
cmd2Boşaltılamıyor çünkü 6. boruya yazılmasını engelliyor, catboşaltmayı bekliyor
cat Boşaltılamıyor çünkü 5. boruda daha fazla girdi kalmayana kadar bekliyor.
cmd1catdaha fazla girdi olmadığını söyleyemez çünkü daha fazla girdi beklemektedir tee.
- ve daha fazla girdi
teeolmadığını söyleyemem cmd1çünkü engellenmiş ... vb.
Bir bağımlılık döngümüz ve dolayısıyla bir kilitlenme var.
Şimdi, çözüm nedir? Daha büyük borular 3 ve 4 (tüm srcçıktıları içerecek kadar büyük ) bunu yapardı. Bunu, örneğin 1G'ye kadar veriyi beklemek ve okumak için araya ve nereye yerleştirerek pv -qB 1Gyapabiliriz . Bu iki anlama geliyor:teecmd2/3pvcmd2cmd3
- potansiyel olarak çok fazla bellek kullanıyor ve dahası
- bu 3 komutun hepsinin birlikte çalışmasını sağlayamaz çünkü
cmd2gerçekte sadece cmd1 tamamlandığında verileri işlemeye başlar.
İkinci soruna bir çözüm, boru 6 ve 7'yi de daha büyük yapmak olacaktır. Bunu varsaymak cmd2ve cmd3tükettikleri kadar çıktı üretmek, daha fazla bellek tüketmez.
Verilerin çoğaltılmasından kaçınmanın tek yolu (ilk problemde), dağıtıcının kendisinde veri tutmayı uygulamaktır, yani teeverileri en hızlı çıktı hızında besleyebilen (veriyi beslemek için verileri tutmak) kendi hızlarında yavaşlar). Gerçekten önemsiz değil.
Sonuç olarak, programlama yapmadan makul bir şekilde elde edebileceğimiz en iyi şey muhtemelen (Zsh sözdizimi) gibi bir şeydir:
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