Uso práctico para mover descriptores de archivos

16

Según la página de manual de bash:

El operador de redireccionamiento

   [n]<&digit-

mueve el descriptor digitde archivo al descriptor de archivo no la entrada estándar (descriptor de archivo 0) si nno se especifica. digitestá cerrado después de ser duplicado a n.

¿Qué significa "mover" un descriptor de archivo a otro? ¿Cuáles son las situaciones típicas para tal práctica?

Quentin
fuente

Respuestas:

14

3>&4-es una extensión ksh93 que también es compatible con bash y que es la abreviatura de 3>&4 4>&-3 ahora apunta a donde solía 4 y 4 ahora está cerrado, por lo que lo que señaló 4 ahora se ha movido a 3.

El uso típico sería en casos donde ha duplicado stdino stdoutpara guardar una copia y desea restaurarlo, como en:

Suponga que desea capturar el stderr de un comando (y solo stderr) mientras deja stdout solo en una variable.

Sustitución de comandos var=$(cmd), crea una tubería. El final de escritura de la tubería se convierte cmden stdout (descriptor de archivo 1) y el otro extremo es leído por el shell para completar la variable.

Ahora, si quieres stderrir a la variable, se podría hacer: var=$(cmd 2>&1). Ahora tanto fd 1 (stdout) como 2 (stderr) van a la tubería (y eventualmente a la variable), que es solo la mitad de lo que queremos.

Si lo hacemos var=$(cmd 2>&1-)(abreviatura de var=$(cmd 2>&1 >&-), ahora solo cmdstderr va a la tubería, pero fd 1 está cerrado. Si cmdintenta escribir alguna salida, eso devolvería un EBADFerror, si abre un archivo, obtendrá el primer fd libre y se le asignará el archivo abierto a stdoutmenos que el comando lo proteja. No es lo que queremos tampoco.

Si queremos que el stdout de cmdse quede solo, es decir, apuntar al mismo recurso que apuntó fuera de la sustitución del comando, entonces necesitamos de alguna manera llevar ese recurso dentro de la sustitución del comando. Para eso podemos hacer una copia de stdout fuera de la sustitución del comando para llevarlo dentro.

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

Cuál es una forma más limpia de escribir:

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

(que también tiene el beneficio de restaurar fd 3 en lugar de cerrarlo al final).

Luego, sobre {(o el exec 3>&1) y hasta el }, tanto fd 1 como 3 apuntan al mismo recurso al que fd 1 apuntó inicialmente. fd 3 también apuntará a ese recurso dentro de la sustitución del comando (la sustitución del comando solo redirige el fd 1, stdout). Así que arriba, para cmd, tenemos para fds 1, 2, 3:

  1. la pipa a var
  2. intacto
  3. igual a lo que apunta 1 fuera de la sustitución del comando

Si lo cambiamos a:

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

Entonces se convierte en:

  1. igual a lo que apunta 1 fuera de la sustitución del comando
  2. la pipa a var
  3. igual a lo que apunta 1 fuera de la sustitución del comando

Ahora, tenemos lo que queríamos: stderr va a la tubería y stdout se deja intacto. Sin embargo, estamos filtrando ese fd 3 a cmd.

Mientras que los comandos (por convención) asumen que los fds 0 a 2 están abiertos y son entrada, salida y error estándar, no asumen nada de otros fds. Lo más probable es que dejen ese fd 3 intacto. Si necesitan otro descriptor de archivo, simplemente harán uno open()/dup()/socket()...que devolverá el primer descriptor de archivo disponible. Si (como un script de shell que lo hace exec 3>&1) necesitan usarlo fdespecíficamente, primero lo asignarán a algo (y en ese proceso, el recurso en poder de nuestro fd 3 será liberado por ese proceso).

Es una buena práctica cerrar ese fd 3 ya cmdque no lo utiliza, pero no es gran cosa si lo dejamos asignado antes de llamar cmd. Los problemas pueden ser: eso cmd(y potencialmente otros procesos que genera) tiene un fd menos disponible. Un problema potencialmente más serio es si el recurso al que apunta fd puede terminar retenido por un proceso generado por eso cmden segundo plano. Puede ser una preocupación si ese recurso es una tubería u otro canal de comunicación entre procesos (como cuando su script se ejecuta como script_output=$(your-script)), ya que eso significará que la lectura del proceso desde el otro extremo nunca verá el final del archivo hasta que el proceso en segundo plano finaliza.

Entonces, aquí, es mejor escribir:

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

Que, con bashse puede acortar a:

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

Para resumir las razones por las que rara vez se usa:

  1. no es estándar y solo azúcar sintáctico. Debe equilibrar el ahorro de unas pocas pulsaciones de teclas para que su script sea menos portátil y menos obvio para las personas que no están acostumbradas a esa característica poco común.
  2. La necesidad de cerrar el fd original después de duplicarlo a menudo se pasa por alto porque la mayoría de las veces, no sufrimos las consecuencias, por lo que simplemente lo hacemos en >&3lugar de >&3-o >&3 3>&-.

La prueba de que rara vez se usa, como descubrió es que es falso en bash . En bash compound-command 3>&4-o any-builtin 3>&4-deja fd 4 cerrado incluso después compound-commando any-builtinha regresado. Ya está disponible un parche para solucionar el problema (2013-02-19).

Stéphane Chazelas
fuente
Gracias, ahora sé lo que es mover un fd. Tengo 4 preguntas básicas con respecto al segundo fragmento (y fds en general): 1) En cmd1, haces que 2 (stderr) sea una copia de 3, ¿qué pasaría si el comando usara internamente este 3 fd? 2) ¿Por qué funcionan 3> & 1 y 4> & 1? Duplicar 3 y 4 solo tiene efecto en esos dos cmds, ¿también se ve afectado el caparazón actual? 3) ¿Por qué cierras 4 en cmd1 y 3 en cmd2? Esos comandos no usan los fds mencionados, ¿verdad? 4) En el último fragmento, ¿qué sucede si un fd se duplica en uno inexistente (en cmd1 y cmd2), quiero decir 3 y 4, respectivamente?
Quentin
@Quentin, utilicé un ejemplo más simple y expliqué un poco más con la esperanza de que ahora plantee menos preguntas de las que responde. Si todavía tiene preguntas que no están directamente relacionadas con la sintaxis móvil de fd, le sugiero que haga una pregunta por separado
Stéphane Chazelas
{ var=$(cmd 2>&1 >&3) ; } 3>&1-¿No es un error tipográfico en el cierre 1?
Quentin
@Quentin, es un error tipográfico en el que no creo que tuviera la intención de incluirlo, sin embargo, no hace ninguna diferencia, ya que nada dentro de los corchetes usa ese fd 1 (era el punto de duplicarlo en fd 3: porque el original 1 de lo contrario no sería accesible por dentro $(...)).
Stéphane Chazelas
1
@Quentin Al ingresar {...}, fd 3 apunta a lo que fd 1 solía señalar y fd 1 está cerrado, luego al ingresar $(...), fd 1 se establece en la tubería que se alimenta $var, luego también cmd2 a eso, y luego 1 a qué 3 puntos a, ese es el 1. externo. El hecho de que 1 permanezca cerrado después es un error en bash, lo reportaré. ksh93 de donde proviene esa característica no tiene ese error.
Stéphane Chazelas
4

Significa hacer que apunte al mismo lugar que el otro descriptor de archivo. Es necesario hacer esto muy rara vez, aparte de la obvia manipulación separada del descriptor de errores estándar ( stderr, fd 2, /dev/stderr -> /proc/self/fd/2). Puede ser útil en algunos casos complejos.

La guía Advanced Bash Scripting tiene este ejemplo de nivel de registro más largo y este fragmento:

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

En Source Mage's Sorcery, por ejemplo, lo usamos para discernir diferentes resultados del mismo bloque 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)

Tiene una sustitución de proceso adicional añadida por razones de registro (VOYEUR decide si los datos deben mostrarse en la pantalla o simplemente registrar), pero algunos mensajes deben presentarse siempre . Para lograr eso, los imprimimos en el descriptor de archivo 3 y luego lo manejamos especialmente.

lynxlynxlynx
fuente
0

En Unix, los archivos se manejan mediante descriptores de archivo (enteros pequeños, por ejemplo, la entrada estándar es 0, la salida estándar es 1, el error estándar es 2; a medida que abre otros archivos, normalmente se les asigna el descriptor no utilizado más pequeño). Por lo tanto, si conoce las entradas del programa y desea enviar la salida que va al descriptor de archivo 5 a la salida estándar, movería el descriptor 5 a 1. De ahí es de donde 2> errorsproviene, y construcciones como2>&1 duplicar errores en El flujo de salida.

Por lo tanto, casi nunca lo uso (recuerdo vagamente haberlo usado una o dos veces con ira en mis más de 25 años de uso casi exclusivo de Unix), pero cuando lo necesito es absolutamente esencial.

vonbrand
fuente
Pero, ¿por qué no duplicar el descriptor de archivo 1 de la siguiente manera: 5> & 1? No entiendo de qué sirve mover FD ya que el núcleo está a punto de cerrarlo justo después ...
Quentin
Eso no duplica 5 en 1, envía 5 a donde va 1. Y antes de preguntar; Sí, hay varias anotaciones diferentes, recogidas de una variedad de conchas precursoras.
vonbrand
Aún no lo entiendo. Si 5>&1envía 5 a donde va 1, ¿qué hace 1>&5-, además de cerrar 5?
Quentin