Uso prático para mover descritores de arquivos

16

De acordo com a página man bash:

The redirection operator

   [n]<&digit-

moves the file descriptor digit to file descriptor n, or the standard input (file descriptor 0) if n is not specified. digit is closed after being dupli‐cated to n.

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

    
por Quentin 16.02.2013 / 19:09

3 respostas

13

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

O uso típico seria nos casos em que você duplicou stdin ou stdout para salvar uma cópia e deseja restaurá-la, como em:

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

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

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

Se fizermos var=$(cmd 2>&1-) (abreviação de var=$(cmd 2>&1 >&- ), agora apenas stderr de cmd vai para o pipe, mas fd 1 é fechado. Se cmd tentar gravar alguma saída, ela retornará com um erro EBADF , se abrir um arquivo, receberá o primeiro fd livre e o arquivo aberto será atribuído a stdout , a menos que o comando proteja contra naquela! Não é o que queremos também.

Se quisermos que o stdout de cmd seja deixado em paz, ou seja, apontar para o mesmo recurso que ele apontou 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 do stdout fora da substituição do comando para levá-lo para dentro.

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

Qual é uma maneira mais clara 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).

Depois, em { (ou exec 3>&1 ) e até } , tanto fd 1 quanto 3 apontam para o mesmo recurso fd 1 apontado inicialmente. O fd 3 também apontará para esse recurso dentro da substituição de comando (a substituição de comando apenas redireciona o fd 1, stdout). Então, acima, para cmd , temos para fds 1, 2, 3:

  1. o pipe para var
  2. intocado
  3. o mesmo que 1 aponta para fora da substituição do comando

Se mudarmos para:

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

Em seguida, torna-se:

  1. o mesmo que 1 aponta para fora da substituição do comando
  2. o pipe para var
  3. o mesmo que 1 aponta para fora da substituição do comando

Agora, nós temos o que queremos: stderr vai para o tubo e stdout é deixado intocado. No entanto, estamos vazando o fd 3 para cmd .

Enquanto os comandos (por convenção) assumem fds 0 a 2 para serem abertos e serem entrada, saída e erro padrão, eles não assumem nada de outros fds. O mais provável é que eles deixem o 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 usa exec 3>&1 ) eles precisam usar esse fd especificamente, eles irão primeiro atribuí-lo a algo (e nesse processo, o recurso mantido pelo nosso fd 3 será liberado por esse processo).

É uma boa prática fechar o fd3 já que cmd não faz uso dele, mas não é grande coisa se deixarmos ele atribuído antes de chamarmos cmd . Os problemas podem ser: que cmd (e potencialmente outros processos que ele gera) tem menos um fd disponível para ele. Um problema potencialmente mais sério é se o recurso para o qual aponta o fd puder ser mantido por um processo criado por cmd em segundo plano. Pode ser uma preocupação se esse recurso for um canal de comunicação pipe ou outro entre processos (como quando seu script está sendo executado como script_output=$(your-script) ), pois isso significará que a leitura do processo do outro lado nunca verá o final de arquivo até que o processo em segundo plano termine.

Então, é melhor escrever:

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

Qual, com bash pode ser reduzido para:

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

Para resumir as razões pelas quais ele é raramente usado:

  1. é um açúcar não padronizado e apenas sintático. Você precisa equilibrar a gravação de alguns pressionamentos de tecla para tornar seu script menos portátil e menos óbvio para pessoas que não estão acostumadas com esse recurso incomum.
  2. A necessidade de fechar o fd original depois de duplicá-lo é frequentemente ignorada porque na maioria das vezes, não sofremos as consequências, por isso só fazemos >&3 em vez de >&3- ou >&3 3>&- .

A prova de que é raramente usada, como você descobriu, é que ela é falsa na bash . No bash compound-command 3>&4- ou any-builtin 3>&4- deixa o fd 4 fechado mesmo depois que compound-command ou any-builtin retornou. Um patch para corrigir o problema é agora (2013-02-19 ) disponível.

    
por 16.02.2013 / 20:50
4

Significa fazê-lo apontar para o mesmo lugar que o outro descritor de arquivo faz. Você precisa fazer isso muito raramente, além da 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 tem este exemplo de nível de log mais longo e este snippet:

# 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.

No Feitiço do Source Mage, por exemplo, usamos para discernir saídas diferentes 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)

Tem uma substituição de processo extra anexada por razões de registro (VOYEUR decide se os dados devem ser mostrados na tela ou apenas log), mas algumas mensagens precisam ser sempre apresentadas. Para conseguir isso, nós os imprimimos para o descritor de arquivo 3 e, em seguida, lidamos com ele especialmente.

    
por 16.02.2013 / 19:59
0

No Unix, os arquivos são manipulados por descritores de arquivos (inteiros pequenos, por exemplo, a entrada padrão é 0, a saída padrão é 1, o erro padrão é 2; à medida que você abre outros arquivos, eles normalmente recebem o menor descritor não usado). Então, se você conhece os inards 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. É de onde vem o 2> errors e construções como 2>&1 para duplicar erros no fluxo de saída.

Então, quase nunca usado (lembro-me vagamente de usá-lo uma ou duas vezes com raiva em meus mais de 25 anos de uso quase exclusivo do Unix), mas quando era absolutamente necessário.

    
por 16.02.2013 / 19:29