Em
{ err=$(exec 2>&1 >&3; ls -ld /x /bin); exec 0<&3; out=$(cat); } 3>&1
O { ... } 3>&1
clona o fd 1 para o fd 3. Isso significa que o fd3 agora aponta para o mesmo recurso (a mesma descrição do arquivo aberto ) que o fd 1 apontou. Se você executou isso de um terminal, provavelmente será um fd aberto no modo de leitura + gravação para um dispositivo de terminal.
Depois de exec 0<&3
, os fds 0, 1 e 3 estão apontando para a mesma descrição do arquivo aberto (criada quando o emulador de terminal abriu o lado escravo do par de pseudo-terminais criado antes executando seu shell no caso do comando executado no terminal acima).
Em seguida, em out=$(cat)
, para o processo que está executando cat
, o $(...)
altera o fd 1 para a extremidade de gravação de um canal, enquanto 0 ainda é o dispositivo tty. Então, cat
lerá do dispositivo terminal, então as coisas que você está digitando no teclado (e se não fosse um dispositivo terminal, você provavelmente receberia um erro, já que o fd provavelmente estava aberto no modo somente gravação).
Para cat
ler o que ls
grava em seu stdout, você precisaria que ls
stdout e cat
stdin tenham duas extremidades em um mecanismo IPC, como pipe, socketpair ou pseudo-terminal. Por exemplo, ls
stdout é o fim de gravação de um pipe e cat
stdin é o final da leitura.
Mas você também precisaria que ls
e cat
sejam executados simultaneamente, não um após o outro, pois é um mecanismo de IPC (comunicação entre processos).
Como canos podem reter alguns dados (64 KiB por padrão nas versões atuais do Linux), você sairia com saídas curtas se você conseguisse criar aquele segundo canal, mas para saídas maiores, você teríamos deadlocks, ls
iria travar quando o pipe estivesse cheio e travaria até que algo esvaziasse o pipe, mas cat
só pode esvaziar o pipe quando ls
retornar.
Além disso, somente yash
tem uma interface bruta para pipe()
, que seria necessário para criar o segundo canal para ler ls
stdout (o outro canal para stderr sendo criado pela $(...)
construct).
No yash, você faria:
{ out=$(ls -d / /x 2>&3); exec 3>&-; err=$(exec cat <&4); } 3>>|4
Onde 3>>|4
(um recurso específico do yash) cria o segundo pipe com o fim da escrita em fd 3 e o final da leitura em fd 4.
Mas, novamente, se a saída stderr for maior que o tamanho do tubo, ela será interrompida. Estamos efetivamente usando o pipe como um arquivo temporário na memória, não um pipe.
Para realmente usar pipes, precisamos iniciar ls
com stdout sendo a extremidade de gravação de um pipe e stderr sendo a extremidade de gravação de outro canal, e então o shell lê as outras extremidades desses pipes simultaneamente, como os dados vêm (não um após o outro ou novamente você se depararia com bloqueios mortos) para armazenar nas duas variáveis.
Para conseguir ler esses dois fds à medida que os dados chegam, você precisaria de um shell com select()
/ poll()
support. zsh
é um shell desse tipo, mas não possui o recurso redirecionamento de pipeline do yash
, portanto, é necessário usar pipes nomeados (para gerenciar sua criação, permissões e limpeza) e use um loop complexo com zselect
/ sysread
...
¹ Se no Linux, porém, você seria capaz de usar o fato de que /proc/self/fd/x
em um pipe se comporta como um pipe nomeado, então você poderia fazer:
#! /bin/zsh
zmodload zsh/zselect
zmodload zsh/system
(){exec {wo}>$1 {ro}<$1} <(:) # like yash's wo>>|ro (but on Linux only)
(){exec {we}>$1 {re}<$1} <(:)
ls -d / /x >&$wo 2>&$we &
exec {wo}>&- {we}>&-
out= err=
o_done=0 e_done=0
while ((! (o_done && e_done))) && zselect -A ready $ro $re; do
if ((${#ready[$ro]})); then
sysread -i $ro && out+=$REPLY || o_done=1
fi
if ((${#ready[$re]})); then
sysread -i $re && err+=$REPLY || e_done=1
fi
done