tee + cat: çıktıyı birkaç kez kullanın ve ardından sonuçları birleştirin


18

Bazı komutları çağırırsam, örneğin, bir echokomutun sonuçlarını diğer birkaç komutta kullanabilirim tee. Misal:

echo "Hello world!" | tee >(command1) >(command2) >(command3)

Kedi ile birkaç komutun sonuçlarını toplayabilirim. Misal:

cat <(command1) <(command2) <(command3)

Her iki şeyi de aynı anda yapabilmek istiyorum, böylece teebu komutları başka bir şeyin çıktısında (örneğin echoyazdım) çağırmak ve sonra tüm sonuçlarını tek bir çıktıda toplamak için kullanabilirim. cat.

Sonuçları sıralı olarak tutmak önemlidir, bu araçlar çıkışında çizgiler command1, command2ve command3(onunla olduğu gibi iç içe ama komutlardır olarak sipariş edilmemelidir cat).

Bundan daha iyi seçenekler olabilir cat, teeancak şimdiye kadar tanıdığım bunlar.

Giriş ve çıkış boyutu büyük olabileceğinden geçici dosyaları kullanmaktan kaçınmak istiyorum.

Bunu nasıl yapabilirim?

PD: başka bir sorun, bunun bir döngüde gerçekleşmesidir, bu da geçici dosyaların işlenmesini zorlaştırır. Bu mevcut kod ve küçük testcases için çalışır, ancak okuma ve yazma yardımcı bir şekilde anlamadığım bir şekilde sonsuz döngüler oluşturur.

somefunction()
{
  if [ $1 -eq 1 ]
  then
    echo "Hello world!"
  else
    somefunction $(( $1 - 1 )) > auxfile
    cat <(command1 < auxfile) \
        <(command2 < auxfile) \
        <(command3 < auxfile)
  fi
}

Yardımcı dosyalardaki okumalar ve yazılar örtüşüyor gibi görünüyor, her şeyin patlamasına neden oluyor.


2
Ne kadar konuşuyoruz? Gereksinimleriniz her şeyi hafızada tutmaya zorlar. Sonuçları düzenli tutmak, command2 ve command3'ün işlemeye başlayabilmesi için önce komut1'in önce tamamlanması gerektiği anlamına gelir (bu nedenle muhtemelen tüm girişi okumuş ve tüm çıktıyı yazdırmıştır) (çıkışlarını bellekte toplamak istemiyorsanız).
frostschutz

haklısınız, command2 ve command3 giriş ve çıkışları bellekte tutulamayacak kadar büyük. Ben takas kullanarak geçici dosyaları kullanmak daha iyi olacağını bekliyordum. Sahip olduğum başka bir sorun, bunun bir döngüde gerçekleşmesi ve dosyaların işlenmesini daha da zorlaştırması. Ben tek bir dosya kullanıyorum ama şu anda nedense okuma ve yazma dosyasında reklam sonsuz büyümesine neden bazı çakışma var. Soruyu, sizi çok fazla ayrıntıdan sıkmadan güncellemeye çalışacağım.
Trylks

4
Geçici dosyalar kullanmalısınız; giriş için echo HelloWorld > file; (command1<file;command2<file;command3<file)veya çıkış için echo | tee cmd1 cmd2 cmd3; cat cmd1-output cmd2-output cmd3-output. Sadece bu şekilde çalışır - tee, yalnızca tüm komutlar paralel olarak çalışır ve işlenirse çatallanabilir. bir komut uyursa (serpiştirmeyi istemediğiniz için) belleğin girişle doldurulmasını önlemek için tüm komutları bloke eder ...
frostschutz

Yanıtlar:


27

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

  1. potansiyel olarak çok fazla bellek kullanıyor ve dahası
  2. 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

Haklısın, kilitlenme geçici dosyaları kullanmaktan kaçınmak için şimdiye kadar bulduğum en büyük problem. Bu dosyalar oldukça hızlı görünüyor, ancak bir yerde önbelleğe alınıp alınmadıklarını bilmiyorum, disk erişim sürelerinden korktum, ancak şu ana kadar makul görünüyorlar.
Trylks

6
+1 Güzel ASCII sanatı için ekstra bir :-)
Kurt Pfeifle

3

Teklif ettiğiniz şey mevcut herhangi bir komutla kolayca yapılamaz ve yine de pek mantıklı değildir. (Boruların fikrine |Unix / Linux) 'de ki bir bellek tampon doldurur kadar (en fazla) yazıyor çıktı ve sonra boşalıncaya kadar (en fazla) tampon veri okuma çalışır. Yani ve aynı zamanda çalıştırıldığında, aralarında sınırlı bir miktarda "uçuşta" veri bulunması asla gerekli değildir. Tek bir çıkışa birden fazla giriş bağlamak istiyorsanız, okuyuculardan biri diğerlerinin gerisinde kalırsa ya diğerlerini durdurursunuz (o zaman paralel olarak koşmanın anlamı nedir?) Ya da gecikmenin henüz okumadıkları çıkışı saklarsınız (o zaman ara dosya olmamasının anlamı nedir?).cmd1 | cmd2cmd1cmd2cmd1cmd2 daha karmaşık.

Yaklaşık 30 yıllık Unix deneyimimde, böyle bir çok çıkışlı boru için gerçekten fayda sağlayacak herhangi bir durumu hatırlamıyorum.

Birden fazla çıktıyı bugün tek bir akışta birleştirebilirsiniz, sadece herhangi bir araya getirilmemiş şekilde değil (çıktıları nasıl cmd1ve cmd2araya eklenir? Sırayla? Sırayla 10 bayt yazıyor? Bir şekilde tanımlanmış alternatif "paragraflar" mı? t Uzun süre bir şey yazmayın, tüm bunların üstesinden gelmek karmaşıktır). Bu, örneğin yapılır (cmd1; cmd2; cmd3) | cmd4, programlar cmd1, cmd2ve cmd3birbiri ardına işletilmektedir, çıkış için giriş olarak gönderilir cmd4.


3

Çakışan sorununuz için, Linux'ta (ve birlikte bashya da zshdeğil ksh93), bunu şu şekilde yapabilirsiniz:

somefunction()
(
  if [ "$1" -eq 1 ]
  then
    echo "Hello world!"
  else
    exec 3> auxfile
    rm -f auxfile
    somefunction "$(($1 - 1))" >&3 auxfile 3>&-
    exec cat <(command1 < /dev/fd/3) \
             <(command2 < /dev/fd/3) \
             <(command3 < /dev/fd/3)
  fi
)

Kullanımına dikkat (...)yerine {...}yeni bir yeni bir fd 3 işaret olabilir bu yüzden her tekrarda yeni bir süreç olsun auxfile. < /dev/fd/3silinmiş dosyaya erişmek için bir hile. Bu Linux dışındaki sistemlerde çalışma olmaz < /dev/fd/3edilir gibi dup2(3, 0)ve bu yüzden dosyanın sonuna imleç salt modunda açık olacağını 0 fd.

Yuvalanmış bir işlev için çataldan kaçınmak için şöyle yazabilirsiniz:

somefunction()
{
  if [ "$1" -eq 1 ]
  then
    echo "Hello world!"
  else
    {
      rm -f auxfile
      somefunction "$(($1 - 1))" >&3 auxfile 3>&-
      exec cat <(command1 < /dev/fd/3) \
               <(command2 < /dev/fd/3) \
               <(command3 < /dev/fd/3)
    } 3> auxfile
  fi
}

Kabuk , her yinelemede fd 3'ü yedeklemeye özen gösterir . Yine de dosya tanımlayıcıları daha kısa sürede bitirdiniz.

Gerçi bunu yapmanın daha verimli olduğunu göreceksiniz:

somefunction() {
  if [ "$1" -eq 1 ]; then
    echo "Hello world!" > auxfile
  else
    somefunction "$(($1 - 1))"
    { rm -f auxfile
      cat <(command1 < /dev/fd/3) \
          <(command2 < /dev/fd/3) \
          <(command3 < /dev/fd/3) > auxfile
    } 3< auxfile
  fi
}
somefunction 12; cat auxfile

Yani, yönlendirmeleri yuvalamayın.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.