tee + cat: użyj wyjścia kilka razy, a następnie połącz wyniki

18

Jeśli wywołam jakieś polecenie, na przykład echomogę użyć wyników tego polecenia w kilku innych poleceniach za pomocą tee. Przykład:

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

Z catem mogę zebrać wyniki kilku poleceń. Przykład:

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

Chciałbym móc wykonywać obie rzeczy jednocześnie, aby móc teewywoływać te polecenia na wyjściu czegoś innego (na przykład echonapisałem), a następnie zebrać wszystkie ich wyniki na jednym wyjściu za pomocą cat.

Ważne jest, aby zachować wyniki w porządku, oznacza to, że linie na wyjściu command1, command2i command3nie powinny być ze sobą powiązane, ale zamawiać polecenia są (jak to się dzieje z cat).

Nie może być lepsze opcje niż cati teeale są to te, które znam tak daleko.

Chcę uniknąć używania plików tymczasowych, ponieważ rozmiar danych wejściowych i wyjściowych może być duży.

Jak mogłem to zrobić?

PD: innym problemem jest to, że dzieje się to w pętli, co utrudnia obsługę plików tymczasowych. To jest obecny kod, który mam i działa on na małe przypadki testowe, ale tworzy nieskończone pętle podczas odczytu i zapisu z pliku pomocniczego w sposób, którego nie rozumiem.

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

Odczyty i zapisy w pliku pomocniczym wydają się nakładać na siebie, powodując, że wszystko eksploduje.

Trylks
źródło
2
Jak duży rozmawiamy? Twoje wymagania wymuszają przechowywanie wszystkiego w pamięci. Utrzymywanie wyników w porządku oznacza, że ​​polecenie 1 musi najpierw wykonać (więc prawdopodobnie przeczytał całe dane wejściowe i wydrukowało całe dane wyjściowe), zanim polecenia 2 i polecenie 3 mogą nawet rozpocząć przetwarzanie (chyba że najpierw chcesz zebrać ich wyniki w pamięci).
frostschutz
masz rację, dane wejściowe i wyjściowe poleceń 2 i 3 są zbyt duże, aby można je było przechowywać w pamięci. Spodziewałem się, że zamiana będzie działać lepiej niż przy użyciu plików tymczasowych. Innym problemem, jaki mam, jest to, że dzieje się to w pętli, co jeszcze bardziej utrudnia obsługę plików. Korzystam z jednego pliku, ale w tym momencie z jakiegoś powodu istnieje pewne nakładanie się odczytu i zapisu z pliku, co powoduje, że rośnie on w nieskończoność. Spróbuję zaktualizować pytanie, nie nudząc cię zbyt wieloma szczegółami.
Trylks,
4
Musisz użyć plików tymczasowych; albo na wejściu, echo HelloWorld > file; (command1<file;command2<file;command3<file)albo na wyjściu echo | tee cmd1 cmd2 cmd3; cat cmd1-output cmd2-output cmd3-output. Tak to działa - tee może rozwidlać dane wejściowe tylko wtedy, gdy wszystkie polecenia działają i przetwarzają równolegle. jeśli jeden śpi Command (bo nie chcesz przeplatanie) będzie po prostu blokować wszystkie polecenia, tak aby zapobiec zapełnieniu pamięci z wejściem ...
Frostschutz

Odpowiedzi:

27

Można użyć kombinacji GNU stdbuf i peeod moreutils :

echo "Hello world!" | stdbuf -o 1M pee cmd1 cmd2 cmd3 > output

sika popen(3)te 3 wiersze poleceń powłoki, a następnie freads dane wejściowe i fwrites to wszystkie trzy, które będą buforowane do 1M.

Chodzi o to, aby bufor był co najmniej tak duży jak dane wejściowe. W ten sposób, mimo że trzy polecenia są uruchamiane w tym samym czasie, będą widzieć wejście przychodzące tylko wtedy, gdy pee pclosetrzy polecenia będą kolejno.

Po każdej pclose, peeopróżnia bufor do polecenia i czeka na jego zakończenie. To gwarantuje, że dopóki te cmdxpolecenia nie zaczną wypisywać niczego, zanim nie otrzymają żadnych danych wejściowych (i nie rozwidlają procesu, który może kontynuować wysyłanie po powrocie ich rodzica), dane wyjściowe trzech poleceń nie będą przeplatane.

W rzeczywistości przypomina to użycie pliku tymczasowego w pamięci, z tą wadą, że 3 polecenia są uruchamiane jednocześnie.

Aby uniknąć jednoczesnego uruchamiania poleceń, możesz napisać peejako funkcję powłoki:

pee() (
  input=$(cat; echo .)
  for i do
    printf %s "${input%.}" | eval "$i"
  done
)
echo "Hello world!" | pee cmd1 cmd2 cmd3 > out

Ale uwaga, że ​​powłoki inne niż zshzawiodłyby dla wejścia binarnego ze znakami NUL.

Pozwala to uniknąć używania plików tymczasowych, ale oznacza to, że całe wejście jest przechowywane w pamięci.

W każdym razie będziesz musiał gdzieś zapisać dane wejściowe, w pamięci lub pliku tymczasowym.

W rzeczywistości jest to dość interesujące pytanie, ponieważ pokazuje limit idei uniksowej polegającej na współpracy kilku prostych narzędzi w jednym zadaniu.

W tym miejscu chcielibyśmy mieć kilka narzędzi współpracujących z zadaniem:

  • polecenie źródłowe (tutaj echo)
  • polecenie dyspozytora ( tee)
  • Niektóre polecenia filtrów ( cmd1, cmd2, cmd3)
  • oraz polecenie agregujące ( cat).

Byłoby miło, gdyby wszyscy mogli pracować razem w tym samym czasie i ciężko pracować na danych, które mają przetwarzać, gdy tylko będą dostępne.

W przypadku jednego polecenia filtru jest to łatwe:

src | tee | cmd1 | cat

Wszystkie polecenia są uruchamiane jednocześnie, cmd1zaczynają munchować dane, srcgdy tylko będą dostępne.

Teraz, dzięki trzem poleceniom filtrowania, nadal możemy zrobić to samo: uruchomić je jednocześnie i połączyć za pomocą potoków:

               ┏━━━┓▁▁▁▁▁▁▁▁▁▁┏━━━━┓▁▁▁▁▁▁▁▁▁▁┏━━━┓
               ┃   ┃░░░░2░░░░░┃cmd1┃░░░░░5░░░░┃   ┃
               ┃   ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃   ┃
┏━━━┓▁▁▁▁▁▁▁▁▁▁┃   ┃▁▁▁▁▁▁▁▁▁▁┏━━━━┓▁▁▁▁▁▁▁▁▁▁┃   ┃▁▁▁▁▁▁▁▁▁┏━━━┓
┃src┃░░░░1░░░░░┃tee┃░░░░3░░░░░┃cmd2┃░░░░░6░░░░┃cat┃░░░░░░░░░┃out┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┃   ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃   ┃▔▔▔▔▔▔▔▔▔┗━━━┛
               ┃   ┃▁▁▁▁▁▁▁▁▁▁┏━━━━┓▁▁▁▁▁▁▁▁▁▁┃   ┃
               ┃   ┃░░░░4░░░░░┃cmd3┃░░░░░7░░░░┃   ┃
               ┗━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━┛

Co możemy stosunkowo łatwo zrobić za pomocą nazwanych potoków :

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'

(powyżej } 3<&0to obejść, że &przekierowania stdinz /dev/null, i użyć <>w celu uniknięcia otwarcia rury do bloku aż do drugiego końca ( cat) jest otwarty, a)

Lub, aby uniknąć nazwanych potoków, nieco bardziej boleśnie z zshcoproc:

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'

Teraz pytanie brzmi: kiedy wszystkie programy zostaną uruchomione i połączone, czy dane przepłyną?

Mamy dwa przeciwwskazania:

  • tee przesyła wszystkie swoje dane wyjściowe z tą samą prędkością, dzięki czemu może wysyłać dane tylko z prędkością najwolniejszej rury wyjściowej.
  • cat zacznie czytać od drugiej rury (rura 6 na powyższym rysunku) dopiero po odczytaniu wszystkich danych z pierwszej (5).

Oznacza to, że dane nie będą płynąć w rurze 6, dopóki cmd1nie zostaną zakończone. I, podobnie jak w tr b Bpowyższym przypadku, może to oznaczać, że dane również nie będą płynąć w rurze 3, co oznacza, że ​​nie będzie płynąć w żadnej z rur 2, 3 lub 4, ponieważ teeprzesyła je najwolniej ze wszystkich 3.

W praktyce rury te mają niepustą wielkość, więc niektóre dane zdołają się przedostać, a przynajmniej w moim systemie mogę sprawić, aby działał do:

yes abc | head -c $((2 * 65536 + 8192)) | pee 'tr a A' 'tr b B' 'tr c C' | uniq -c -c

Poza tym z

yes abc | head -c $((2 * 65536 + 8192 + 1)) | pee 'tr a A' 'tr b B' 'tr c C' | uniq -c

Mamy impas, w którym znajdujemy się w takiej sytuacji:

               ┏━━━┓▁▁▁▁2▁▁▁▁▁┏━━━━┓▁▁▁▁▁5▁▁▁▁┏━━━┓
               ┃   ┃░░░░░░░░░░┃cmd1┃░░░░░░░░░░┃   ┃
               ┃   ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃   ┃
┏━━━┓▁▁▁▁1▁▁▁▁▁┃   ┃▁▁▁▁3▁▁▁▁▁┏━━━━┓▁▁▁▁▁6▁▁▁▁┃   ┃▁▁▁▁▁▁▁▁▁┏━━━┓
┃src┃██████████┃tee┃██████████┃cmd2┃██████████┃cat┃░░░░░░░░░┃out┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┃   ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃   ┃▔▔▔▔▔▔▔▔▔┗━━━┛
               ┃   ┃▁▁▁▁4▁▁▁▁▁┏━━━━┓▁▁▁▁▁7▁▁▁▁┃   ┃
               ┃   ┃██████████┃cmd3┃██████████┃   ┃
               ┗━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━┛

Wypełniliśmy rury 3 i 6 (64 kB każda). teeprzeczytał ten dodatkowy bajt, nakarmił go cmd1, ale

  • teraz blokuje pisanie na potoku 3, ponieważ czeka na cmd2jego opróżnienie
  • cmd2nie można go opróżnić, ponieważ jest zablokowany zapis na potoku 6, czekając na catjego opróżnienie
  • cat nie można go opróżnić, ponieważ czeka, aż nie będzie już żadnych danych wejściowych w rurze 5.
  • cmd1nie mogę powiedzieć, catże nie ma już danych wejściowych, ponieważ sam oczekuje na więcej danych wejściowych tee.
  • i teenie mogę powiedzieć, cmd1że nie ma już danych wejściowych, ponieważ jest zablokowany ... i tak dalej.

Mamy pętlę zależności, a zatem impas.

Jakie jest rozwiązanie? Zrobiłyby to większe rury 3 i 4 (wystarczająco duże, aby pomieścić całość danych srcwyjściowych). Możemy to zrobić na przykład, wstawiając pv -qB 1Gpomiędzy teei cmd2/3gdzie pvmożna przechowywać do 1G danych oczekujących cmd2i cmd3odczytujących je. Oznaczałoby to dwie rzeczy:

  1. to zużywa potencjalnie dużo pamięci, a ponadto powiela ją
  2. to nie współpracuje ze wszystkimi 3 poleceniami, ponieważ cmd2w rzeczywistości zacząłby przetwarzać dane dopiero po zakończeniu cmd1.

Rozwiązaniem drugiego problemu byłoby zwiększenie również rur 6 i 7. Zakładając to cmd2i cmd3wytwarzając tyle danych, ile zużywają, nie zużyłoby to więcej pamięci.

Jedynym sposobem uniknięcia duplikowania danych (w pierwszym problemie) byłoby zaimplementowanie zatrzymywania danych w samym dyspozytorze, czyli wprowadzenie wariantu, teektóry może przesyłać dane z prędkością najszybszego wyjścia (przechowywanie danych w celu dostarczenia wolniejsze we własnym tempie). Niezbyt trywialne.

Ostatecznie najlepsze, co możemy rozsądnie uzyskać bez programowania, to prawdopodobnie coś w rodzaju (składnia 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
Stéphane Chazelas
źródło
Masz rację, impas jest największym problemem, jaki do tej pory znalazłem, aby uniknąć używania plików tymczasowych. Pliki te wydają się dość szybkie, choć nie wiem, czy są gdzieś buforowane, bałem się czasu dostępu do dysku, ale jak dotąd wydają się rozsądne.
Trylks,
6
Dodatek +1 do ładnej sztuki ASCII :-)
Kurt Pfeifle
3

To, co zaproponujesz, nie może być łatwo wykonane za pomocą żadnego istniejącego polecenia i i tak nie ma większego sensu. Cała koncepcja potoków ( |w systemach Unix / Linux) polega na tym, cmd1 | cmd2że cmd1zapisuje dane wyjściowe (co najwyżej), aż bufor pamięci się zapełni, a następnie cmd2uruchamia odczyt danych z bufora (co najwyżej), dopóki nie będzie pusty. To znaczy , cmd1i cmd2działając jednocześnie, nigdy nie jest potrzebne, aby między nimi znajdowała się więcej niż ograniczona ilość danych. Jeśli chcesz podłączyć kilka wejść do jednego wyjścia, jeśli jeden z czytników pozostaje w tyle za innymi, możesz zatrzymać inne (jaki jest sens równoległego uruchamiania?) Lub ukryć wyjście, którego laggard jeszcze nie odczytał (po co więc nie mieć pliku pośredniego?). bardziej złożony.

W ciągu prawie 30 lat doświadczenia w Uniksie nie pamiętam żadnej sytuacji, która naprawdę przyniosłaby korzyści w przypadku potoku z wieloma wyjściami.

Można łączyć wiele wyjść do jednego strumienia dzisiaj, ale nie w każdym przeplatanego sposób (jak powinno Wyjścia cmd1i cmd2być przeplatane? Jeden wiersz z kolei? Po kolei pisanie 10 bajtów? Alternate „paragrafy” zdefiniowane w jakiś sposób? A jeśli po prostu nie robi” nie piszesz nic przez długi czas? to wszystko jest skomplikowane w obsłudze). Odbywa się to poprzez, na przykład (cmd1; cmd2; cmd3) | cmd4, programów cmd1, cmd2i cmd3są prowadzone jedna po drugiej, to wyjście jest wysyłany jako dane wejściowe cmd4.

vonbrand
źródło
3

W przypadku nakładającego się problemu w systemie Linux (z bashlub zshbez ksh93) możesz to zrobić jako:

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
)

Zwróć uwagę na użycie (...)zamiast, {...}aby uzyskać nowy proces przy każdej iteracji, abyśmy mogli mieć nowy fd 3 wskazujący na nowy auxfile. < /dev/fd/3jest sztuczką, aby uzyskać dostęp do tego teraz usuniętego pliku. Nie będzie działał na systemach innych niż Linux, na których < /dev/fd/3jest podobny, dup2(3, 0)dlatego fd 0 byłby otwarty w trybie tylko do zapisu z kursorem na końcu pliku.

Aby uniknąć rozwidlenia dla funkcji zagnieżdżonej, możesz zapisać go jako:

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
}

Powłoka zajmowałaby się tworzeniem kopii zapasowej fd 3 przy każdej iteracji. Ostatecznie skończyłyby się deskryptory plików wcześniej.

Chociaż przekonasz się, że bardziej efektywne jest to zrobić:

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

Oznacza to, że nie zagnieżdżaj przekierowań.

Stéphane Chazelas
źródło