Por favor, considere o seguinte log de uma sessão de terminal (Debian Buster, Bash 5.0):
root@cerberus ~/scripts # rm -f result
root@cerberus ~/scripts # { { echo test; } | cat > result; }
root@cerberus ~/scripts # cat result
test
root@cerberus ~/scripts #
Nada de especial aqui, este é o comportamento esperado, e eu o entendo.
Mas não entendo o comportamento no seguinte caso:
root@cerberus ~/scripts # rm -f result
root@cerberus ~/scripts # { { echo test >&3; } | cat > result; } 3>&1
test
root@cerberus ~/scripts # cat result
root@cerberus ~/scripts #
Para ser preciso, acredito que entendo por que "teste" é gerado ao executar a segunda linha, mas não entendo por que nada entra no arquivo de resultado. Meu entendimento do que acontece é o seguinte:
A princípio, fd 3 é configurado como uma duplicata de
stdout
. Tenho certeza de que isso acontece antes que o pipe seja executado, porque, caso contrário, nenhum dos comandos no pipe teria acesso ao fd 3, o que causaria uma mensagem de erro "bad descriptor".Um pipeline não é um comando simples, então um subshell é gerado para executá-lo. O subshell herda o ambiente de execução do shell pai, incluindo descritores de arquivo e redirecionamentos. [1]
Cada um dos comandos no pipeline também é executado em seu próprio subshell [2] , novamente herdando o ambiente de execução e os descritores de arquivo.
echo
A saída de 's é redirecionada para fd 3, que por sua vez foi duplicada destdout
antes, o que em resumo levaecho
a saída de 's a aparecerstdout
(a saída vai para fd 3, que vai para fd 1, que é stdout).Mas não entendo por que
echo
a saída de 's não entra no arquivo de resultado. Do manual do bash (ênfase minha):
A saída de cada comando no pipeline é conectada por meio de um pipe à entrada do próximo comando. Ou seja, cada comando lê a saída do comando anterior. Essa conexão é executada antes de qualquer redirecionamento especificado pelo comando.
Estou entendendo isso no sentido de que echo
a saída de ' deve ser conectada à cat
entrada de ' antes que o redirecionamento >&3
seja configurado ou aplicado, respectivamente. Mas se isso for verdade, o arquivo de resultado existirá (e conterá "teste") depois que o comando for executado. Portanto, meu entendimento está obviamente errado.
Alguém poderia explicar o que estou perdendo?
Atualização, com base na excelente resposta de AB e Gilles abaixo, com mais explicações
A fonte das minhas preocupações é o que escrevi no item 3. acima. Simplesmente não funciona assim; veja também a resposta de Gilles.
AB foi o primeiro a fornecer uma resposta (veja abaixo). No entanto, eu precisava de algum tempo para entendê-lo. Por isso vou explicar algumas passagens para que possam ser compreendidas mais facilmente.
Última parte da linha:
3>&1
é feito primeiro: fd 1 apontando para a saída do terminal é duplicado para fd 3. Isso significa que fd 1 e fd 3 agora apontam para a saída do terminal. Eles são idênticos e podem ser usados alternadamente.Antes de bifurcar, um pipe é criado, normalmente usando a
pipe(2)
chamada de sistema, nos próximos fds disponíveis: digamos fd 4 e fd 5. O processo de preparação então bifurca em future echo e future cat, executando as seguintes etapas:
a) O processo de preparação para echo funciona assim:
fd 5 é duplicado para fd 1 (sobrescrevendo onde fd 1 apontava: a saída do terminal). Isso significa que fd 1 agora é idêntico a fd 5 e que eles podem ser usados alternadamente. Especificamente, fd 1 não aponta mais para a saída do terminal, mas aponta para a extremidade de escrita do pipe.
Neste estágio (mas veja abaixo), a saída deecho
iria para a extremidade de escrita do pipe, porqueecho
escreve para fd 1, que aponta para essa extremidade de escrita.
Como não precisamos de dois descritores de arquivo para a mesma coisa, e porqueecho
grava em fd 1 de qualquer maneira, fd 5 é fechado agora.
Entãoecho
é executado, mas depois de ter configurado o redirecionamento adicional observado por trás dele (ver 3.).
b) Da mesma forma, o processo de preparação paracat
duplicatas fd 4 a fd 0, significando que fd 0 não aponta mais para a entrada do terminal, mas aponta para o lado receptor do tubo. Nesse estágio, a entrada paracat
viria do lado receptor do tubo, porquecat
lê de fd 0, e fd 0 está conectado a esse lado receptor. Porque não precisamos de dois descritores de arquivo para a mesma coisa, e porquecat
lê de fd 0 de qualquer maneira, fd 4 é fechado agora. Em seguida,cat
é executado.
Enquanto tudo isso acontece, o fd 3 é herdado em todos os lugares.>&3
faz o oposto do marcador 1: ele duplica fd 3 para fd 1. fd 3 foi criado para apontar para a saída do terminal e é herdado pelo subshell que executa o pipe e os subshells adicionais que executam os comandos individuais do pipe.
Na etapa 2a), fd 1 foi apontado para o lado de escrita do tubo. Mas agora, o redirecionamento>&3
sobrescreve fd 1 novamente e o torna igual a fd 3, que por sua vez (ainda) aponta para a saída do terminal. Isso significa que fd 1 não aponta mais para o lado de escrita do pipe, mas para a saída do terminal. Esta é a razão pela qual "test" aparece no terminal quando o pipe é executado (lembre-se queecho
sempre escreve para fd 1, independentemente de onde fd 1 apontar).
Além disso, quando fd 1 é "substituído" pelo redirecionamento, sua versão antiga é fechada (porque a chamada de sistema subjacentedup2(2)
faz isso). Como sua versão antiga apontava para a extremidade de escrita do tubo, essa extremidade de escrita agora está fechada.
Por causa disso, a extremidade receptora e, portanto,cat
, não receberá nenhum dado. Eles recebem imediatamente uma notificação EOF. Esta é a razão pela qualcat
não recebe nada e, consequentemente, o arquivo de resultado permanece vazio ou é truncado.
[ Nota lateral: eu deveria ter fechado o fd 3 após o redirecionamento (ou seja, deveríamos ter escrito>&3 3>&-
em vez de>&3
), porqueecho
-como mencionado acima- escreve para fd 1 e não sabe nada sobre fd 3. No entanto, essa parte estava faltando no meu exemplo, e eu gostaria de deixar assim para não distrair do problema real). ]
Isso é por causa do bullet 4 do OP que funciona assim, com o fd herdado ao longo dos vários processos de criação/execução. Eu não estou escrevendo todos os lugares onde o fork/exec acontece. Certamente estou simplificando um pouco (com comandos embutidos...). Links de documentação fornecidos para Linux, mas o mesmo comportamento deve ocorrer em qualquer sistema POSIX ou semelhante a POSIX.
3>&1
é feito primeiro: fd 1 apontando para o terminal é duplicado como fd 3 (normalmente usando adup2(2)
chamada do sistema).pipe(2)
chamada do sistema, no próximo fd disponível s disponíveis: digamos 4 e 5. O processo de preparação então bifurca em futureecho
e futurecat
. proto-echo dups 5 para 1 ("sobrescrevendo" onde apontava: o terminal), fecha 5 e execsecho
, proto-cat dups2() 4 para 0, fecha 4 e execscat
. fd 3 é herdado em todos os lugares.>&3
faz o oposto do marcador 1: duplica o fd 3 (apontando para o terminal) para fd 1. Assim, o lado de escrita do pipe foi substituído e agora está fechado (dup2(2)
diz: "Se o descritor de arquivo newfd foi aberto anteriormente, ele é silenciosamente fechado antes de ser reutilizado."). Nada será gravado no pipe. Terminal recebetest
e exibe.cat
abre e trunca o arquivo de destinoresult
e inicia a leitura do pipe. Isso aciona um EOF conformepipe(7)
porque o lado da gravação está/foi fechado:cat
o comando termina.Resultado:
test
no terminal e arquivo vazioresult
.Isso está correto, como afirmado, mas um pouco estranho, e você parece ter entendido mal o que isso significa. Isso não significa que onde esse redirecionamento está em vigor, escrever em fd 3 é equivalente a escrever em stdout. Isso significa que o fd 3 está conectado a qualquer stdout ao qual esteja conectado no ponto em que o redirecionamento está configurado. Se você estiver executando este código em um terminal,
3>&1
conecte o descritor de arquivo 3 ao terminal. E entao…FD 3 é o terminal. O fato de que em algum momento aconteceu também ser fd 1 de algum outro processo é um detalhe histórico irrelevante.