Comportamiento correcto de las trampas EXIT y ERR cuando se usa `set -eu`

27

Estoy observando un comportamiento extraño cuando uso set -e( errexit), set -u( nounset) junto con trampas ERR y EXIT. Parecen relacionados, por lo que ponerlos en una pregunta parece razonable.

1) set -uno activa trampas ERR

  • Código:

    #!/bin/bash
    trap 'echo "ERR (rc: $?)"' ERR
    set -u
    echo ${UNSET_VAR}
    
  • Esperado: se llama la trampa ERR, RC! = 0
  • Real: no se llama la trampa ERR , RC == 1
  • Nota: set -eno cambia el resultado

2) Usar set -euel código de salida en una trampa EXIT es 0 en lugar de 1

  • Código:

    #!/bin/bash
    trap 'echo "EXIT (rc: $?)"' EXIT
    set -eu
    echo ${UNSET_VAR}
    
  • Esperado: se llama la trampa EXIT, RC == 1
  • Real: se llama a EXIT trap, RC == 0
  • Nota: Cuando se usa set +e, el RC == 1. La trampa EXIT devuelve el RC apropiado cuando cualquier otro comando arroja un error.
  • Editar: hay una publicación SO sobre este tema con un comentario interesante que sugiere que esto podría estar relacionado con la versión Bash que se está utilizando. Probar este fragmento con Bash 4.3.11 da como resultado un RC = 1, por lo que es mejor. Lamentablemente, la actualización de Bash (desde 3.2.51) en todos los hosts no es posible en este momento, por lo que tenemos que encontrar alguna otra solución.

¿Alguien puede explicar cualquiera de estos comportamientos?

La búsqueda de estos temas no tuvo mucho éxito, lo cual es bastante sorprendente dada la cantidad de publicaciones en la configuración y trampas de Bash. Sin embargo, hay un hilo en el foro , pero la conclusión es bastante insatisfactoria.

dvdgsng
fuente
3
A partir de las 4, creo que bashrompió con el estándar y comenzó a poner trampas en subcapas. Se supone que la trampa se ejecuta en el mismo entorno de donde vino el regreso, pero bashno lo ha hecho durante bastante tiempo.
mikeserv
1
Espera un minuto, ¿quieres una solución o una explicación? Y si quieres una solución, ¿una solución para qué exactamente? ¿Qué es lo que quieres que ocurra? set -ey set -uambos están diseñados específicamente para matar un shell con script. Usarlos en condiciones que puedan desencadenar su aplicación matará un shell con script. No hay forma de evitar eso, excepto para no usarlos, y en su lugar para probar esas condiciones cuando se aplican en una secuencia de código. Entonces, básicamente, puedes escribir un buen código de shell, o puedes usarlo set -eu.
mikeserv
2
En realidad, estoy buscando ambos, ya que no pude encontrar suficiente información sobre por -uqué no dispararía la trampa ERR (es un error, así que no debería disparar la trampa) o el código de error es 0 en lugar de 1. El Esto último parece ser un error que ya se ha solucionado en una versión posterior, así que eso es todo. Pero la primera parte es bastante difícil de entender si no se ha dado cuenta de que los errores en la evaluación de shell (expansión de parámetros) y los errores reales en los comandos parecen ser dos cosas diferentes. Para la solución, bueno, como sugirió, ahora estoy tratando de evitar -euy verificar manualmente cuando sea necesario.
dvdgsng
1
@dvdsng - Bien. Ese es el camino a seguir: debe publicar su guión cuando lo haga como respuesta y otorgarse la recompensa. Realmente no me gustan esas opciones: no permiten el manejo de excepciones de manera segura.
mikeserv
1
@dvdsng, donde cualquiera de esas opciones puede ser útil, sin embargo, se encuentra en un contexto subdescamado. Por lo tanto, es concebible que lo que sea que haya estado usando antes se pueda localizar en un contexto de subshell como: (set -u; : $UNSET_VAR)y similar. Este tipo de cosas también pueden ser buenas, de vez en cuando puedes soltar muchas &&: (set -e; mkdir dir; cd dir; touch dirfile)si me entiendes. Es solo que esos son contextos controlados: cuando los configura como opciones globales, pierde el control y se vuelve controlado. Sin embargo, generalmente hay soluciones más eficientes.
mikeserv

Respuestas:

15

De man bash:

  • set -u
    • Trate las variables y parámetros no establecidos que no sean los parámetros especiales "@"y "*"como un error al realizar la expansión de parámetros. Si se intenta la expansión en una variable o parámetro no establecido, el shell imprime un mensaje de error y, si no es -iinteractivo, sale con un estado distinto de cero.

POSIX declara que, en caso de un error de expansión , una shell no interactiva debe salir cuando la expansión está asociada con un shell incorporado especial (que es una distinción que bashse ignora regularmente de todos modos, por lo que tal vez sea irrelevante) o cualquier otra utilidad además .

  • Consecuencias de los errores de Shell :
    • Un error de expansión es aquel que ocurre cuando se llevan a cabo las expansiones de shell definidas en Word Expansions (por ejemplo "${x!y}", porque !no es un operador válido) ; una implementación puede tratarlos como errores de sintaxis si puede detectarlos durante la tokenización, en lugar de durante la expansión.
    • [A] n shell interactivo escribirá un mensaje de diagnóstico a error estándar sin salir.

También de man bash:

  • trap ... ERR
    • Si un sigspec es ERR , el comando arg se ejecuta cada vez que una tubería (que puede consistir en un solo comando simple) , una lista o un comando compuesto devuelve un estado de salida distinto de cero, sujeto a las siguientes condiciones:
      • La trampa ERR no se ejecuta si el comando fallido es parte de la lista de comandos inmediatamente después de una palabra clave whileo until...
      • ... parte de la prueba en una ifdeclaración ...
      • ... parte de un comando ejecutado en una lista &&o ||excepto el comando que sigue al final &&o ||...
      • ... cualquier comando en una tubería pero el último ...
      • ... o si el valor de retorno del comando se está invirtiendo usando !.
    • Estas son las mismas condiciones obedecidas por la opción errexit -e .

Tenga en cuenta que la trampa ERR se trata de la evaluación del retorno de algún otro comando. Pero cuando se produce un error de expansión , no se ejecuta ningún comando para devolver nada. En su ejemplo, echo nunca sucede , porque mientras el shell evalúa y expande sus argumentos, encuentra una -uvariable nset, que ha sido especificada por la opción de shell explícito para provocar una salida inmediata del shell actual con script.

Y así, la trampa EXIT , si la hay, se ejecuta, y el shell sale con un mensaje de diagnóstico y un estado de salida distinto de 0, exactamente como debería hacerlo.

En cuanto a la cosa rc: 0 , espero que sea un error específico de la versión de algún tipo, probablemente relacionado con los dos desencadenantes para la SALIDA que se producen al mismo tiempo y el que obtiene el código de salida del otro (que no debería ocurrir) . Y de todos modos, con un bashbinario actualizado según lo instalado por pacman:

bash <<\IN
    printf "shell options:\t$-\n"
    trap 'echo "EXIT (rc: $?)"' EXIT
    set -eu
    echo ${UNSET_VAR}
IN

He añadido la primera línea para que pueda ver que las condiciones de la concha son los de una concha con guión - es no interactivo. El resultado es:

shell options:  hB
bash: line 4: UNSET_VAR: unbound variable
EXIT (rc: 1)

Aquí hay algunas notas relevantes de registros de cambios recientes :

  • Se corrigió un error que causaba que los comandos asincrónicos no se configuraran $?correctamente.
  • Se corrigió un error que causaba que los mensajes de error generados por errores de expansión en los forcomandos tuvieran el número de línea incorrecto
  • Se corrigió un error que causaba que SIGINT y SIGQUIT no se trapresolvieran en los comandos de subshell asíncronos.
  • Se corrigió un problema con el manejo de interrupciones que causaba que un segundo SIGINT y los subsiguientes fueran ignorados por los shells interactivos.
  • El shell ya no bloquea la recepción de señales mientras se ejecutan trapcontroladores para esas señales, y permite que la mayoría de los trap controladores se ejecuten de forma recursiva (ejecutando trapcontroladores mientras trapse ejecuta un controlador) .

Creo que es el último o el primero lo más relevante, o posiblemente una combinación de los dos. Un trapmanejador es, por su propia naturaleza, asíncrono porque todo su trabajo es esperar y manejar señales asíncronas . Y disparas dos simultáneamente con -euy $UNSET_VAR.

Entonces, tal vez deberías actualizar, pero si te gustas, lo harás con un shell completamente diferente.

mikeserv
fuente
Gracias por la explicación de cómo la expansión de parámetros se maneja de manera diferente. Eso me aclaró muchas cosas.
dvdgsng
Te estoy otorgando la recompensa porque tu explicación fue de gran ayuda.
dvdgsng
@dvdgsng - Gracias. Por curiosidad, ¿alguna vez apareciste con tu solución?
mikeserv
9

(Estoy usando bash 4.2.53). Para la parte 1, la página de manual de bash solo dice "Se escribirá un mensaje de error en el error estándar y se cerrará un shell no interactivo". No dice que se llamará una trampa ERR, aunque estoy de acuerdo en que sería útil si lo hiciera.

Para ser pragmático, si lo que realmente quiere es hacer frente de manera más limpia a las variables indefinidas, una posible solución es colocar la mayor parte de su código dentro de una función, luego ejecutar esa función en un sub-shell y recuperar el código de retorno y la salida stderr. Aquí hay un ejemplo donde "cmd ()" es la función:

#!/bin/bash
trap 'rc=$?; echo "ERR at line ${LINENO} (rc: $rc)"; exit $rc' ERR
trap 'rc=$?; echo "EXIT (rc: $rc)"; exit $rc' EXIT
set -u
set -E # export trap to functions

cmd(){
 echo "args=$*"
 echo ${UNSET_VAR}
 echo hello
}
oops(){
 rc=$?
 echo "$@"
 return $rc # provoke ERR trap
}

exec 3>&1 # copy stdin to use in $()
if output=$(cmd "$@" 2>&1 >&3) # collect stderr, not stdout 
then    echo ok
else    oops "fail: $output"
fi

En mi fiesta consigo

./script my stuff; echo "exit was $?"
args=my stuff
fail: ./script: line 9: UNSET_VAR: unbound variable
ERR at line 15 (rc: 1)
EXIT (rc: 1)
exit was 1
meuh
fuente
¡agradable, una solución práctica que realmente agrega valor!
Florian Heigl