bash: ¿cómo propagar errores en la sustitución de procesos?

19

Quiero que mis scripts de shell fallen siempre que un comando ejecutado con ellos falle.

Típicamente hago eso con:

set -e
set -o pipefail

(normalmente agrego set -utambién)

El caso es que ninguno de los anteriores funciona con la sustitución de procesos. Este código imprime "ok" y sale con el código de retorno = 0, mientras que me gustaría que fallara:

#!/bin/bash -e
set -o pipefail
cat <(false) <(echo ok)

¿Hay algo equivalente a "pipefail" pero para la sustitución del proceso? ¿Hay alguna otra forma de pasar a un comando la salida de comandos como si fueran archivos, pero generar un error cada vez que falla alguno de esos programas?

La solución de un hombre pobre sería detectar si esos comandos escriben en stderr (pero algunos comandos escriben en stderr en escenarios exitosos).

Otra solución más compatible con posix sería usar canalizaciones con nombre, pero necesito eliminar esos comandos que utilizan la sustitución de procesos como líneas integradas sobre la marcha a partir de código compilado, y crear canalizaciones con nombre complicaría las cosas (comandos adicionales, error de captura para eliminándolos, etc.)

juanleon
fuente
No necesita atrapar errores para eliminar canalizaciones con nombre. De hecho, esa no es una buena manera en absoluto. Considere mkfifo pipe; { rm pipe; cat <file; } >pipe. Ese comando se bloqueará hasta que se abra un lector pipeporque es el shell el que hace el comando open()y, en cuanto haya un lector en pipeel enlace fs para pipeis rm'd, y luego catcopie el archivo en el descriptor del shell para ese conducto. Y de todos modos, si desea propagar un error fuera de un subproceso de proceso: <( ! : || kill -2 "$$")
mikeserv
Gracias por escribir sobre la eliminación de canalizaciones con nombre. Desafortunadamente, la $$sustitución no funciona para mí, ya que la sustitución de este comando no se realiza ya que el comando que utiliza la sustitución del proceso se realiza dentro de una tubería de comando que se genera a partir de un código "no shell" (python). Probablemente debería crear un subproceso en python y canalizarlos programáticamente.
juanleon
Así que usa kill -2 0.
mikeserv
Desafortunadamente "kill -2 0" mataría (señal) a python. Escribir manejadores de señales para realizar lógica de negocios dentro de una aplicación multiproceso no es algo que esté esperando :-)
juanleon
Si no desea manejar las señales, ¿por qué está tratando de recibirlas? De todos modos, espero que las canalizaciones con nombre sean la solución más simple al final, aunque solo sea porque hacerlo correctamente requiere diseñar su propio marco y configurar su propio despachador personalizado. Superar ese obstáculo y todo se junta.
mikeserv

Respuestas:

6

Solo podría solucionar ese problema con eso, por ejemplo:

cat <(false || kill $$) <(echo ok)
other_command

La subshell del script es SIGTERMd antes de que se pueda ejecutar el segundo comando ( other_command). El echo okcomando se ejecuta "a veces": el problema es que las sustituciones de proceso son asíncronas. No hay garantía de que el kill $$comando se ejecute antes o después del echo okcomando. Se trata de la programación de los sistemas operativos.

Considere un script bash como este:

#!/bin/bash
set -e
set -o pipefail
cat <(echo pre) <(false || kill $$) <(echo post)
echo "you will never see this"

El resultado de ese script puede ser:

$ ./script
Terminated
$ echo $?
143           # it's 128 + 15 (signal number of SIGTERM)

O:

$ ./script
Terminated
$ pre
post

$ echo $?
143

Puede probarlo y después de algunos intentos, verá las dos órdenes diferentes en la salida. En el primero, el script finalizó antes de que los otros dos echocomandos pudieran escribir en el descriptor de archivo. En el segundo, el falseo el killcomando probablemente se programaron después de los echocomandos.

O para ser más precisos: la llamada al sistema signal()de la killutilidad que envía la SIGTERMseñal al proceso de shells se programó (o se entregó) más tarde o antes que las write()llamadas de sistema de eco .

Sin embargo, el script se detiene y el código de salida no es 0. Por lo tanto, debería resolver su problema.

Otra solución es, por supuesto, utilizar tuberías con nombre para esto. Pero, depende de su secuencia de comandos lo complejo que sería implementar canalizaciones con nombre o la solución anterior.

Referencias

caos
fuente
2

Para el registro, e incluso si las respuestas y comentarios fueron buenos y útiles, terminé implementando algo un poco diferente (tenía algunas restricciones sobre la recepción de señales en el proceso principal que no mencioné en la pregunta)

Básicamente, terminé haciendo algo como esto:

command <(subcomand 2>error_file && rm error_file) <(....) ...

Luego verifico el archivo de error. Si existe, sé qué subcomando falló (y el contenido del archivo de error puede ser útil). Más detallado y hackeo de lo que originalmente quería, pero menos engorroso que crear tuberías con nombre en un comando bash de una sola línea.

juanleon
fuente
Cualquier cosa que funcione para usted suele ser la mejor manera. Gracias especialmente por volver y responder: los selfies son mis favoritos.
mikeserv
2

Este ejemplo muestra cómo usar killjunto con trap.

#! /bin/bash
failure ()
{
  echo 'sub process failed' >&2
  exit 1
}
trap failure SIGUSR1
cat < <( false || kill -SIGUSR1 $$ )

Pero killno puede pasar un código de retorno de su subproceso al proceso padre.

ceving
fuente
Me gusta esta variación en la respuesta aceptada
sehe
1

De manera similar a como implementaría $PIPESTATUS/ $pipestatuscon un shell POSIX que no lo admite , puede obtener el estado de salida de los comandos pasándolos a través de una tubería:

unset -v false_status echo_status
{ code=$(
    exec 3>&1 >&4 4>&-
    cat 3>&- <(false 3>&-; echo >&3 "false_status=$?") \
             <(echo ok 3>&-; echo >&3 "echo_status=$?")
);} 4>&1
cat_status=$?
eval "$code"
printf '%s_code=%d\n' cat   "$cat_status" \
                      false "$false_status" \
                      echo  "$echo_status"

Lo que da:

ok
cat_code=0
false_code=1
echo_code=0

O puede usar pipefaile implementar la sustitución de procesos a mano como lo haría con shells que no lo admiten:

set -o pipefail
{
  false <&5 5<&- | {
    echo OK <&5 5<&- | {
      cat /dev/fd/3 /dev/fd/4
    } 4<&0 <&5 5<&-
  } 3<&0
} 5<&0
Stéphane Chazelas
fuente