Uso prático para mover descritores de arquivos

16

De acordo com a página de manual do bash:

O operador de redirecionamento

   [n]<&digit-

move o descritor digitde arquivo para o descritor de arquivo nou a entrada padrão (descritor de arquivo 0) se nnão for especificado. digité fechado depois de duplicado para n.

O que significa "mover" um descritor de arquivo para outro? Quais são as situações típicas para essa prática?

Quentin
fonte

Respostas:

14

3>&4-é uma extensão ksh93 também suportada pelo bash e que é a abreviação de 3>&4 4>&-3 pontos agora para onde 4 costumava ser e 4 agora está fechado, então o que foi apontado por 4 agora mudou para 3.

O uso típico seria nos casos em que você duplicou stdinou stdoutsalvou uma cópia dela e deseja restaurá-la, como em:

Suponha que você queira capturar o stderr de um comando (e somente stderr) enquanto deixa o stdout sozinho em uma variável.

Substituição de comando var=$(cmd), cria um pipe. A extremidade de gravação do pipe torna-se cmdstdout (descritor de arquivo 1) e a outra extremidade é lida pelo shell para preencher a variável.

Agora, se você quiser stderrir para a variável, você poderia fazer: var=$(cmd 2>&1). Agora, tanto o fd 1 (stdout) quanto o 2 (stderr) vão para o pipe (e eventualmente para a variável), que é apenas metade do que queremos.

Se o fizermos var=$(cmd 2>&1-)(abreviação de var=$(cmd 2>&1 >&-), agora apenas cmdo stderr vai para o tubo, mas o fd 1 está fechado. Se cmdtentar gravar qualquer saída, que retornaria com um EBADFerro, se abrir um arquivo, ele receberá o primeiro fd livre e o arquivo aberto será atribuído a ele, a stdoutmenos que o comando se proteja disso! Também não é o que queremos.

Se queremos que o stdout cmdseja deixado sozinho, ou seja, aponte para o mesmo recurso que apontou para fora da substituição de comando, precisamos, de alguma forma, trazer esse recurso para dentro da substituição de comando. Para isso, podemos fazer uma cópia de stdout fora da substituição de comando para levá-la para dentro.

{
  var=$(cmd)
} 3>&1

Qual é uma maneira mais limpa de escrever:

exec 3>&1
var=$(cmd)
exec 3>&-

(que também tem o benefício de restaurar o fd 3 em vez de fechá-lo no final).

Em seguida, no {(ou no exec 3>&1) e no até }, os pontos 1 e 3 apontam para o mesmo recurso apontado inicialmente por fd 1. O fd 3 também apontará para esse recurso dentro da substituição de comando (a substituição de comando redireciona apenas o fd 1, stdout). Então, acima, pois cmdtemos os fds 1, 2, 3:

  1. o tubo para var
  2. intocado
  3. igual ao que 1 aponta para fora da substituição de comando

Se mudarmos para:

{
  var=$(cmd 2>&1 >&3)
} 3>&1-

Então se torna:

  1. igual ao que 1 aponta para fora da substituição de comando
  2. o tubo para var
  3. igual ao que 1 aponta para fora da substituição de comando

Agora, temos o que queríamos: stderr vai para o pipe e stdout é deixado intocado. No entanto, estamos vazando esse fd 3 para cmd.

Enquanto os comandos (por convenção) assumem que os fds de 0 a 2 estão abertos e são entrada, saída e erro padrão, eles não assumem nada de outros fds. Muito provavelmente eles deixarão esse fd 3 intocado. Se eles precisarem de outro descritor de arquivo, eles farão um open()/dup()/socket()...que retornará o primeiro descritor de arquivo disponível. Se (como um script de shell que faz isso exec 3>&1) eles precisarem usar isso fdespecificamente, eles primeiro o atribuirão a algo (e nesse processo, o recurso mantido pelo nosso fd 3 será liberado por esse processo).

É uma boa prática encerrar o fd 3, pois cmdnão o utiliza, mas não é grande coisa se o deixarmos designado antes de ligar cmd. Os problemas podem ser: que cmd(e potencialmente outros processos que ele gera) tem um fd a menos disponível. Um problema potencialmente mais sério é se o recurso apontado por fd pode acabar retido por um processo gerado por ele cmdem segundo plano. Pode ser uma preocupação se esse recurso é um canal ou outro canal de comunicação entre processos (como quando seu script está sendo executado como script_output=$(your-script)), pois isso significa que a leitura do processo pela outra extremidade nunca verá o final do arquivo até que processo em segundo plano termina.

Então aqui é melhor escrever:

{
  var=$(cmd 2>&1 >&3 3>&-)
} 3>&1

Com o qual, bashpode ser reduzido para:

{
  var=$(cmd 2>&1 >&3-)
} 3>&1

Para resumir os motivos pelos quais raramente é usado:

  1. é açúcar não-padrão e apenas sintático. Você precisa equilibrar a economia de algumas teclas para tornar seu script menos portátil e menos óbvio para as pessoas que não estão acostumadas a esse recurso incomum.
  2. A necessidade de fechar o fd original após duplicá-lo é muitas vezes negligenciada, porque na maioria das vezes não sofremos com a conseqüência, então apenas o fazemos em >&3vez de >&3-ou >&3 3>&-.

Prova de que raramente é usada, como você descobriu, é falso no bash . No bash compound-command 3>&4-ou nas any-builtin 3>&4-folhas, o fd 4 é fechado mesmo depois compound-commandou any-builtinvoltou. Um patch para corrigir o problema está agora disponível (19/02/2013).

Stéphane Chazelas
fonte
Obrigado, agora eu sei o que é mover um fd. Eu tenho 4 perguntas básicas sobre o segundo snippet (e fds em geral): 1) No cmd1, você faz 2 (stderr) para ser uma cópia de 3, e se o comando usasse internamente esse 3 fd? 2) Por que 3> & 1 e 4> & 1 funcionam? A duplicação de 3 e 4 entra em vigor apenas nesses dois cmds. O shell atual também é afetado? 3) Por que você fecha 4 em cmd1 e 3 em cmd2? Esses comandos não usam fds mencionados, usam? 4) No último trecho, o que acontece se um fd é duplicado para um inexistente (em cmd1 e cmd2), quero dizer 3 e 4, respectivamente?
Quentin
@ Quentin, usei um exemplo mais simples e expliquei um pouco mais, esperando que agora levante menos perguntas do que respostas. Se você ainda tem perguntas que não estão directamente relacionadas com a sintaxe movimento fd, eu sugiro que você faça uma pergunta separada
Stéphane Chazelas
{ var=$(cmd 2>&1 >&3) ; } 3>&1-Não é um erro de digitação no fechamento de 1?
Quentin
@ Quentin, é um erro de digitação que eu acho que não pretendia incluí-lo, no entanto, não faz diferença, pois nada dentro do aparelho usa esse fd 1 (era o objetivo de duplicá-lo para o fd 3: porque o original 1 caso contrário, não seria acessível por dentro $(...)).
Stéphane Chazelas
11
@Quentin Ao entrar {...}, o fd 3 aponta para o que o fd 1 costumava apontar e o fd 1 é fechado; depois, ao entrar $(...), o fd 1 é definido para o tubo que alimenta $var, depois para cmd2 para o mesmo, e então 1 para o que 3 pontos para, esse é o 1. externo. O fato de que 1 permanecer fechado depois é um bug no bash, relatarei. O ksh93 de onde vem esse recurso não possui esse bug.
Stéphane Chazelas
4

Significa fazê-lo apontar para o mesmo local que o outro descritor de arquivo. Você precisa fazer isso muito raramente, excluindo a manipulação separada óbvia do descritor de erro padrão ( stderr, fd 2, /dev/stderr -> /proc/self/fd/2). Pode ser útil em alguns casos complexos.

O guia Advanced Bash Scripting possui este exemplo mais longo no nível do log e este trecho:

# Redirecting only stderr to a pipe.
exec 3>&1                              # Save current "value" of stdout.
ls -l 2>&1 >&3 3>&- | grep bad 3>&-    # Close fd 3 for 'grep' (but not 'ls').
#              ^^^^   ^^^^
exec 3>&-                              # Now close it for the remainder of the script.

Na Sorcery do Source Mage, por exemplo, a usamos para discernir diferentes saídas do mesmo bloco de código:

  (
    # everything is set, so run the actual build infrastructure
    run_build
  ) 3> >(tee -a $C_LOG >> /dev/stdout) \
    2> >(tee -a $C_LOG 1>&2 > $VOYEUR_STDERR) \
     > >(tee -a $C_LOG > $VOYEUR_STDOUT)

Ele tem substituição extra de processo anexada por razões de registro (VOYEUR decide se os dados devem ser mostrados na tela ou apenas registro), mas algumas mensagens precisam sempre ser apresentadas. Para isso, imprimimos no descritor de arquivos 3 e, em seguida, tratamos especialmente.

lynxlynxlynx
fonte
0

No Unix, os arquivos são manipulados pelos descritores de arquivos (números inteiros pequenos, por exemplo, entrada padrão é 0, saída padrão é 1, erro padrão é 2; quando você abre outros arquivos, eles normalmente recebem o menor descritor não utilizado). Portanto, se você conhece as entranhas do programa e deseja enviar a saída que vai para o descritor de arquivo 5 para a saída padrão, mova o descritor 5 para 1. É daí que 2> errorsvem a origem e as construções gostam 2>&1de duplicar erros em o fluxo de saída.

Portanto, quase nunca foi usado (lembro-me vagamente de usá-lo uma ou duas vezes com raiva nos meus 25 anos de uso quase exclusivo do Unix), mas quando necessário, absolutamente essencial.

vonbrand
fonte
Mas por que não duplicar o descritor de arquivo 1 da seguinte maneira: 5> & 1? Eu não entendo o que é o uso de mover FD desde que o kernel está prestes a fechar-lo logo após ...
Quentin
Isso não duplica 5 em 1, envia 5 para onde está indo. E antes que você pergunte; Sim, existem várias notações diferentes, obtidas de uma variedade de invólucros precursores.
vonbrand
Ainda não entendi. Se 5>&1envia 5 para onde está indo, o que exatamente faz 1>&5-, além de fechar 5?
Quentin