A saída de substituição do processo está fora de ordem

16

o

echo one; echo two > >(cat); echo three; 

comando fornece saída inesperada.

Eu li o seguinte: Como a substituição de processo é implementada no bash? e muitos outros artigos sobre substituição de processos na internet, mas não entendo por que ela se comporta dessa maneira.

Saída esperada:

one
two
three

Saída real:

prompt$ echo one; echo two > >(cat); echo three;
one
three
prompt$ two

Além disso, esses dois comandos devem ser equivalentes do meu ponto de vista, mas não:

##### first command - the pipe is used.
prompt$ seq 1 5 | cat
1
2
3
4
5
##### second command - the process substitution and redirection are used.
prompt$ seq 1 5 > >(cat)
prompt$ 1
2
3
4
5

Por que eu acho que eles devem ser os mesmos? Porque, ambos conectam a seqsaída à catentrada através do canal anônimo - Wikipedia, Substituição de processo .

Pergunta: Por que se comporta dessa maneira? Onde está o meu erro? A resposta abrangente é desejada (com explicação de como o bashfaz sob o capô).

MiniMax
fonte
2
Mesmo que não está tão claro, à primeira vista, é realmente uma duplicata de espera do bash para o processo em substituição processo mesmo que o comando é inválido
Stéphane Chazelas
2
Na verdade, seria melhor se essa outra pergunta fosse marcada como duplicada para esta, pois esta é mais direta. É por isso que copiei minha resposta lá.
Stéphane Chazelas

Respostas:

21

Sim, bashcomo em ksh(de onde o recurso vem), os processos dentro da substituição do processo não são esperados (antes de executar o próximo comando no script).

para <(...)um, geralmente é bom como em:

cmd1 <(cmd2)

o shell estará aguardando cmd1e cmd1normalmente estará aguardando em cmd2virtude da leitura até o final do arquivo no tubo substituído, e esse fim do arquivo normalmente acontece quando cmd2morre. Essa é a mesma razão várias conchas (não bash) não se incomode à espera de cmd2no cmd2 | cmd1.

Pois cmd1 >(cmd2), no entanto, esse geralmente não é o caso, pois é mais o cmd2que normalmente espera por cmd1isso, e geralmente sai depois.

Isso está fixo no zshque aguarda por cmd2lá (mas não se você o escrever como cmd1 > >(cmd2)e cmd1não estiver embutido, use {cmd1} > >(cmd2)como documentado ).

kshnão espera por padrão, mas permite que você espere por ele com o waitbuilt-in (também disponibiliza o pid $!, embora isso não ajude se você o fizer cmd1 >(cmd2) >(cmd3))

rc(com a cmd1 >{cmd2}sintaxe), o mesmo que com a kshexceção de que você pode obter os pids de todos os processos em segundo plano $apids.

es(também com cmd1 >{cmd2}) aguarda o cmd2like in zshe também aguarda redirecionamentos cmd2no <{cmd2}processo.

bashtorna disponível o pid cmd2(ou mais exatamente do subshell, como ele é executado cmd2em um processo filho desse subshell, mesmo que seja o último comando) $!, mas não deixa você esperar por ele.

Se você precisar usar bash, poderá solucionar o problema usando um comando que aguardará os dois comandos com:

{ { cmd1 >(cmd2); } 3>&1 >&4 4>&- | cat; } 4>&1

Isso faz os dois cmd1e cmd2tem seu fd 3 aberto em um cano. cataguardará o final do arquivo na outra extremidade e, portanto, normalmente somente sairá quando ambos cmd1e cmd2estiverem mortos. E o shell aguardará esse catcomando. Você pode ver isso como uma rede para capturar o término de todos os processos em segundo plano (você pode usá-lo para outras coisas iniciadas em segundo plano, como &coprocs ou mesmo comandos que são em segundo plano, desde que não fechem todos os descritores de arquivos, como os daemons normalmente fazem )

Observe que, graças ao processo desperdiçado do subshell mencionado acima, ele funciona mesmo se cmd2fechar o fd 3 (os comandos geralmente não fazem isso, mas alguns gostam sudoou sshfazem). Versões futuras do bashpodem eventualmente fazer a otimização como em outros shells. Então você precisaria de algo como:

{ { cmd1 >(sudo cmd2; exit); } 3>&1 >&4 4>&- | cat; } 4>&1

Para garantir que ainda haja um processo de shell extra com esse fd 3 aberto aguardando esse sudocomando.

Observe que catnão lerá nada (já que os processos não gravam em seu fd 3). Está lá apenas para sincronização. Ele fará apenas uma read()chamada do sistema que retornará sem nada no final.

Na verdade, você pode evitar a execução catusando uma substituição de comando para fazer a sincronização de pipe:

{ unused=$( { cmd1 >(cmd2); } 3>&1 >&4 4>&-); } 4>&1

Desta vez, é o shell, em vez disso, catque está lendo no canal, cuja outra extremidade está aberta nos fd 3 de cmd1e cmd2. Estamos usando uma atribuição de variável para que o status de saída de cmd1esteja disponível em $?.

Ou você pode fazer a substituição do processo manualmente, e então você pode até usar o sistema, shpois isso se tornaria a sintaxe padrão do shell:

{ cmd1 /dev/fd/3 3>&1 >&4 4>&- | cmd2 4>&-; } 4>&1

observe, como observado anteriormente, que nem todas as shimplementações esperariam cmd1após a cmd2conclusão (embora isso seja melhor do que o contrário). Nesse momento, $?contém o status de saída de cmd2; embora bashe zshdisponibilize cmd1o status de saída em ${PIPESTATUS[0]}e $pipestatus[1]respectivamente (consulte também a pipefailopção em algumas conchas para que $?possamos relatar a falha de outros componentes do tubo que não o anterior)

Observe que yashhá problemas semelhantes com o recurso de redirecionamento de processo . cmd1 >(cmd2)seria escrito cmd1 /dev/fd/3 3>(cmd2)lá. Mas cmd2não é esperado e você também não pode waitesperar por isso, e seu pid também não é disponibilizado na $!variável. Você usaria as mesmas soluções alternativas de bash.

Stéphane Chazelas
fonte
Primeiramente, tentei echo one; { { echo two > >(cat); } 3>&1 >&4 4>&- | cat; } 4>&1; echo three;, depois simplifiquei para o echo one; echo two > >(cat) | cat; echo three;e ele gera valores na ordem certa também. Todas essas manipulações de descritores 3>&1 >&4 4>&-são necessárias? Além disso, eu não entendo isso >&4 4>&- somos redirecionados stdoutpara o quarto fd, fechando o quarto fd e depois usá- 4>&1lo novamente . Por que é necessário e como funciona? Pode ser, eu devo criar uma nova pergunta sobre esse tópico?
MiniMax
11
@MiniMax, mas aí, você está afetando o stdout de cmd1e cmd2, o objetivo da pequena dança com o descritor de arquivo é restaurar os originais e usar apenas o pipe extra para a espera, em vez de também canalizar a saída dos comandos.
Stéphane Chazelas 11/11
@MiniMax Demorei um pouco para entender, eu não tinha os canos em um nível tão baixo antes. O mais à direita 4>&1cria um descritor de arquivo (fd) 4 para a lista de comandos de chaves externas e a torna igual ao stdout das chaves externas. As chaves internas têm o stdin / stdout / stderr configurado automaticamente para conectar-se às chaves externas. No entanto, 3>&1faz com que o fd 3 se conecte ao stdin dos aparelhos externos. >&4faz com que o stdout do aparelho interno se conecte ao aparelho externo fd 4 (o que criamos antes). 4>&-fecha o fd 4 do aparelho interno (Como o stdout do aparelho interno já está conectado ao fd 4 do aparelho externo).
Nicholas Pipitone
@MiniMax A parte confusa era a parte da direita para a esquerda, 4>&1é executada primeiro, antes dos outros redirecionamentos, para que você não "use novamente 4>&1". No geral, o aparelho interno está enviando dados para seu stdout, que foi sobrescrito com o valor de fd 4 fornecido. O fd 4 que o aparelho interno recebeu, é o fd 4 do aparelho externo, que é igual ao stdout original do aparelho externo.
Nicholas Pipitone
O Bash faz parecer que 4>5significa "4 vai para 5", mas realmente "fd 4 é substituído por fd 5". E antes da execução, o fd 0/1/2 é conectado automaticamente (junto com qualquer fd do shell externo), e você pode substituí-los conforme desejar. Essa é pelo menos a minha interpretação da documentação do bash. Se você entendeu algo mais disso , lmk.
25418 Nicholas Pipitone
4

Você pode canalizar o segundo comando para outro cat, o que aguardará até que o canal de entrada seja fechado. Ex:

prompt$ echo one; echo two > >(cat) | cat; echo three;
one
two
three
prompt$

Curto e simples.

==========

Por mais simples que pareça, muita coisa está acontecendo nos bastidores. Você pode ignorar o restante da resposta se não estiver interessado em como isso funciona.

Quando você tem echo two > >(cat); echo three, >(cat)é bifurcado pelo shell interativo e é executado independentemente de echo two. Assim, echo twotermina e depois echo threeé executado, mas antes dos >(cat)acabamentos. Quando bashobtém dados de >(cat)quando não os esperava (alguns milissegundos mais tarde), fornece uma situação semelhante a prompt, em que você precisa acessar a nova linha para voltar ao terminal (o mesmo que se outro usuário o tivesse mesgeditado).

No entanto, dado echo two > >(cat) | cat; echo three, duas sub-conchas são geradas (conforme a documentação do |símbolo).

Um subshell chamado A é para echo two > >(cat)e um subshell chamado B é para cat. A é conectado automaticamente a B (stdout de A é stdin de B). Então, echo twoe >(cat)comece a executar. >(cat)stdout de é definido como stdout de A, que é igual ao stdin de B. Depois de echo twoacabamentos, A saídas, fechando sua stdout. No entanto, >(cat)ainda está mantendo a referência ao stdin de B. O catstdin do segundo está segurando o stdin de B e isso catnão sairá até que ele veja um EOF. Um EOF é fornecido apenas quando ninguém mais tem o arquivo aberto no modo de gravação, portanto >(cat), o stdout está bloqueando o segundo cat. B permanece esperando nesse segundo cat. Desde que echo twosaiu, >(cat)finalmente obtém um EOF, então>(cat)libera seu buffer e sai. Ninguém mais segura o catstdin de B / segundo , então o segundo catlê um EOF (B não está lendo o stdin, não se importa). Esse EOF faz com que o segundo catlibere seu buffer, feche o stdout e saia e, em seguida, B sai porque catsaiu e B estava aguardando cat.

Uma ressalva disso é que o bash também gera um subshell >(cat)! Por isso, você verá que

echo two > >(sleep 5) | cat; echo three

ainda esperará 5 segundos antes de executar echo three, mesmo que sleep 5não esteja segurando o stdin de B. Isso ocorre porque um subshell oculto que C gerou >(sleep 5)está esperando sleepe C está segurando o stdin de B. Você pode ver como

echo two > >(exec sleep 5) | cat; echo three

No entanto, não esperará, já que sleepnão está segurando o stdin de B, e não há nenhum subconjunto fantasma C que está segurando o stdin de B (o executivo forçará o sono a substituir C, em vez de bifurcar e fazer C esperar sleep). Independentemente desta ressalva,

echo two > >(exec cat) | cat; echo three

ainda executará corretamente as funções em ordem, conforme descrito anteriormente.

Nicholas Pipitone
fonte
Conforme observado na conversão com @MiniMax nos comentários da minha resposta, isso tem, no entanto, a desvantagem de afetar o desvio padrão do comando e significa que a saída precisa ser lida e escrita por um tempo extra.
Stéphane Chazelas
A explicação não é precisa. Anão está esperando pelos catgerados >(cat). Como mencionei na minha resposta, a razão pela qual a echo two > >(sleep 5 &>/dev/null) | cat; echo threesaída é realizada threeapós 5 segundos é porque as versões atuais bashdesperdiçam um processo de shell extra >(sleep 5)que aguarda sleepe esse processo ainda tem o stdout indo para o pipeque impede o segundo catde terminar. Se você substituí-lo por echo two > >(exec sleep 5 &>/dev/null) | cat; echo threepara eliminar esse processo extra, verá que ele retorna imediatamente.
Stéphane Chazelas
Faz um subshell aninhado? Eu tenho tentado olhar para a implementação do bash para descobrir, tenho certeza que echo two > >(sleep 5 &>/dev/null)o mínimo recebe seu próprio subshell. É um detalhe de implementação não documentado que faz sleep 5com que também obtenha seu próprio subshell? Se estiver documentado, seria uma maneira legítima de fazê-lo com menos caracteres (a menos que haja um loop apertado, acho que ninguém notará problemas de desempenho com um subshell ou um gato) `. Se não estiver documentado, então rip, nice hack, porém, não funcionará em versões futuras.
Nicholas Pipitone
$(...), de <(...)fato envolvem um subshell, mas o ksh93 ou o zsh executaria o último comando nesse subshell no mesmo processo, e não bashé por isso que ainda existe outro processo mantendo o pipe aberto enquanto sleepestá executando e não mantendo o pipe aberto. Versões futuras do bashpodem implementar uma otimização semelhante.
Stéphane Chazelas
11
@ StéphaneChazelas Atualizei minha resposta e acho que a explicação atual da versão mais curta está correta, mas você parece conhecer detalhes de implementação de shells para que possa verificar. Eu acho que essa solução deve ser usada em oposição à dança do descritor de arquivos, porém, já que mesmo abaixo exec, ela funciona como esperado.
Nicholas Pipitone