tee + cat: usa un output più volte e poi concatena i risultati


18

Se chiamo qualche comando, per esempio un echoposso usare i risultati di quel comando in molti altri comandi con tee. Esempio:

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

Con cat posso raccogliere i risultati di diversi comandi. Esempio:

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

Mi piacerebbe essere in grado di fare entrambe le cose contemporaneamente, in modo da poter usare teeper chiamare quei comandi sull'output di qualcos'altro (ad esempio quello echoche ho scritto) e quindi raccogliere tutti i risultati su un singolo output con cat.

È importante mantenere i risultati in ordine, ciò significa che le linee nell'output di command1, command2e command3non dovrebbero essere intrecciate, ma ordinate come sono i comandi (come accade con cat).

Potrebbero esserci opzioni migliori di cate, teema quelle sono quelle che conosco finora.

Voglio evitare di utilizzare file temporanei perché le dimensioni dell'input e dell'output potrebbero essere grandi.

Come potrei farlo?

PD: un altro problema è che ciò accade in un ciclo, il che rende più difficile la gestione dei file temporanei. Questo è il codice attuale che ho e funziona per piccole prove, ma crea loop infiniti durante la lettura e la scrittura dall'audio in un modo che non capisco.

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

Letture e scritti in auxfile sembrano sovrapporsi, facendo esplodere tutto.


2
Di quanto stiamo parlando? I tuoi requisiti impongono che tutto venga conservato in memoria. Mantenere i risultati in ordine significa che command1 deve essere completato per primo (quindi ha presumibilmente letto l'intero input e stampato l'intero output), prima che command2 e command3 possano persino iniziare l'elaborazione (a meno che non si desideri raccogliere anche i propri output in memoria all'inizio).
frostschutz,

hai ragione, l'input e l'output di command2 e command3 sono troppo grandi per essere conservati in memoria. Mi aspettavo che l'uso di swap avrebbe funzionato meglio dell'utilizzo di file temporanei. Un altro problema che ho è che questo accade in un ciclo e che rende ancora più difficile la gestione dei file. Sto usando un singolo file ma in questo momento per qualche ragione c'è una certa sovrapposizione nella lettura e nella scrittura dal file che lo fa crescere all'infinito. Proverò ad aggiornare la domanda senza annoiarti con troppi dettagli.
Trylks

4
Devi usare file temporanei; sia per l'input echo HelloWorld > file; (command1<file;command2<file;command3<file)che per l'output echo | tee cmd1 cmd2 cmd3; cat cmd1-output cmd2-output cmd3-output. Funziona così: tee può fork input solo se tutti i comandi funzionano ed elaborano in parallelo. se un comando dorme (perché non vuoi intercalare) bloccherà semplicemente tutti i comandi, in modo da impedire il riempimento della memoria con input ...
frostschutz

Risposte:


27

Puoi usare una combinazione di GNU stdbuf e peedi moreutils :

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

fai pipì su popen(3)quelle 3 righe di comando della shell e poi freads l'input e fwrites tutte e tre, che saranno bufferizzate fino a 1M.

L'idea è di avere un buffer almeno grande quanto l'input. In questo modo, anche se i tre comandi vengono avviati contemporaneamente, vedranno l'ingresso solo quando pee pclosei tre comandi vengono eseguiti in sequenza.

Su ciascuno pclose, peesvuota il buffer sul comando e attende la sua conclusione. Ciò garantisce che fino a quando questi cmdxcomandi non iniziano a emettere nulla prima di aver ricevuto alcun input (e non fork un processo che potrebbe continuare a essere emesso dopo che il loro genitore è tornato), l'output dei tre comandi non sarà intercalati.

In effetti, è un po 'come usare un file temporaneo in memoria, con lo svantaggio che i 3 comandi vengono avviati contemporaneamente.

Per evitare di avviare i comandi contemporaneamente, è possibile scrivere peecome una funzione shell:

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

Ma attenzione che shell diverse da quelle zshfallirebbero per l'input binario con caratteri NUL.

Ciò evita di utilizzare file temporanei, ma ciò significa che l'intero input è archiviato in memoria.

In ogni caso, dovrai archiviare l'input da qualche parte, in memoria o in un file temporaneo.

In realtà, è una domanda piuttosto interessante, in quanto ci mostra il limite dell'idea Unix di avere diversi strumenti semplici cooperare per un singolo compito.

Qui, vorremmo che diversi strumenti collaborassero all'attività:

  • un comando sorgente (qui echo)
  • un comando dispatcher ( tee)
  • alcuni comandi filtro ( cmd1, cmd2, cmd3)
  • e un comando di aggregazione ( cat).

Sarebbe bello se potessero funzionare tutti insieme contemporaneamente e fare il loro duro lavoro sui dati che dovrebbero elaborare non appena saranno disponibili.

Nel caso di un comando di filtro, è facile:

src | tee | cmd1 | cat

Tutti i comandi vengono eseguiti contemporaneamente, cmd1inizia a sgranocchiare i dati srcnon appena sono disponibili.

Ora, con tre comandi di filtro, possiamo ancora fare lo stesso: avviarli contemporaneamente e collegarli con i tubi:

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

Cosa che possiamo fare relativamente facilmente con le pipe denominate :

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'

(sopra } 3<&0è per aggirare il fatto che i &reindirizzamenti stdinda /dev/null, e usiamo <>per evitare l'apertura dei tubi per bloccare fino a quando non catsi apre anche l'altra estremità ( ))

O per evitare pipe nominate, un po 'più dolorosamente con 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'

Ora, la domanda è: una volta che tutti i programmi sono stati avviati e collegati, i dati fluiranno?

Abbiamo due controindicazioni:

  • tee alimenta tutti i suoi output alla stessa velocità, quindi può inviare i dati solo alla velocità del suo tubo di uscita più lento.
  • cat inizierà la lettura dalla seconda pipe (pipe 6 nel disegno sopra) solo quando tutti i dati sono stati letti dalla prima (5).

Ciò significa che i dati non fluiranno nel tubo 6 fino al cmd1termine. E, come nel caso di quanto tr b Bsopra, ciò può significare che i dati non fluiranno neanche nella tubazione 3, il che significa che non fluiranno in nessuna delle tubazioni 2, 3 o 4 poiché si teealimenta alla velocità più lenta di tutte e 3.

In pratica quelle pipe hanno una dimensione non nulla, quindi alcuni dati riusciranno a passare, e almeno sul mio sistema, posso farlo funzionare fino a:

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

Oltre a ciò, con

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

Abbiamo un punto morto, in cui siamo in questa situazione:

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

Abbiamo riempito i tubi 3 e 6 (64 kB ciascuno). teeha letto quel byte extra, lo ha alimentato cmd1, ma

  • ora è bloccato scrivendo sulla pipe 3 mentre sta aspettando cmd2di svuotarlo
  • cmd2impossibile svuotarlo perché è bloccato nella scrittura sulla pipe 6, in attesa catdi svuotarlo
  • cat non può svuotarlo perché è in attesa fino a quando non ci sarà più input sulla pipe 5.
  • cmd1non posso dire che non catci sono più input perché è in attesa di ulteriori input da tee.
  • e teenon posso dire che cmd1non c'è più input perché è bloccato ... e così via.

Abbiamo un ciclo di dipendenza e quindi un deadlock.

Ora, qual è la soluzione? I tubi più grandi 3 e 4 (abbastanza grandi da contenere tutta srcl'uscita) lo farebbero. Potremmo farlo ad esempio inserendo pv -qB 1Gtra teee cmd2/3dove pvpotrebbero archiviare fino a 1G di dati in attesa cmd2e cmd3leggerli. Ciò significherebbe due cose però:

  1. che utilizza potenzialmente molta memoria e, inoltre, la duplica
  2. questo non riesce a far cooperare tutti e 3 i comandi perché cmd2in realtà inizierebbe a elaborare i dati solo quando cmd1 è terminato.

Una soluzione al secondo problema sarebbe quella di ingrandire anche i tubi 6 e 7. Supponendo che cmd2e cmd3produrre tanto output quanto consumano, ciò non consumerebbe più memoria.

L'unico modo per evitare la duplicazione dei dati (nel primo problema) sarebbe implementare la conservazione dei dati nel dispatcher stesso, ovvero implementare una variazione in teegrado di alimentare i dati alla velocità dell'output più veloce (mantenendo i dati per alimentare il quelli più lenti al loro ritmo). Non proprio banale.

Quindi, alla fine, il meglio che possiamo ragionevolmente ottenere senza programmazione è probabilmente qualcosa di simile (sintassi 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

Hai ragione, il deadlock è il problema più grande che ho trovato finora per evitare l'uso di file temporanei. Questi file sembrano essere abbastanza veloci, tuttavia, non so se vengono memorizzati nella cache da qualche parte, avevo paura dei tempi di accesso al disco, ma finora sembrano ragionevoli.
Trylks,

6
Un extra +1 per la bella arte ASCII :-)
Kurt Pfeifle

3

Quello che proponi non può essere fatto facilmente con nessun comando esistente, e comunque non ha molto senso. L'idea di tubi ( |in Unix / Linux) è che nel cmd1 | cmd2l' cmd1uscita scrive (al massimo) finché un buffer di memoria riempimenti, quindi cmd2, che gira la lettura dei dati dal buffer (al massimo) fino a quando è vuoto. Vale a dire, cmd1ed cmd2eseguire allo stesso tempo, non è mai necessario avere più di una quantità limitata di dati "in volo" tra di loro. Se si desidera collegare più ingressi a un singolo output, se uno dei lettori è in ritardo rispetto agli altri o si fermano gli altri (qual è il punto di correre in parallelo, allora?) O si nasconde l'output che il ritardatario non ha ancora letto (qual è il punto di non avere un file intermedio allora?). più complesso.

Nei miei quasi 30 anni di esperienza in Unix, non ricordo alcuna situazione che avrebbe davvero beneficiato di un simile tubo a più uscite.

È possibile combinare più output in un flusso oggi, semplicemente non in alcun modo interlacciato (come dovrebbero essere gli output di cmd1e cmd2essere interfogliati? Una riga a turno? A turno scrivendo 10 byte? Alternando "paragrafi" definiti in qualche modo? E se uno semplicemente non lo fa ' scrivere qualcosa per molto tempo? tutto ciò è complesso da gestire). Si è fatto, ad esempio (cmd1; cmd2; cmd3) | cmd4, i programmi cmd1, cmd2e cmd3vengono eseguiti uno dopo l'altro, l'output viene inviato come input cmd4.


3

Per il tuo problema di sovrapposizione, su Linux (e con basho zshma non con ksh93), puoi farlo come:

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
)

Nota l'uso di (...)invece di {...}ottenere un nuovo processo ad ogni iterazione in modo che possiamo avere un nuovo fd 3 che punta a un nuovo auxfile. < /dev/fd/3è un trucco per accedere a quel file ora eliminato. Non funzionerà su sistemi diversi da Linux dove < /dev/fd/3è simile dup2(3, 0)e quindi fd 0 sarebbe aperto in modalità di sola scrittura con il cursore alla fine del file.

Per evitare il fork per la funzione annidata, è possibile scrivere come:

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
}

La shell si occuperebbe di eseguire il backup di fd 3 ad ogni iterazione. Ma finiresti per esaurire i descrittori di file prima.

Anche se troverai che è più efficiente farlo come:

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

Cioè, non annidare i reindirizzamenti.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.