Então, eu queria contribuir com uma resposta como a da Lesmana, mas acho que a minha talvez seja um pouco mais simples e um pouco mais vantajosa, solução de Bourne-shell pura:
# You want to pipe command1 through command2:
exec 4>&1
exitstatus=`{ { command1; printf $? 1>&3; } | command2 1>&4; } 3>&1`
# $exitstatus now has command1's exit status.
Eu acho que isso é melhor explicado de dentro para fora - command1 executará e imprimirá sua saída regular em stdout (descritor de arquivo 1); depois que estiver pronto, printf executará e imprimirá o código de saída do icommand1 em seu stdout, mas esse stdout será redirecionado para descritor de arquivo 3.
Enquanto o comando1 está em execução, seu stdout está sendo canalizado para o comando2 (a saída do printf nunca chega ao comando2 porque enviamos para o descritor de arquivo 3 em vez de 1, que é o que o canal lê). Em seguida, redirecionamos a saída do command2 para o descritor de arquivo 4, para que ele também fique fora do descritor de arquivo 1 - porque queremos que o descritor de arquivo 1 fique livre por um pouco mais tarde, porque traremos a saída printf do descritor de arquivo 3 de volta para o descritor de arquivo 1 - porque é isso que a substituição de comando (os backticks) captura e é isso que será colocado na variável.
A parte final da mágica é que primeiro exec 4>&1
fizemos como um comando separado - ele abre o descritor de arquivo 4 como uma cópia do stdout do shell externo. A substituição de comando capturará o que está escrito no padrão da perspectiva dos comandos dentro dele - mas como a saída do command2 vai arquivar o descritor 4 no que diz respeito à substituição de comando, a substituição de comando não o captura - no entanto, uma vez que "sai" da substituição de comando, ele ainda está efetivamente indo para o descritor de arquivo geral 1 do script.
( exec 4>&1
Tem que ser um comando separado, porque muitos shells comuns não gostam quando você tenta gravar em um descritor de arquivo dentro de uma substituição de comando, que é aberto no comando "externo" que está usando a substituição. Portanto, este é o maneira portátil mais simples de fazer isso.)
Você pode vê-lo de uma maneira menos técnica e mais divertida, como se as saídas dos comandos estivessem saltando uma contra a outra: command1 canaliza para command2, então a saída do printf salta sobre o comando 2 para que o comando2 não o pegue e depois a saída do comando 2 salta para cima e para fora da substituição de comando, assim como printf chega bem a tempo de ser capturada pela substituição, para que acabe na variável, e a saída do comando2 segue seu caminho alegre sendo gravada na saída padrão, assim como em um tubo normal.
Além disso, pelo que entendi, $?
ainda conterá o código de retorno do segundo comando no canal, porque atribuições de variáveis, substituições de comandos e comandos compostos são efetivamente transparentes ao código de retorno do comando dentro deles, portanto, o status de retorno de command2 deve ser propagado - isso, e não sendo necessário definir uma função adicional, é por isso que acho que essa pode ser uma solução um pouco melhor do que a proposta por lesmana.
De acordo com as advertências mencionadas por lesmana, é possível que o command1 acabe, em algum momento, usando os descritores de arquivo 3 ou 4; portanto, para ser mais robusto, faça o seguinte:
exec 4>&1
exitstatus=`{ { command1 3>&-; printf $? 1>&3; } 4>&- | command2 1>&4; } 3>&1`
exec 4>&-
Observe que eu uso comandos compostos no meu exemplo, mas subshells (usar em ( )
vez de { }
também funcionará, embora talvez seja menos eficiente.)
Os comandos herdam os descritores de arquivo do processo que os inicia; portanto, a segunda linha inteira herdará o descritor de arquivo quatro e o comando composto seguido por 3>&1
herdará o descritor de arquivo três. Portanto, ele 4>&-
garante que o comando composto interno não herda o descritor de arquivo quatro e o 3>&-
herdeiro não descreva o arquivo três; portanto, o comando1 obtém um ambiente mais limpo e padrão. Você também pode mover o interior 4>&-
para o próximo 3>&-
, mas acho que não basta limitar o escopo o máximo possível.
Não sei com que frequência as coisas usam o descritor de arquivos três e quatro diretamente - acho que na maioria das vezes os programas usam syscalls que retornam descritores de arquivo não usados no momento, mas às vezes o código grava diretamente no descritor de arquivo 3, palpite (eu poderia imaginar um programa verificando um descritor de arquivo para ver se ele está aberto e usando-o se estiver, ou se comportando de maneira diferente se não estiver). Portanto, é provável que o último seja lembrado e usado em casos de uso geral.