Los captchas

Al principio te pedían que identificases señales y cosas así. Después era que identificases helicópteros… Lo próximo te va a poner una lista de gente y venga: identifica los terroristas afganos entre estos cinco.
Carlos González, Coffee Break Ep. 167

Anuncios

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

La navaja

Nunca tuve en mi poder una navaja. Vaya usted a saber porqué a pesar de que desde pequeño siempre me han fascinado los personajes de las películas, libros y videojuegos de aventuras. Ese hombre (o esa mujer) resuelto, capaz de salir de cualquier apuro con una combinación de habilidad, conocimientos prácticos e ingenio. Podría mencionar a Indiana Jones, a Tintín, a Lara Croft… o a mis amigos. Personas cárnicas de cuerpo humano caliente.
Reconozco que son herramientas la mar de prácticas en mil y una situaciones; y cada vez que veía a alguien utilizarla, me venía el típico pensamiento “globo-chicle“. Una idea fugaz que surge en mi cabeza y que, tan pronto como aparece, explota y se va, desplazada por las cosas que realmente me ocupan, la conversación que estoy manteniendo, o una idea nueva con algo más de sustancia. ¿Por qué narices no tengo yo una también?

La navaja
El caso es que nunca tuve una pero hace algo más de un año, uno de mis mejores amigos se dejó una en mi casa y la enganché a mis llaves para recordar devolvérsela en cuanto tuviese ocasión.
Se trata de una Victorinox Classic SD en color azul, probablemente la navaja más común que existe y que podrías encontrar en casi cualquier parte.

Ésta en concreto, aunque pequeña, dispone de 7 herramientas básicas: tijeras, hoja de cuchillo, lima, destornillador, anilla, mondadientes y pinzas. No utilicé jamás el mondadientes por lo poco higiénico del asunto, pero las demás ya las he empleado todas.
Victorinox es sin duda la marca de navajas suizas por excelencia, tal y como puedes leer en la Wikipedia. Por eso no resulta difícil encontrarlas, incluso a través de Amazon. Otra marca bastante conocida y que me resulta familiar es Leatherman.

El día a día
Aquí van una lista hecha “al vuelo” de cosas mundanas que he llegado a hacer con la pequeña Victorinox:
– Abrir paquetes y cartas
– Cortar esos molestos pellejos de piel alrededor de las uñas
– Extraer astillas cuando me he clavado alguna
– Cortar y pelar cables, cuerdas y bridas
– Quitar las etiquetas a prendas de ropa nuevas
– Ayudarme de las pinzas para manipular tornillos muy pequeños
– Cortar fixo, cinta americana, aislante o de carrocero sin tener que recurrir a los dientes ni estropear más cantidad de la que iba a utilizar
– Abrir envases de comida rebeldes
– Pelar fruta
– Darle vueltas entre mis dedos (tengo las manos inquietas, pero cuidado con la hoja de corte)

El accidente
La situación donde tener la navaja fue determinante sucedió tal día como hoy el verano pasado. Volvíamos de una actuación y ya casi eran las siete de la mañana. En la furgoneta intentábamos no quedarnos dormidos volviendo desde A Cañiza hasta A Coruña; apenas había tráfico y todavía no habíamos pasado Santiago. A la altura más o menos de Padrón, nos encontramos un coche volcado unos pocos cientos de metros más adelante, tras una curva. Aquello nos despertó a todos de sobresalto.
Una mujer gateaba para salir por el hueco de la ventana de los asientos traseros, mientras un señor mayor hacía lo mismo desde la posición del conductor.
– ¡Os**as! ¡Frena! ¡Para ahí!
En apenas un par de segundos fuimos capaces de organizarnos: el conductor estaba llamando ya a los servicios de emergencias. Iba a poner triángulos y hacer señas a los otros vehículos que viniesen, para aminorar y tener precaución: cada vez que pasaba uno, minúsculos cristales y trozos de la carrocería del vehículo saltaban a una velocidad que los convertía en peligrosas cuchillas. El otro compañero, a ayudar al hombre; yo a la mujer. Todavía quedaba una señora en el asiento del copiloto, con el cinturón de seguridad aun puesto (gracias al cielo).
– ¡Emilio busca un cuchillo o un cúter en la furgo para cortar el cinturón!
La señora estaba sangrando por la cabeza y teníamos que sacarla de allí como fuese. Con la mujer sentada junto al quitamiedos, saqué la navaja de mi bolsillo y preparé la cuchilla. Ojalá pudiese cortar el cinturón. Por suerte, conseguimos cortarlo y sacar a aquella pobre señora.
Según el hombre relataba, no sabía lo que había pasado, podría haberse quedado dormido durante unas décimas de segundo. No dejaba de llorar, pedir perdón, clamar por que llegasen rápido las ambulancias e intentar que su mujer no perdiese el conocimiento. Estaban los tres desorientados, en shock. Les recuperamos sus pertenencias básicas. Había un par de teléfonos en el asfalto, llaves, una cartera, etc.
Con unas mantas, unas estructuras plegables de metal y unas espumas que llevábamos en la carga, pudimos hacer una especie de “tienda de campaña” improvisada donde refugiar a aquellas personas de la lluvia y calmarlas. Por suerte, las ambulancias y los bomberos llegaron rápidamente y nosotros continuamos nuestro camino más despiertos que nunca. Hoy en día (con el bonito recuerdo de haber podido ayudar a esta familia) nos reímos imaginando a los bomberos preguntándose al ver la situación: “¿Que rayos transportarán estos tres en esa furgoneta para haber montado este despliegue tan bizarro?” 😝

Otras Multi-herramientas
A raíz de todo lo que he utilizado la navaja desde que la tengo, me gustaría poder incluir también en mi mochila una Multi-tool (por ejemplo ésta, u otra similar). Además de las funciones de mi Victorinox, tienen muchos más usos. Como desventaja, son un poco más grandes y pesadas, con lo que no la utilizaría como llavero. Sin embargo, no está de más y no debería pesar en tu mochila o bandolera.

El regalo
Al volver a ver a mi amigo y contarle la historia con la intención de devolverle su navaja, directamente me dijo:
– Quédatela. Es útil y no tienes así que te la regalo. De todas maneras, yo necesito otra diferente.
Desde ese día, no me he separado nunca de ella. Aprendí la lección de que siempre hay que tener una navaja suiza a mano por lo que pueda pasar.