¿Cómo guardar / dev / stdout ubicación de destino en un script bash?

12

Tengo un cierto script bash, que quiere preservar la /dev/stdoutubicación original antes de reemplazar el descriptor del primer archivo con otra ubicación.

Entonces, naturalmente, escribí algo como

old_stdout=$(readlink -f /dev/stdout)

Y no funcionó. Muy rápidamente entiendo cuál era el problema:

test@ubuntu:~$ echo $(readlink -f /dev/stdout)
/proc/5175/fd/pipe:[31764]
test@ubuntu:~$ readlink -f /dev/stdout
/dev/pts/18

Obviamente, se $()ejecuta en un subshell, que se canaliza al shell principal.

Entonces, la pregunta es: ¿hay una manera confiable (con un alcance de portabilidad entre distribuciones de Linux) para guardar la /dev/stdoutubicación como una cadena en un script bash?

alexey.e.egorov
fuente
Esto suena un poco como un problema XY . ¿Cuál es el problema subyacente?
Kusalananda
El problema subyacente es un cierto script de instalación que se ejecuta en dos modos: silencioso, en el que registra toda la salida en un archivo y detallado, en el que no solo se registra en el archivo, sino que también imprime todo en el terminal. Pero en ambos modos, el script desea interactuar con el usuario, es decir, imprimir en el terminal y leer la respuesta del usuario. Así que pensé que guardar /dev/stdoutresolvería el problema con la impresión de mensajes en modo silencioso. La alternativa es redirigir cualquier otra acción que produzca resultados, y hay bastantes. Aproximadamente 100 veces más que los mensajes de interacción del usuario.
alexey.e.egorov
La forma estándar de interactuar con el usuario es imprimir stderr. Esto es, por ejemplo, por qué los avisos irán stderrpor defecto.
Kusalananda
Desafortunadamente, stderrtambién debe ser redirigido y guardado, ya que el script llama a una serie de programas externos, y todos los posibles mensajes de error deben ser recopilados y registrados.
alexey.e.egorov

Respuestas:

14

Para guardar un descriptor de archivo, lo duplica en otro fd. Guardar una ruta al archivo correspondiente no es suficiente, necesitará guardar el modo de apertura, los indicadores de apertura, la posición actual dentro del archivo, etc. Y, por supuesto, para tuberías o tomas anónimas, eso no funcionaría, ya que no tienen camino. Lo que desea guardar es la descripción del archivo abierto al que se refiere el fd, y duplicar un fd en realidad está devolviendo un nuevo fd a la misma descripción del archivo abierto .

Para duplicar un descriptor de archivo en otro, con un shell tipo Bourne, la sintaxis es:

exec 3>&1

Arriba, fd 1 está duplicado en fd 3.

Cualquier cosa que fd 3 ya estuviera abierta antes estaría cerrada, pero tenga en cuenta que los fds 3 a 9 (generalmente más, hasta 99 con yash) están reservados para ese propósito (y no tienen un significado especial contrario a 0, 1 o 2), el Shell sabe que no debe usarlos para su propio negocio interno. La única razón por la que fd 3 habría estado abierto de antemano es porque lo hizo en el script 1 , o la persona que llamó lo filtró.

Luego, puede cambiar stdout a otra cosa:

exec > /dev/null

Y más tarde, para restaurar stdout:

exec >&3 3>&-

( 3>&-para cerrar el descriptor de archivo que ya no necesitamos).

Ahora, el problema con eso es que, excepto en ksh, cada comando que ejecute después exec 3>&1heredará ese fd 3. Esa es una fuga de fd. Generalmente no es un gran problema, pero eso puede causar problemas.

kshestablece el indicador close-on-exec en esos fds (para fds mayores de 2), pero no otros shells y otros shells no tienen forma de establecer ese indicador manualmente.

La solución para otro shell es cerrar el fd 3 para cada comando, como:

exec 3>&-

exec > file.log

ls 3>&-
uname 3>&-

exec >&3 3>&-

Incómodo. Aquí, la mejor manera sería no usar execnada, sino redirigir los grupos de comandos:

{
  ls
  uname
} > file.log

Allí, es el shell el que se encarga de guardar stdout y restaurarlo después (y lo hace internamente duplicándolo en un fd (por encima de 9, por encima de 99 para yash) con el conjunto de indicadores close-on-exec ).

Nota 1

Ahora, la administración de esos fds 3 a 9 puede ser engorrosa y problemática si los usa extensamente o en funciones, especialmente si su script usa algún código de terceros que a su vez puede usar esos fds.

Algunas conchas ( zsh, bash, ksh93, todo ello sumado la característica ( sugerido por Oliver Kiddle dezsh ) alrededor del mismo tiempo en 2005 después de haber sido discutido entre sus desarrolladores) tiene una sintaxis alternativa para asignar la primera fd libre por encima de 10 en vez que ayuda en este caso:

myfunction() {
  local fd
  exec {fd}>&1
  # stdout was duplicated onto a new fd above 10, whose actual value
  # is stored in the fd variable
  ...
  # it should even be safe to re-enter the function here
  ...
  exec >&"$fd" {fd}>&-
}
Stéphane Chazelas
fuente
Además, su código es incorrecto en el sentido de que fd 3 ya podría estar tomado, como sucede cuando se ejecuta un script desde un rc.localservicio, por ejemplo, realmente debería haber usado algo como exec {FD}>&1o algo. Pero esto solo se admite en bash 4, lo cual es realmente triste. Entonces esto no es realmente portátil.
alexey.e.egorov
@ alexey.e.egorov, ver edición.
Stéphane Chazelas
Bash 3. * no es compatible con esta función, y esta versión se usa en Centos 5, que todavía es compatible y aún se usa. Y encontrar un descriptor gratuito y luego eval "exec $i>&1"es algo que me gustaría evitar, debido a lo engorroso. ¿ Realmente puedo confiar en que fds por encima de 9 sería gratis entonces?
alexey.e.egorov
@ alexey.e.egorov, no, lo estás mirando hacia atrás. Los fds 3 a 9 son de uso gratuito (y depende de usted administrarlos como desee) y están destinados a ese fin. Los fds superiores a 9 pueden ser utilizados internamente por el shell y cerrarlos puede tener consecuencias desagradables La mayoría de los proyectiles no te permitirán usarlos. bashte permitirá dispararte en el pie.
Stéphane Chazelas
2
@ alexey.e.egorov, si al comenzar su script tiene algunos fds en (3..9) abiertos, es porque su interlocutor se olvidó de cerrarlos o de configurar el indicador de cierre de ejecución en ellos. Eso es lo que yo llamo una fuga de fd. Ahora, tal vez la persona que llamó tenía la intención de pasar esos fds a usted, para que pueda leer y / o escribir datos desde / hacia ellos, pero entonces lo sabría. Si no los conoce, entonces no le importa, puede cerrarlos libremente (tenga en cuenta que solo cierra el proceso fd de su script, no el de la persona que llama).
Stéphane Chazelas
3

Como puede ver, las secuencias de comandos bash no son como un lenguaje de programación normal donde puede asignar descriptores de archivo.

La solución más simple es usar un subconjunto para ejecutar lo que desea redirigir, de modo que el procesamiento pueda revertirse al caparazón superior que tiene su E / S estándar intacta.

Una solución alternativa sería utilizar ttypara identificar el dispositivo TTY y controlar la E / S en su secuencia de comandos. Por ejemplo:

dev=$(tty)

y luego puedes ...

echo message > $dev
Julie Pelletier
fuente
> Una solución alternativa sería usar tty para identificar el dispositivo TTY y controlar la E / S en su secuencia de comandos. ¿Cómo se hace esto?
alexey.e.egorov
1
Acabo de incluir un ejemplo en mi respuesta.
Julie Pelletier
1

$$ obtendría el PID del proceso actual, en caso de shell interactivo o script el PID de shell relevante.

Entonces puedes usar:

readlink -f /proc/$$/fd/1

Ejemplo:

% readlink -f /proc/$$/fd/1
/dev/pts/33

% var=$(readlink -f /proc/$$/fd/1)

% echo $var                       
/dev/pts/33
heemayl
fuente
1
Si bien es funcional, confiar en una /procestructura específica causa problemas de portabilidad, al igual que el uso /dev/stdoutcomo se menciona en la pregunta.
Julie Pelletier
1
@JuliePelletier ¿Confía en una /procestructura específica ? Que funcionaría en cualquier Linux que tiene procfs..
heemayl
1
Correcto, entonces podemos generalizar para Linux como procfscasi siempre está presente, pero a menudo vemos preguntas de portabilidad y una buena metodología de desarrollo incluye considerar la portabilidad a otros sistemas. bashpuede ejecutarse en una multitud de sistemas operativos.
Julie Pelletier