En el código "{exec> / dev / null; }> / dev / null "¿qué está pasando bajo el capó?

15

Cuando redirige una lista de comandos que contiene una redirección de exec, parece que el exec> / dev / null todavía no se aplica después, como con:

{ exec >/dev/null; } >/dev/null; echo "Hi"

Se imprime "Hola".

Tenía la impresión de que la {}lista de comandos no se considera una subshell a menos que sea parte de una tubería, por lo exec >/dev/nullque aún debería aplicarse en el entorno de shell actual en mi mente.

Ahora si lo cambia a:

{ exec >/dev/null; } 2>/dev/null; echo "Hi"

no hay salida como se esperaba; el descriptor de archivo 1 sigue apuntando a / dev / null para futuros comandos también. Esto se muestra al volver a ejecutar:

{ exec >/dev/null; } >/dev/null; echo "Hi"

que no dará salida.

Intenté hacer un guión y alinearlo, pero todavía no estoy seguro de qué está sucediendo aquí.

En cada punto de este script, ¿qué está pasando con el descriptor de archivo STDOUT?

EDITAR: Agregar mi salida de strace:

read(255, "#!/usr/bin/env bash\n{ exec 1>/de"..., 65) = 65
open("/dev/null", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
fcntl(1, F_GETFD)                       = 0
fcntl(1, F_DUPFD, 10)                   = 10
fcntl(1, F_GETFD)                       = 0
fcntl(10, F_SETFD, FD_CLOEXEC)          = 0
dup2(3, 1)                              = 1
close(3)                                = 0
close(10)                               = 0
open("/dev/null", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
fcntl(1, F_GETFD)                       = 0
fcntl(1, F_DUPFD, 10)                   = 10
fcntl(1, F_GETFD)                       = 0
fcntl(10, F_SETFD, FD_CLOEXEC)          = 0
dup2(3, 1)                              = 1
close(3)                                = 0
dup2(10, 1)                             = 1
fcntl(10, F_GETFD)                      = 0x1 (flags FD_CLOEXEC)
close(10)                               = 0
fstat(1, {st_mode=S_IFCHR|0666, st_rdev=makedev(1, 3), ...}) = 0
ioctl(1, TCGETS, 0x7ffee027ef90)        = -1 ENOTTY (Inappropriate ioctl for device)
write(1, "hi\n", 3)                     = 3
Joey Pabalinas
fuente
Eso es extraño; No puedo reproducir el close(10). ¿También puede publicar todo el contenido del script en el que ejecutó strace?
DepressedDaniel
@DepressedDaniel Aquí está el guión completo y la secuencia: secuencia de secuencia de comandos
Joey Pabalinas
;Después de todo }, tiene un parásito que cambia el significado de > /dev/nullno aplicarse a la lista compuesta {}.
DepressedDaniel
@DepressedDaniel ¡Ah, tienes toda la razón! Ahora la salida es lo que espero; ¡Gracias por sus respuestas!
Joey Pabalinas

Respuestas:

17

Vamos a seguir

{ exec >/dev/null; } >/dev/null; echo "Hi"

paso a paso.

  1. Hay dos comandos:

    a. { exec >/dev/null; } >/dev/null, seguido por

    si. echo "Hi"

    El shell ejecuta primero el comando (a) y luego el comando (b).

  2. La ejecución de los { exec >/dev/null; } >/dev/nullingresos de la siguiente manera:

    a. Primero, el shell realiza la redirección >/dev/null y recuerda deshacerla cuando finaliza el comando .

    si. Entonces, el shell se ejecuta { exec >/dev/null; }.

    C. Finalmente, el shell cambia la salida estándar a donde estaba. (Este es el mismo mecanismo que en ls -lR /usr/share/fonts >~/FontList.txt- las redirecciones se realizan solo mientras dura el comando al que pertenecen).

  3. Una vez que se realiza el primer comando, se ejecuta el shell echo "Hi". La salida estándar es donde estaba antes del primer comando.

AlexP
fuente
¿Hay alguna razón detrás de por qué 2a se ejecuta antes que 2b? (derecha a izquierda)
Joey Pabalinas
55
Las redirecciones deben ejecutarse antes del comando al que se aplican, ¿no? ¿Cómo podrían funcionar de otra manera?
AlexP
¡Ajá, nunca lo pensé así! Las dos primeras son buenas respuestas; dándole un poco antes de decidirme por uno, ¡pero agradezco ambas explicaciones!
Joey Pabalinas
Desafortunadamente, solo puedo elegir una respuesta, así que voy con esta, ya que es un poco menos técnica y, por lo tanto, creo que podría ayudar incluso a los usuarios menos expertos en tecnología. Sin embargo, @DepressedDaniel tuvo una respuesta igualmente excelente aquí que ofrece una explicación más profunda.
Joey Pabalinas
14

Para no utilizar un subconjunto o subproceso, cuando {}se canaliza la salida de una lista compuesta >, el shell guarda el descriptor STDOUT antes de ejecutar la lista compuesta y la restaura después. Por lo tanto, exec >en la lista compuesta no lleva su efecto más allá del punto donde el descriptor anterior se restablece como STDOUT.

Echemos un vistazo a la parte relevante de strace bash -c '{ exec >/dev/null; } >/dev/null; echo hi' 2>&1 | cat -n:

   132  open("/dev/null", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
   133  fcntl(1, F_GETFD)                       = 0
   134  fcntl(1, F_DUPFD, 10)                   = 10
   135  fcntl(1, F_GETFD)                       = 0
   136  fcntl(10, F_SETFD, FD_CLOEXEC)          = 0
   137  dup2(3, 1)                              = 1
   138  close(3)                                = 0
   139  open("/dev/null", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
   140  fcntl(1, F_GETFD)                       = 0
   141  fcntl(1, F_DUPFD, 10)                   = 11
   142  fcntl(1, F_GETFD)                       = 0
   143  fcntl(11, F_SETFD, FD_CLOEXEC)          = 0
   144  dup2(3, 1)                              = 1
   145  close(3)                                = 0
   146  close(11)                               = 0
   147  dup2(10, 1)                             = 1
   148  fcntl(10, F_GETFD)                      = 0x1 (flags FD_CLOEXEC)
   149  close(10)                               = 0

Puede ver cómo, en la línea 134, el descriptor 1( STDOUT) se copia en otro descriptor con índice al menos 10(eso es lo que F_DUPFDhace; devuelve el descriptor más bajo disponible comenzando en el número dado después de duplicar en ese descriptor). Vea también cómo, en la línea 137, el resultado de open("/dev/null")(descriptor 3) se copia en descriptor 1( STDOUT). Finalmente, en línea 147, el antiguo STDOUTguardado en el descriptor 10se copia nuevamente en el descriptor 1( STDOUT). El efecto neto es aislar el cambio a STDOUTen línea 144(que corresponde al interior exec >/dev/null).

Daniel deprimido
fuente
Dado que FD 1 es sobrescrito por FD 3 en la línea 137, ¿por qué la línea 141 no señala el punto 10 a / dev / null?
Joey Pabalinas
@JoeyPabalinas La línea 141 está duplicando FD 1 (es decir, stdout) al siguiente descriptor disponible después de 10 , que resulta ser 11, como puede ver en el valor de retorno de esa llamada al sistema. 10 está codificado en bash para que el guardado de descriptores de bash no interfiera con los descriptores de un solo dígito que puede manipular en su script exec.
DepressedDaniel
Entonces, ¿fcntl (1, F_DUPFD, 10) siempre se referirá a STDOUT sin importar a dónde apunta FD 1 actualmente?
Joey Pabalinas
@JoeyPabalinas No estoy seguro de cuál es tu pregunta. FD 1 ES STDOUT. Ellos son la misma cosa.
DepressedDaniel
Se agregó salida de strace completo a mi publicación original.
Joey Pabalinas
8

La diferencia entre { exec >/dev/null; } >/dev/null; echo "Hi"y { exec >/dev/null; }; echo "Hi"es que la doble redirección lo hace dup2(10, 1);antes de cerrar fd 10, que es la copia del original stdout, antes de ejecutar el siguiente comando ( echo).

Sucede de esa manera porque la redirección externa está superponiendo la redirección interna. Es por eso que copia el stdoutfd original una vez que se completa.

Julie Pelletier
fuente
+1 para explicar la diferencia de manera fácil. La respuesta de AlexP carece de esta explicación.
Kamil Maciorowski