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 seq
saída à cat
entrada 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 bash
faz sob o capô).
bash
process-substitution
MiniMax
fonte
fonte
Respostas:
Sim,
bash
como emksh
(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:o shell estará aguardando
cmd1
ecmd1
normalmente estará aguardando emcmd2
virtude da leitura até o final do arquivo no tubo substituído, e esse fim do arquivo normalmente acontece quandocmd2
morre. Essa é a mesma razão várias conchas (nãobash
) não se incomode à espera decmd2
nocmd2 | cmd1
.Pois
cmd1 >(cmd2)
, no entanto, esse geralmente não é o caso, pois é mais ocmd2
que normalmente espera porcmd1
isso, e geralmente sai depois.Isso está fixo no
zsh
que aguarda porcmd2
lá (mas não se você o escrever comocmd1 > >(cmd2)
ecmd1
não estiver embutido, use{cmd1} > >(cmd2)
como documentado ).ksh
não espera por padrão, mas permite que você espere por ele com owait
built-in (também disponibiliza o pid$!
, embora isso não ajude se você o fizercmd1 >(cmd2) >(cmd3)
)rc
(com acmd1 >{cmd2}
sintaxe), o mesmo que com aksh
exceção de que você pode obter os pids de todos os processos em segundo plano$apids
.es
(também comcmd1 >{cmd2}
) aguarda ocmd2
like inzsh
e também aguarda redirecionamentoscmd2
no<{cmd2}
processo.bash
torna disponível o pidcmd2
(ou mais exatamente do subshell, como ele é executadocmd2
em 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:Isso faz os dois
cmd1
ecmd2
tem seu fd 3 aberto em um cano.cat
aguardará o final do arquivo na outra extremidade e, portanto, normalmente somente sairá quando amboscmd1
ecmd2
estiverem mortos. E o shell aguardará essecat
comando. 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
cmd2
fechar o fd 3 (os comandos geralmente não fazem isso, mas alguns gostamsudo
oussh
fazem). Versões futuras dobash
podem eventualmente fazer a otimização como em outros shells. Então você precisaria de algo como:Para garantir que ainda haja um processo de shell extra com esse fd 3 aberto aguardando esse
sudo
comando.Observe que
cat
não lerá nada (já que os processos não gravam em seu fd 3). Está lá apenas para sincronização. Ele fará apenas umaread()
chamada do sistema que retornará sem nada no final.Na verdade, você pode evitar a execução
cat
usando uma substituição de comando para fazer a sincronização de pipe:Desta vez, é o shell, em vez disso,
cat
que está lendo no canal, cuja outra extremidade está aberta nos fd 3 decmd1
ecmd2
. Estamos usando uma atribuição de variável para que o status de saída decmd1
esteja disponível em$?
.Ou você pode fazer a substituição do processo manualmente, e então você pode até usar o sistema,
sh
pois isso se tornaria a sintaxe padrão do shell:observe, como observado anteriormente, que nem todas as
sh
implementações esperariamcmd1
após acmd2
conclusão (embora isso seja melhor do que o contrário). Nesse momento,$?
contém o status de saída decmd2
; emborabash
ezsh
disponibilizecmd1
o status de saída em${PIPESTATUS[0]}
e$pipestatus[1]
respectivamente (consulte também apipefail
opção em algumas conchas para que$?
possamos relatar a falha de outros componentes do tubo que não o anterior)Observe que
yash
há problemas semelhantes com o recurso de redirecionamento de processo .cmd1 >(cmd2)
seria escritocmd1 /dev/fd/3 3>(cmd2)
lá. Mascmd2
não é esperado e você também não podewait
esperar por isso, e seu pid também não é disponibilizado na$!
variável. Você usaria as mesmas soluções alternativas debash
.fonte
echo one; { { echo two > >(cat); } 3>&1 >&4 4>&- | cat; } 4>&1; echo three;
, depois simplifiquei para oecho one; echo two > >(cat) | cat; echo three;
e ele gera valores na ordem certa também. Todas essas manipulações de descritores3>&1 >&4 4>&-
são necessárias? Além disso, eu não entendo isso>&4 4>&
- somos redirecionadosstdout
para o quarto fd, fechando o quarto fd e depois usá-4>&1
lo novamente . Por que é necessário e como funciona? Pode ser, eu devo criar uma nova pergunta sobre esse tópico?cmd1
ecmd2
, 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.4>&1
cria 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>&1
faz com que o fd 3 se conecte ao stdin dos aparelhos externos.>&4
faz 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).4>&1
é executada primeiro, antes dos outros redirecionamentos, para que você não "use novamente4>&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.4>5
significa "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.Você pode canalizar o segundo comando para outro
cat
, o que aguardará até que o canal de entrada seja fechado. Ex: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 deecho two
. Assim,echo two
termina e depoisecho three
é executado, mas antes dos>(cat)
acabamentos. Quandobash
obté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 tivessemesg
editado).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 é paracat
. A é conectado automaticamente a B (stdout de A é stdin de B). Então,echo two
e>(cat)
comece a executar.>(cat)
stdout de é definido como stdout de A, que é igual ao stdin de B. Depois deecho two
acabamentos, A saídas, fechando sua stdout. No entanto,>(cat)
ainda está mantendo a referência ao stdin de B. Ocat
stdin do segundo está segurando o stdin de B e issocat
nã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 segundocat
. B permanece esperando nesse segundocat
. Desde queecho two
saiu,>(cat)
finalmente obtém um EOF, então>(cat)
libera seu buffer e sai. Ninguém mais segura ocat
stdin de B / segundo , então o segundocat
lê um EOF (B não está lendo o stdin, não se importa). Esse EOF faz com que o segundocat
libere seu buffer, feche o stdout e saia e, em seguida, B sai porquecat
saiu e B estava aguardandocat
.Uma ressalva disso é que o bash também gera um subshell
>(cat)
! Por isso, você verá queecho two > >(sleep 5) | cat; echo three
ainda esperará 5 segundos antes de executar
echo three
, mesmo quesleep 5
não esteja segurando o stdin de B. Isso ocorre porque um subshell oculto que C gerou>(sleep 5)
está esperandosleep
e C está segurando o stdin de B. Você pode ver comoecho two > >(exec sleep 5) | cat; echo three
No entanto, não esperará, já que
sleep
nã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 esperarsleep
). Independentemente desta ressalva,echo two > >(exec cat) | cat; echo three
ainda executará corretamente as funções em ordem, conforme descrito anteriormente.
fonte
A
não está esperando peloscat
gerados>(cat)
. Como mencionei na minha resposta, a razão pela qual aecho two > >(sleep 5 &>/dev/null) | cat; echo three
saída é realizadathree
após 5 segundos é porque as versões atuaisbash
desperdiçam um processo de shell extra>(sleep 5)
que aguardasleep
e esse processo ainda tem o stdout indo para opipe
que impede o segundocat
de terminar. Se você substituí-lo porecho two > >(exec sleep 5 &>/dev/null) | cat; echo three
para eliminar esse processo extra, verá que ele retorna imediatamente.echo two > >(sleep 5 &>/dev/null)
o mínimo recebe seu próprio subshell. É um detalhe de implementação não documentado que fazsleep 5
com 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.$(...)
, de<(...)
fato envolvem um subshell, mas o ksh93 ou o zsh executaria o último comando nesse subshell no mesmo processo, e nãobash
é por isso que ainda existe outro processo mantendo o pipe aberto enquantosleep
está executando e não mantendo o pipe aberto. Versões futuras dobash
podem implementar uma otimização semelhante.exec
, ela funciona como esperado.