Bash: la salida de captura del comando se ejecuta en segundo plano

Intento escribir un script bash que obtenga el resultado de un comando que se ejecuta en segundo plano. Desafortunadamente no puedo hacer que funcione, la variable a la que asigné la salida está vacía; si reemplazo la asignación con un comando echo todo funciona como esperaba.

#!/bin/bash function test { echo "$1" } echo $(test "echo") & wait a=$(test "assignment") & wait echo $a echo done 

Este código produce el resultado:

 echo done 

Cambiar la asignación a

 a=`echo $(test "assignment") &` 

funciona, pero parece que debería haber una mejor manera de hacerlo.

Bash tiene una función llamada Sustitución de procesos para lograr esto.

 $ echo < (yes) /dev/fd/63 

Aquí, la expresión < (yes) se reemplaza con un nombre de ruta de un archivo (pseudodispositivo) que está conectado a la salida estándar de un trabajo asíncrono yes (que imprime la cadena y en un bucle infinito).

Ahora intentemos leer de él:

 $ cat /dev/fd/63 cat: /dev/fd/63: No such file or directory 

El problema aquí es que el proceso yes terminó mientras tanto porque recibió un SIGPIPE (no tenía lectores en stdout).

La solución es la siguiente construcción

 $ exec 3< <(yes) # Save stdout of the 'yes' job as (input) fd 3. 

Esto abre el archivo como entrada fd 3 antes de que se inicie el trabajo en segundo plano.

Ahora puede leer desde el trabajo de fondo cuando lo prefiera. Para un ejemplo estúpido

 $ for i in 1 2 3; do read < &3 line; echo "$line"; done y y y 

Tenga en cuenta que esto tiene una semántica ligeramente diferente que hacer que el trabajo en segundo plano escriba en un archivo respaldado por la unidad: el trabajo en segundo plano se bloqueará cuando el búfer esté lleno (vaciar el búfer leyendo fd). Por el contrario, escribir en un archivo respaldado por una unidad solo está bloqueando cuando el disco duro no responde.

La sustitución de procesos no es una función POSIX sh.

Aquí hay un truco rápido para dar un respaldo de unidad de trabajo asíncrono (casi) sin asignarle un nombre de archivo:

 $ yes > backingfile & # Start job in background writing to a new file. Do also look at `mktemp(3)` and the `sh` option `set -o noclobber` $ exec 3< backingfile # open the file for reading in the current shell, as fd 3 $ rm backingfile # remove the file. It will disappear from the filesystem, but there is still a reader and a writer attached to it which both can use it. $ for i in 1 2 3; do read <&3 line; echo "$line"; done y y y 

Recientemente, Linux también agregó la opción O_TEMPFILE, lo que hace posibles dichos ataques sin que el archivo sea visible en absoluto. No sé si bash ya lo admite.

ACTUALIZAR :

@rthur, si quiere capturar toda la salida de fd 3, use

 output=$(cat < &3) 

Pero tenga en cuenta que no puede capturar datos binarios en general: solo es una operación definida si el resultado es texto en el sentido POSIX. Las implementaciones que conozco simplemente filtran todos los bytes NUL. Además, POSIX especifica que todas las nuevas líneas finales deben ser eliminadas.

(Tenga en cuenta también que la captura de la salida dará como resultado OOM si el escritor nunca se detiene ( yes nunca se detiene). Pero, naturalmente, ese problema se mantiene incluso para read si el separador de línea nunca se escribe de forma adicional)

Una manera muy robusta de tratar con coprocesos en Bash es usar … el coproc incorporado.

Supongamos que tiene un script o una función llamada banana que desea ejecutar en segundo plano, captura todo su resultado mientras hace algunas stuff y espera hasta que finalice. Haré la simulación con esto:

 banana() { for i in {1..4}; do echo "gorilla eats banana $i" sleep 1 done echo "gorilla says thank you for the delicious bananas" } stuff() { echo "I'm doing this stuff" sleep 1 echo "I'm doing that stuff" sleep 1 echo "I'm done doing my stuff." } 

A continuación, ejecutará banana con el coproc la coproc manera:

 coproc bananafd { banana; } 

esto es como ejecutar banana & pero con los siguientes extras: crea dos descriptores de archivos que están en la matriz bananafd (en el índice 0 para la salida y el índice 1 para la entrada). Capturará la salida de banana con la read integrada de read :

 IFS= read -r -d '' -u "${bananafd[0]}" banana_output 

Intentalo:

 #!/bin/bash banana() { for i in {1..4}; do echo "gorilla eats banana $i" sleep 1 done echo "gorilla says thank you for the delicious bananas" } stuff() { echo "I'm doing this stuff" sleep 1 echo "I'm doing that stuff" sleep 1 echo "I'm done doing my stuff." } coproc bananafd { banana; } stuff IFS= read -r -d '' -u "${bananafd[0]}" banana_output echo "$banana_output" 

Advertencia: ¡debes terminar con las stuff antes de que el banana termine! si el gorila es más rápido que tú:

 #!/bin/bash banana() { for i in {1..4}; do echo "gorilla eats banana $i" done echo "gorilla says thank you for the delicious bananas" } stuff() { echo "I'm doing this stuff" sleep 1 echo "I'm doing that stuff" sleep 1 echo "I'm done doing my stuff." } coproc bananafd { banana; } stuff IFS= read -r -d '' -u "${bananafd[0]}" banana_output echo "$banana_output" 

En este caso, obtendrá un error como este:

 ./banana: line 22: read: : invalid file descriptor specification 

Puedes verificar si es demasiado tarde (es decir, si has tardado demasiado haciendo tus stuff ) porque después de que se coproc el coproc , bash elimina los valores en la matriz bananafd , y es por eso que obtuvimos el error anterior.

 #!/bin/bash banana() { for i in {1..4}; do echo "gorilla eats banana $i" done echo "gorilla says thank you for the delicious bananas" } stuff() { echo "I'm doing this stuff" sleep 1 echo "I'm doing that stuff" sleep 1 echo "I'm done doing my stuff." } coproc bananafd { banana; } stuff if [[ -n ${bananafd[@]} ]]; then IFS= read -r -d '' -u "${bananafd[0]}" banana_output echo "$banana_output" else echo "oh no, I took too long doing my stuff..." fi 

Finalmente, si realmente no quieres perderte ninguno de los movimientos de gorila, incluso si tardas demasiado en tus stuff , puedes copiar el descriptor de archivo de banana a otro fd, 3 por ejemplo, haz tus cosas y luego lee de 3 :

 #!/bin/bash banana() { for i in {1..4}; do echo "gorilla eats banana $i" sleep 1 done echo "gorilla says thank you for the delicious bananas" } stuff() { echo "I'm doing this stuff" sleep 1 echo "I'm doing that stuff" sleep 1 echo "I'm done doing my stuff." } coproc bananafd { banana; } # Copy file descriptor banana[0] to 3 exec 3>&${bananafd[0]} stuff IFS= read -d '' -u 3 output echo "$output" 

Esto funcionará muy bien! la última read también jugará el rol de wait , de modo que la output contendrá la salida completa de banana .

Eso fue genial: no hay archivos temporales para tratar (bash maneja todo en silencio) y 100% pure bash!

¡Espero que esto ayude!

Una forma de capturar el resultado del comando de fondo es redirigir su salida en un archivo y capturar el resultado del archivo una vez que el proceso de fondo ha finalizado:

 test "assignment" > /tmp/_out & wait a=$(