Shell-scripts: errores, señales y trampas

Aunque hace bastante que no preparo ningún script al que valga la pena hacer mención, he estado repasando algunas ideas sobre el tratamiento de errores. El scripting se parece mucho a programar, pero como todo lenguaje, tiene sus puntos fuertes y sus carencias. En particular voy a centrarme en cómo determinar el comportamiento de un script cuando se produce un error durante su ejecución, y como ayudarnos de unas pocas directivas para detectarlos y depurarlos.
Todo lo que viene a continuación está hecho con Bash en mente, así que funcionará tanto en MacOS como en GNU/Linux porque es la Shell más extendida, pero no lo he probado con otros terminales. Por cierto, ya que voy a utilizar las palabras “foo” y “bar“, no está de más aclarar que no tienen ningún significado, simplemente están ahí porque hay que poner un nombre y por convencionalismo.

Detener el script en caso de error
Imaginemos un script con el siguiente código:

Su salida es lo que uno espera. Un error alertándonos de que no existe ningún comando con el nombre “foo” y luego se imprime la palabra “bar”:

./script.sh: line 3: foo: command not found
bar

Aunque ha habido un error, el script ha continuado ejecutándose en la siguiente instrucción. Esto puede resultarnos peligroso o inadecuado para lo que estemos preparando, así que podemos utilizar la directiva “-e” para evitarlo:

Ahora sí, el guión se detiene en cuanto surge el error:

./script.sh: line 4: foo: command not found

He de aclarar que aunque la concatenación de instrucciones tiene implicaciones similares, abusar de ellas convertiría nuestro código en un churro intelegible; pero por si acaso:
foo & echo “bar”: Detecta el error y ejecuta la siguiente instrucción
foo && echo “bar”: Detiene el script cuando el comando devuelve error

La directiva -e no sólo rige los errores de comandos no válidos, sino que se aplica de forma más amplia a otros ejemplos de código, por ejemplo, a resultados de una operación que hayamos almacenado en una variable:

Produce:

ls: foobar: No such file or directory

Si de forma puntual necesitásemos esquivar el comportamiento marcado por esta directiva, podremos utilizar un pequeño truco de lógica:

El operador OR (||) con el segundo operando siendo “true” permitirá continuar la ejecución:

./script.sh: line 4: foo: command not found
bar

Y si nuestro comando fuese una condición de un bucle o de un “if”? Se evaluaría como falso, como por ejemplo aquí:

Se tomaría la rama del “else” y veríamos:

ls: foobar: No such file or directory
bar

Por lo tanto, la directiva -e puede ser de gran ayuda para evitar que nuestro script cometa errores… y que los arrastre durante el resto de su ejecución, que según lo que haga, puede terminar en resultados desastrosos. Pero aún hay más.


Detener el script si el error está en una tubería
No, no vamos a hablar de fontanería digital:

Cuando el error forma parte de una tubería de comandos, donde la salida del primero es la entrada del segundo, y así sucesivamente hasta el final de la tubería; el terminal simplemente indica el error y ejecuta el último comando de la tubería.
¡Ojo! No ejecuta los comandos que estén después del que da error; únicamente el último:

./script.sh: line 4: foo: command not found
a
bar

El problema entonces es que tenemos una tubería rota. Y una fuga. Y no hay nada peor que una fuga en una tubería rota. Eso es horrible. No quieres que eso suceda.
Para evitar que el problema vaya a más, tenemos otra directiva adicional (-o pipefail). Y digo adicional porque requiere que “-e” también esté presente para surtir efecto.

Si, se ejecuta el último comando de la tubería, pero la ejecución se detiene antes de continuar:

./script.sh: line 4: foo: command not found
a

Detener el script si hay variables no declaradas
Supongamos esto:

Nos arroja una línea en blanco porque la variable $a no está inicializada y luego imprime “bar” en la siguiente línea. Hasta aquí todo correcto. Pero si queremos evitar el uso de variables sin inicializar, usaremos la directiva -u:

Y con esto detenemos el script al llegar a ese punto de la ejecución:

./script.sh: line 4: a: unbound variable

Hay un truco que da un valor por defecto a una variable si no está inicializada o si está vacía, dependiendo del operador que utilices. Si quieres consultar más detalles sobre estos operadores, hay mucha información disponible sobre Expansión de Parámetros.
La opción “-u” está implementada de forma suficientemente inteligente como para ser tolerante en estos casos. Por ejemplo:

Digamos que como la variable VAR no estaba incivilizada, el operador “:-” le asigna el valor de la variable “default”. Por eso, la salida de estas líneas es:

5

Sin errores. Perfecto. Ahora veamos como aprovecharnos de esto para evaluar condiciones. El siguiente código se detiene porque la variable “mi_var” está sin inicializar y la directiva “-u” lo ha detectado:

Así lo muestra:

./script.sh: line 4: mi_var: unbound variable

Si queremos esquivarlo (y que el if pueda evaluar si mi_var está vacío o sin determinar, aprovecharemos ese operador:

Y obtenemos el mensaje:

mi_var no esta inicializada

No se detiene la ejecución por un error, ya que podemos evaluar la condición; pero conservamos la protección para no usar variables vacías más adelante. Lo puedes probar intentando mostrarla después por pantalla:

Te da:

mi_var no esta inicializada
./script.sh: line 10: mivar: unbound variable

Debug del código “por las bravas”
La opción “-x” permite imprimir por pantalla cada instrucción antes de que se ejecute. Si tienes un script con errores y todavía no los has localizado, tal vez te interese ir viendo paso a paso lo que sucede. Por ejemplo:

Tendrás:

+ a=5
+ echo 5
5
+ echo bar
bar

Simple, verdad?

Errores y trampas
De forma invisible, cada vez que hemos obtenido un error, el sistema recibe una señal ERR que podremos interceptar si queremos, mediante una trampa.
Una trampa no es más que un código que se ejecuta si se detecta la señal asociada. Algo así como una función de toda la vida, pero que en vez de ser disparada por una instrucción escrita por el programador, responde a la señal emitida por la máquina.

Bash (como otros lenguajes de programación) nos permite hacer esto de una forma bastante simple mediante la opción “-E” (en mayúscula). Por ejemplo:

Provoca:

./script.sh: line 5: foo: command not found
ERR detectado

En cuanto sale el error ERR, la trampa (“trap”) toma el control para ese tipo de señal y ejecuta la orden “echo ERR detectado”. Podríamos hacer una solución un poco más rebuscada, que mostrase el número de línea:

Que arroja:

./script.sh: line 6: foo: command not found
ERR detectado en 6

O incluso, creando un procedimiento algo más estilizado:

Que puede resultar muy práctico si utilizamos el procedimiento para imprimir los errores de diferentes trampas, con el numero de linea también como ayuda:

./script.sh: line 10: foo: command not found
*** ERROR DETECTADO
*** Error en la linea 10

Por último, si modificamos el mensaje donde informamos de la línea del error y cambiamos la variable $1 (donde hemos recibido el número de línea) por el resultado de la función “caller”:

Podemos también mostrar el archivo en el que se produjo el error:

./script.sh: line 10: foo: command not found
*** ERROR DETECTADO
*** Error en la linea 10 ./script.sh

Lo cual puede ser muy útil si el script consiste de varios ficheros.

#################################
Recursos:
https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/
https://unix.stackexchange.com/questions/122845/using-a-b-for-variable-assignment-in-scripts/122878
https://unix.stackexchange.com/questions/39623/trap-err-and-echoing-the-error-line

Anuncios

Estás minando criptomonedas sin saberlo?

Captura de pantalla 2018-04-17 a las 2.06.39

Empieza a ser preocupante con el auge de las criptodivisas (Bitcoin, Monero, etc.) que la avaricia de algunos webmasters esté pasando factura a los ordenadores de sus visitantes. Yo venía notando una carga de trabajo elevada en el portátil últimamente, cada vez que veía alguna serie online.

Sigue leyendo

No quiero registrarme en tu servicio

Ya he escrito en algún que otro post que estoy muy cansado de ir teniendo cuentas abiertas por ahí en servicios de cualquier tipo. He conseguido cerrar cuentas en cosas como Spotify, Mega, Dropbox, etc. porque he encontrado buenos reemplazos para este tipo de software.
Y la verdad es que también me gustaría poder cerrar mi cuenta de Feedly y tan sólo emplear un lector sencillo en mi Mac.
Sigue leyendo