tee + cat: use uma saída várias vezes e concatene os resultados

18

Se eu chamar algum comando, por exemplo, echoeu posso usar os resultados desse comando em vários outros comandos com tee. Exemplo:

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

Com o gato eu posso coletar os resultados de vários comandos. Exemplo:

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

Gostaria de poder fazer as duas coisas ao mesmo tempo, para poder teechamar esses comandos na saída de outra coisa (por exemplo, a echoque escrevi) e depois coletar todos os resultados em uma única saída com cat.

É importante manter os resultados em ordem, isto significa que as linhas na saída command1, command2e command3não devem ser interligados, mas ordenou que os comandos são (como acontece com cat).

Pode haver opções melhores do que cate, teemas essas são as que eu conheço até agora.

Quero evitar o uso de arquivos temporários porque o tamanho da entrada e da saída pode ser grande.

Como eu pude fazer isso?

PD: outro problema é que isso acontece em um loop, o que dificulta o manuseio de arquivos temporários. Este é o código atual que eu tenho e funciona para pequenos casos de teste, mas cria loops infinitos ao ler e escrever no auxfile de alguma forma que não entendo.

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

Leituras e escritos em auxfile parecem se sobrepor, fazendo com que tudo exploda.

Trylks
fonte
2
Quão grande estamos falando? Seus requisitos forçam tudo a ser mantido na memória. Manter os resultados em ordem significa que o comando1 deve ser concluído primeiro (por isso, presumivelmente, você leu toda a entrada e imprimiu toda a saída), antes que o comando2 e o comando3 possam iniciar o processamento (a menos que você deseje coletar sua saída na memória também).
Frostschutz 4/03/13
você está certo, a entrada e a saída do comando2 e do comando3 são muito grandes para serem mantidas na memória. Eu esperava que o uso do swap funcionasse melhor do que usar arquivos temporários. Outro problema que tenho é que isso acontece em um loop e torna o manuseio de arquivos ainda mais difícil. Estou usando um único arquivo, mas neste momento, por algum motivo, há alguma sobreposição na leitura e gravação do arquivo que faz com que ele cresça ad infinitum. Vou tentar atualizar a pergunta sem incomodá-lo com muitos detalhes.
Trylks
4
Você precisa usar arquivos temporários; para a entrada echo HelloWorld > file; (command1<file;command2<file;command3<file)ou para a saída echo | tee cmd1 cmd2 cmd3; cat cmd1-output cmd2-output cmd3-output. É assim que funciona - o tee pode dividir a entrada apenas se todos os comandos funcionarem e processarem em paralelo. se se dorme de comando (porque você não quer intercalação) ele vai simplesmente bloquear todos os comandos, de modo a evitar o preenchimento de memória com entrada ...
frostschutz

Respostas:

27

Você pode usar uma combinação do GNU stdbuf e peedo moreutils :

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

fazer xixi popen(3)essas 3 linhas de comando do shell e depois freada entrada efwrite nas três, que serão armazenadas em buffer até 1M.

A idéia é ter um buffer pelo menos tão grande quanto a entrada. Dessa maneira, mesmo que os três comandos sejam iniciados ao mesmo tempo, eles verão apenas a entrada de entrada quando os três comandos forem pee pcloseseqüenciais.

Em cada um pclose, peelibera o buffer para o comando e aguarda sua finalização. Isso garante que, enquanto aquelescmdx comandos não comecem a produzir nada antes de receberem qualquer entrada (e não bifurcem um processo que possa continuar produzindo após o retorno do pai), a saída dos três comandos não será intercalado.

Na verdade, é como usar um arquivo temporário na memória, com a desvantagem de os três comandos serem iniciados simultaneamente.

Para evitar iniciar os comandos simultaneamente, você pode escrever peecomo uma função shell:

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

Mas tome cuidado para que outras conchas zshfalhem na entrada binária com caracteres NUL.

Isso evita o uso de arquivos temporários, mas isso significa que toda a entrada é armazenada na memória.

De qualquer forma, você precisará armazenar a entrada em algum lugar, na memória ou em um arquivo temporário.

Na verdade, é uma pergunta bastante interessante, pois mostra o limite da ideia do Unix de ter várias ferramentas simples cooperando para uma única tarefa.

Aqui, gostaríamos de ter várias ferramentas para cooperar com a tarefa:

  • um comando de origem (aqui echo )
  • um comando do dispatcher (tee )
  • alguns comandos de filtro ( cmd1, cmd2,cmd3 )
  • e um comando de agregação ( cat).

Seria bom se todos pudessem rodar juntos ao mesmo tempo e trabalhar duro com os dados que devem processar assim que estiverem disponíveis.

No caso de um comando de filtro, é fácil:

src | tee | cmd1 | cat

Todos os comandos são executados simultaneamente, cmd1começa a coletar dados srcassim que estiverem disponíveis.

Agora, com três comandos de filtro, ainda podemos fazer o mesmo: iniciá-los simultaneamente e conectá-los aos tubos:

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

O que podemos fazer com relativa facilidade com pipes nomeados :

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'

(acima disso } 3<&0é para contornar o fato de &redirecionar stdinde /dev/nulle usamos <>para evitar a abertura dos tubos para bloquear até a outra extremidade (cat ) também seja aberta)

Ou, para evitar pipes nomeados, um pouco mais dolorosamente com o 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'

Agora, a pergunta é: quando todos os programas forem iniciados e conectados, os dados fluirão?

Temos duas restrições:

  • tee alimenta todas as suas saídas na mesma taxa, para que ele possa enviar dados apenas na taxa do tubo de saída mais lento.
  • cat só começará a ler a partir do segundo tubo (tubo 6 no desenho acima) quando todos os dados tiverem sido lidos no primeiro (5).

O que isso significa é que os dados não fluirão no tubo 6 até a cmd1conclusão. E, como no caso tr b Bacima, isso pode significar que os dados também não fluirão no tubo 3, o que significa que não fluirão em nenhum dos tubos 2, 3 ou 4, pois são teealimentados na taxa mais lenta de todos os 3.

Na prática, esses canais têm um tamanho não nulo; portanto, alguns dados conseguirão passar e, pelo menos no meu sistema, posso fazê-lo funcionar até:

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

Além disso, com

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

Temos um impasse, onde estamos nessa situação:

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

Enchemos os tubos 3 e 6 (64 kiB cada). teeleu que byte extra, ele alimentou-a cmd1, mas

  • agora está bloqueado a escrita no tubo 3, à espera de cmd2esvaziá-lo
  • cmd2não pode esvaziá-lo porque está bloqueado na escrita no tubo 6, esperando catesvaziá-lo
  • cat não pode esvaziá-lo porque está aguardando até que não haja mais entrada no canal 5.
  • cmd1não posso dizer que catnão há mais entrada porque ela está esperando por mais entradas tee.
  • e teenão posso dizer que cmd1não há mais entrada porque está bloqueada ... e assim por diante.

Temos um loop de dependência e, portanto, um impasse.

Agora, qual é a solução? Tubos maiores 3 e 4 (grandes o suficiente para conter toda srca produção) seriam suficientes . Podemos fazer isso, por exemplo, inserindo pv -qB 1Gentre teee cmd2/3onde podemos pvarmazenar até 1 G de dados aguardando cmd2e cmd3lendo-os. Isso significaria duas coisas:

  1. que está usando potencialmente muita memória e, além disso, duplicando-a
  2. está falhando em ter todos os 3 comandos cooperados porque cmd2, na realidade, só começaria a processar dados quando o cmd1 terminasse.

Uma solução para o segundo problema seria aumentar também os tubos 6 e 7. Supondo que cmd2e cmd3produzindo tanto quanto eles consomem, isso não consumiria mais memória.

A única maneira de evitar a duplicação dos dados (no primeiro problema) seria implementar a retenção de dados no próprio despachante, ou seja, implementar uma variação teeque possa alimentar os dados na taxa da saída mais rápida (mantendo os dados para alimentar o mais lentos no seu próprio ritmo). Não é realmente trivial.

Portanto, no final, o melhor que podemos obter razoavelmente sem programação é provavelmente algo como (sintaxe 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
fonte
Você está certo, o impasse é o maior problema que encontrei até agora para evitar o uso de arquivos temporários. Esses arquivos parecem bastante rápidos, porém, não sei se eles estão sendo armazenados em cache em algum lugar, eu tinha medo dos tempos de acesso ao disco, mas eles parecem razoáveis ​​até agora.
Trylks
6
Um extra +1 para a arte agradável ASCII :-)
Kurt Pfeifle
3

O que você propõe não pode ser feito facilmente com nenhum comando existente e, de qualquer maneira, não faz muito sentido. A idéia de tubos ( |em Unix / Linux) é que cmd1 | cmd2na cmd1saída de gravações (no máximo) até que um buffer de memória preenchimentos, e, em seguida, cmd2executa a leitura de dados a partir do buffer (no máximo) até que ele está vazio. Ou seja, cmd1e é cmd2executado ao mesmo tempo, nunca é necessário ter mais do que uma quantidade limitada de dados "em voo" entre eles. Se você deseja conectar várias entradas a uma única saída, se um dos leitores ficar atrás dos outros, você interrompe os outros (qual é o objetivo de executar em paralelo, então?) Ou esconde a saída que o retardatário ainda não leu (qual é o sentido de não ter um arquivo intermediário?).

Nos meus quase 30 anos de experiência no Unix, não me lembro de nenhuma situação que realmente se beneficiasse com um canal de saída múltipla.

Hoje, você pode combinar várias saídas em um fluxo, mas não de maneira intercalada (como as saídas cmd1e cmd2devem ser intercaladas? Uma linha por vez? Se revezam escrevendo 10 bytes? "Parágrafos" alternativos definidos de alguma forma? E se apenas não escrever algo por um longo tempo - tudo isso é complexo de lidar). É feito através, por exemplo (cmd1; cmd2; cmd3) | cmd4, os programas cmd1, cmd2e cmd3são executados um após o outro, a saída é enviada como entrada para cmd4.

vonbrand
fonte
3

Para o seu problema de sobreposição, no Linux (e com bashou zshnão ksh93), você pode fazer o seguinte:

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
)

Observe o uso de, em (...)vez de, {...}para obter um novo processo a cada iteração, para que possamos ter um novo fd 3 apontando para um novo auxfile. < /dev/fd/3é um truque para acessar o arquivo excluído agora. Ele não funcionará em sistemas diferentes do Linux onde < /dev/fd/3é semelhante dup2(3, 0)e, portanto, o fd 0 seria aberto no modo somente gravação com o cursor no final do arquivo.

Para evitar a bifurcação da função aninhada, você pode escrevê-la como:

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
}

O shell cuidaria de fazer backup do fd 3 em cada iteração. Você acabaria ficando sem descritores de arquivo mais cedo.

Embora você ache mais eficiente fazê-lo como:

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

Ou seja, não aninhe os redirecionamentos.

Stéphane Chazelas
fonte