Como posso enviar o stdout de um processo para vários processos usando pipes (de preferência sem nome) no Unix (ou Windows)?

StackOverflow https://stackoverflow.com/questions/60942

Pergunta

Gostaria de redirecionar o stdout do processo proc1 para dois processos proc2 e proc3:

         proc2 -> stdout
       /
 proc1
       \ 
         proc3 -> stdout

tentei

 proc1 | (proc2 & proc3)

mas não parece funcionar, ou seja,

 echo 123 | (tr 1 a & tr 1 b)

escreve

 b23

para stdout em vez de

 a23
 b23
Foi útil?

Solução

Nota do editor:
- >(…) é um substituição de processo Aquilo é um recurso de shell fora do padrão de alguns Conchas compatíveis com POSIX: bash, ksh, zsh.
- Esta resposta envia acidentalmente a saída da substituição do processo de saída através do pipeline também: echo 123 | tee >(tr 1 a) | tr 1 b.
- A saída das substituições do processo será intercalada de forma imprevisível e, exceto em zsh, o pipeline pode terminar antes dos comandos dentro >(…) fazer.

No unix (ou em um mac), use o tee comando:

$ echo 123 | tee >(tr 1 a) >(tr 1 b) >/dev/null
b23
a23

Normalmente você usaria tee Para redirecionar a saída para vários arquivos, mas usando> (...), você pode redirecionar para outro processo.Então, em geral,

$ proc1 | tee >(proc2) ... >(procN-1) >(procN) >/dev/null

fará o que você quiser.

No Windows, não acho que o shell integrado tenha um equivalente.da Microsoft Windows PowerShell tem um tee comando embora.

Outras dicas

Como disse DF, bash permite usar o >(…) construa executando um comando no lugar de um nome de arquivo.(Há também o <(…) construir para substituir o saída de outro comando no lugar de um nome de arquivo, mas isso é irrelevante agora, menciono-o apenas para completar).

Se você não possui o bash ou está executando em um sistema com uma versão mais antiga do bash, você pode fazer manualmente o que o bash faz, usando arquivos FIFO.

A maneira genérica de conseguir o que você deseja é:

  • decida quantos processos devem receber a saída do seu comando e crie quantos FIFOs, de preferência em uma pasta temporária global:
    subprocesses="a b c d"
    mypid=$$
    for i in $subprocesses # this way we are compatible with all sh-derived shells  
    do
        mkfifo /tmp/pipe.$mypid.$i
    done
  • inicie todos os seus subprocessos aguardando entrada dos FIFOs:
    for i in $subprocesses
    do
        tr 1 $i </tmp/pipe.$mypid.$i & # background!
    done
  • execute seu comando indo para os FIFOs:
    proc1 | tee $(for i in $subprocesses; do echo /tmp/pipe.$mypid.$i; done)
  • finalmente, remova os FIFOs:
    for i in $subprocesses; do rm /tmp/pipe.$mypid.$i; done

OBSERVAÇÃO:por motivos de compatibilidade, eu faria o $(…) com crases, mas não consegui escrever esta resposta (as crases são usadas no SO).Normalmente, o $(…) tem idade suficiente para funcionar mesmo em versões antigas do ksh, mas se não funcionar, inclua o parte entre aspas.

Unix (bash, ksh, zsh)

resposta de dF. contém o semente de uma resposta baseada em tee e saída substituições de processo
(>(...)) que posso ou não posso trabalho, dependendo de suas necessidades:

Observe que as substituições de processo são uma fora do padrão recurso que (principalmente) falhas de fatos de posix, como dash (que funciona como /bin/sh no Ubuntu, por exemplo), faça não apoiar.Direcionamento de scripts de shell /bin/sh deve não confie neles.

echo 123 | tee >(tr 1 a) >(tr 1 b) >/dev/null

O armadilhas desta abordagem são:

  • comportamento de saída imprevisível e assíncrono:os fluxos de saída dos comandos dentro das substituições do processo de saída >(...) intercalar de maneiras imprevisíveis.

  • Em bash e ksh (em oposição a zsh - mas veja a exceção abaixo):

    • a saída pode chegar depois o comando foi concluído.
    • comandos subsequentes podem começar a ser executados antes os comandos nas substituições do processo terminaram - bash e ksh fazer não aguarde até que os processos gerados pela substituição do processo de saída terminem, pelo menos por padrão.
    • jmb coloca isso bem em um comentário sobre a resposta do dF.:

esteja ciente de que os comandos começaram dentro >(...) estão dissociados do shell original e você não pode determinar facilmente quando eles terminam;o tee terminará depois de escrever tudo, mas os processos substituídos ainda consumirão os dados de vários buffers no kernel e E/S de arquivo, além de qualquer tempo gasto no manuseio interno de dados.Você pode encontrar condições de corrida se seu shell externo depender de qualquer coisa produzida pelos subprocessos.

  • zsh é a única concha que faz por padrão, espere que os processos executados nas substituições do processo de saída terminem, exceto se for stderr que é redirecionado para um (2> >(...)).

  • ksh (pelo menos a partir da versão 93u+) permite o uso de argumentos sem argumentos wait esperar que os processos gerados pela substituição do processo de saída terminem.
    Observe que em uma sessão interativa isso poderia resultar na espera de qualquer pendência trabalhos em segundo plano também, no entanto.

  • bash v4.4+ pode esperar pelo mais recentemente lançou a substituição do processo de saída com wait $!, mas sem argumentos wait faz não funcionar, tornando isso inadequado para um comando com múltiplo substituições de processos de saída.

  • No entanto, bash e ksh pode ser forçado esperar canalizando o comando para | cat, mas observe que isso faz com que o comando seja executado em um subnível. Ressalvas:

    • ksh (a partir de ksh 93u+) não suporta envio stderr para uma substituição do processo de saída (2> >(...));tal tentativa é silenciosamente ignorado.

    • Enquanto zsh é (louvavelmente) síncrono por padrão com o (muito mais comum) saída padrão substituições no processo de saída, até mesmo o | cat técnica não pode torná-los sincronizados com stderr substituições do processo de saída (2> >(...)).

  • No entanto, mesmo se você garantir execução síncrona, o problema de saída imprevisivelmente intercalada restos.

O seguinte comando, quando executado em bash ou ksh, ilustra os comportamentos problemáticos (talvez seja necessário executá-lo várias vezes para ver ambos sintomas):O AFTER normalmente imprimirá antes a saída das substituições de saída, e a saída desta última pode ser intercalada de forma imprevisível.

printf 'line %s\n' {1..30} | tee >(cat -n) >(cat -n) >/dev/null; echo AFTER

Resumidamente:

  • Garantindo uma sequência de saída específica por comando:

    • Nenhum bash nem ksh nem zsh apoiar isso.
  • Execução síncrona:

    • Viável, exceto com stderrsubstituições de processos de saída de origem:
      • Em zsh, eles estão invariavelmente assíncrono.
      • Em ksh, eles não funciona de jeito nenhum.

Se você consegue conviver com essas limitações, usar substituições de processos de saída é uma opção viável (por exemplo, se todos eles gravarem em arquivos de saída separados).


Observe que tzot é uma solução muito mais complicada, mas potencialmente compatível com POSIX também exibe comportamento de saída imprevisível;no entanto, usando wait você pode garantir que os comandos subsequentes não comecem a ser executados até que todos os processos em segundo plano sejam concluídos.
Veja abaixo para uma implementação de saída serializada mais robusta e síncrona.


A única direto bash solução com comportamento de saída previsível é o seguinte, que, no entanto, é proibitivamente lento com grandes conjuntos de entradas, porque os shell loops são inerentemente lentos.
Observe também que isso suplentes as linhas de saída dos comandos de destino.

while IFS= read -r line; do 
  tr 1 a <<<"$line"
  tr 1 b <<<"$line"
done < <(echo '123')

Unix (usando GNU Paralelo)

Instalando GNU parallel permite um solução robusta com saída serializada (por comando) que permite adicionalmente execução paralela:

$ echo '123' | parallel --pipe --tee {} ::: 'tr 1 a' 'tr 1 b'
a23
b23

parallel por padrão garante que a saída dos diferentes comandos não seja intercalada (esse comportamento pode ser modificado - consulte man parallel).

Observação:Algumas distribuições Linux vêm com um diferente parallel utilitário, que não funcionará com o comando acima;usar parallel --version para determinar qual deles, se houver, você possui.


janelas

Resposta útil de Jay Bazuzi mostra como fazer isso em PowerShell.Dito isto:sua resposta é análoga ao looping bash resposta acima, será proibitivamente lento com grandes conjuntos de entradas e também suplentes as linhas de saída dos comandos de destino.



bashsolução Unix baseada em , mas portátil, com execução síncrona e serialização de saída

O que se segue é uma implementação simples, mas razoavelmente robusta, da abordagem apresentada em resposta de tzot que fornece adicionalmente:

  • execução síncrona
  • saída serializada (agrupada)

Embora não seja estritamente compatível com POSIX, porque é um bash roteiro, deveria ser portátil para qualquer plataforma Unix que tenha bash.

Observação:Você pode encontrar uma implementação mais completa lançada sob a licença do MIT em esta essência.

Se você salvar o código abaixo como script fanout, torne-o executável e coloque-o no seu PATH, o comando da pergunta funcionaria da seguinte forma:

$ echo 123 | fanout 'tr 1 a' 'tr 1 b'
# tr 1 a
a23
# tr 1 b
b23

fanout código fonte do script:

#!/usr/bin/env bash

# The commands to pipe to, passed as a single string each.
aCmds=( "$@" )

# Create a temp. directory to hold all FIFOs and captured output.
tmpDir="${TMPDIR:-/tmp}/$kTHIS_NAME-$$-$(date +%s)-$RANDOM"
mkdir "$tmpDir" || exit
# Set up a trap that automatically removes the temp dir. when this script
# exits.
trap 'rm -rf "$tmpDir"' EXIT 

# Determine the number padding for the sequential FIFO / output-capture names, 
# so that *alphabetic* sorting, as done by *globbing* is equivalent to
# *numerical* sorting.
maxNdx=$(( $# - 1 ))
fmtString="%0${#maxNdx}d"

# Create the FIFO and output-capture filename arrays
aFifos=() aOutFiles=()
for (( i = 0; i <= maxNdx; ++i )); do
  printf -v suffix "$fmtString" $i
  aFifos[i]="$tmpDir/fifo-$suffix"
  aOutFiles[i]="$tmpDir/out-$suffix"
done

# Create the FIFOs.
mkfifo "${aFifos[@]}" || exit

# Start all commands in the background, each reading from a dedicated FIFO.
for (( i = 0; i <= maxNdx; ++i )); do
  fifo=${aFifos[i]}
  outFile=${aOutFiles[i]}
  cmd=${aCmds[i]}
  printf '# %s\n' "$cmd" > "$outFile"
  eval "$cmd" < "$fifo" >> "$outFile" &
done

# Now tee stdin to all FIFOs.
tee "${aFifos[@]}" >/dev/null || exit

# Wait for all background processes to finish.
wait

# Print all captured stdout output, grouped by target command, in sequences.
cat "${aOutFiles[@]}"

Desde @dF:mencionei que o PowerShell tem tee, pensei em mostrar uma maneira de fazer isso no PowerShell.

PS > "123" | % { 
    $_.Replace( "1", "a"), 
    $_.Replace( "2", "b" ) 
}

a23
1b3

Observe que cada objeto proveniente do primeiro comando é processado antes da criação do próximo objeto.Isso pode permitir o dimensionamento para entradas muito grandes.

outra maneira de fazer seria,

 eval `echo '&& echo 123 |'{'tr 1 a','tr 1 b'} | sed -n 's/^&&//gp'`

saída:

a23
b23

não há necessidade de criar um subshell aqui

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top