Revista Tecnología

Bash: ¿Cómo ejecutar código antes y después de cada comando? Logging, monitorización, notificaciones y mucho más

Publicado el 18 septiembre 2017 por Gaspar Fernández Moreno @gaspar_fm

Bash: ¿Cómo ejecutar código antes después cada comando? Logging, monitorización, notificaciones muchoBash: ¿Cómo ejecutar código antes y después de cada comando? Logging, monitorización, notificaciones y mucho más

Con el fin de hacer nuestro sistema (o servidor) más flexible. Aunque podemos hacer muchas cosas con estas técnicas. Es de gran utilidad poder ejecutar un script justo antes de la ejecución de cualquier orden de Bash y que, justo cuando esta orden termine, podamos ejecutar cualquier otra cosa. Esto nos abre las puertas, por ejemplo a notificaciones de inicio/finalización, logging, monitorización y muchas más posibilidades que veremos en futuros posts. Cosas que podremos hacer tanto en el ámbito local, como en servidores de forma muy sencilla.

Aunque otros intérpretes, como ksh, tcsh, zsh y algunos más tienen facilidades para estas tareas. Vamos a hacerlo en bash, ¡porque somos unos valientes! Porque se instala por defecto con muchas distribuciones y porque es el que me he encontrado con más frecuencia.

Ejecutar algo cuando termina un comando

Cuando un comando se ha ejecutado y Bash nos vuelve a pedir una nueva orden, justo antes de darnos el control, y para mostrar el prompt (ese texto que suele ser usuario@host /directorio/actual y termina en un dólar o una almohadilla), Bash, ejecutará lo que contenga la variable PROMPT_COMMAND, ya sea una función, un alias, o la ejecución de un programa. Vamos a hacer una prueba:

Incluso si sólo pulsamos enter, como se vuelve a generar el prompt, justo antes nos mostrará la fecha (porque el comando a ejecutar es date)

Como ejemplo práctico, vamos a hacer un pequeño script que nos notificará cuando alguna partición de nuestro disco duro llegue a un punto crítico, en este caso, vamos a revisar todas las particiones de /dev/sdb y avisar al usuario cuando alguna de estas sobrepase el 90% (opciones que podremos configurar luego), para ello, vamos a crear un script llamado critp.sh con lo siguiente:

Y lo cargaremos de la siguiente manera:

Con esto, cada vez que pulsemos enter o finalice un comando, veremos el conjunto de dispositivos que ha alcanzado ese porcentaje de espacio:

Bash: ¿Cómo ejecutar código antes después cada comando? Logging, monitorización, notificaciones mucho
Bash: ¿Cómo ejecutar código antes y después de cada comando? Logging, monitorización, notificaciones y mucho más

Una buena idea sería también notificar sólo a veces, tal vez con una probabilidad del 50%. Para que no siempre esté calculando ni nos sintamos abrumados por los mensajes de notificación. Para ello, podemos dejar el script así:

Ya tenemos lo básico. Aunque estaría muy bien conocer información sobre el comando que se acaba de ejecutar y que, por supuesto, tenemos a nuestra disposición como es el propio comando ejecutado y su estado de salida. Más adelante podremos saber si se ha ejecutado realmente un comando, porque claro, cuando pulsas enter, vuelve a llamarse a la función. Ahora mismo, está muy bien si queremos realizar tareas de mantenimiento, notificar al usuario si está mal de espacio en disco, etc. Como el siguiente ejemplo que servirá para notificar al usuario cuando termine una determinada tarea. Suponemos que estamos en un entorno gráfico, así que avisaremos con zenity:

Ahora, cargaremos el archivo con source. Y podremos ejecutar cualquier comando con normalidad, aunque, cuando vayamos a hacer algo que vaya para largo podremos hacer lo siguiente:

De esta forma, cuando termine la ejecución de sleep 100, o de cualquier otra cosa que creamos que va a tardar mucho, recibiremos un mensaje parecido a este:

Bash: ¿Cómo ejecutar código antes después cada comando? Logging, monitorización, notificaciones mucho
Bash: ¿Cómo ejecutar código antes y después de cada comando? Logging, monitorización, notificaciones y mucho más

Este script tiene algunas cosas curiosas. Para hallar el estado de la ejecución de las órdenes, utilizo PIPESTATUS. Esta variable, si el comando es sencillo, devolverá sólo un valor, haciendo lo mismo que $?. Pero cuando hemos llamado varios comandos unidos por una pipe o tubería, nos va a devolver el estado de todos los comandos ejecutados. Eso sí, como PIPESTATUS se actualiza tras cada cosa que ejecutamos, lo primero que haremos nada más entrar en la función será guardar su valor en otra variable, y así nos aseguramos de no perderlo.

Y esto está muy bien cuando estamos haciendo varias cosas y se nos puede ir la cabeza con una tarea. Nos puede servir para cuando estemos creando o restaurando un dump de base de datos, cuando estemos haciendo una copia de archivos (local o remota), o estemos realizando alguna tarea de cálculo intenso. Tan solo tenemos que llamar antes de la tarea en cuestión a notify y ya se encargará él de todo. Podemos extender la funcionalidad de notify, incluso aprovechar lo que voy a contar a continuación para hacerlo más potente (te adelanto que en unos días publicaré un script mucho más completo).

Ejecutar algo justo antes de llamar al comando

Para tener control total sobre los comandos ejecutados es interesante poder ejecutar código antes de la ejecución de los comandos. Esto, principalmente nos ayuda a depurar nuestros scripts, porque sabremos en qué orden se ejecuta cada cosa, pero también puede ser una buena herramienta de control, gran ayuda para hacer una visualización interactiva de comandos o, para saber lo que ejecutan nuestras visitas o los comandos que se ejecutan en un servidor además de muchas otras utilidades.

Como ejemplo, empezaremos con algo básico, simplemente haciendo echo antes y después de una llamada. Y, para dar un valor añadido, ya que somos capaces de ejecutar algo antes de hacer cualquier llamada, podremos detectar cuando simplemente se pulsa enter. Ya que, cuando pulsamos enter, la primera función, precmd, no se va a ejecutar:

Trabajando un poco en este código podremos ser capaces, por ejemplo de guardar un log de todo lo que los usuarios ejecutan, simplemente introduciendo:

En lugar del echo "INICIA EJECUCION". Así podemos hacer que el sistema sea totalmente transparente al usuario. Además, para completar el registro podremos incluir otra función:

Que vinculamos a cualquier error. Es decir, cancelación de ejecución o que el código de salida de lo que sea que hayamos llamado no sea 0. Por supuesto, podríamos ayudarnos de más traps como int (para Control+C), quit (para Control+\), term (para cuando se mata el proceso), etc.

Bueno, y, hagamos algo interesante como evitar que el usuario ejecute cosas que no queremos que ejecute:

En este caso, haremos return 0, sólo cuando ejecutemos shopt -u extdebug y el comando ping. Mostrando el texto " ACCESO DENEGADO" si queremos ejecutar cualquier otra cosa. Esto en la práctica no vale de nada, tendríamos que introducir muchas restricciones o tal vez denegar (return 1) ciertas palabras, manteniendo el return 0 por defecto. Pero nos da una idea de cómo funciona.
Gracias a la opción extdebug, esta función es capaz de denegar la ejecución de la orden que hemos llamado en función del valor de retorno de la función denyaccess().
En este caso, el argumento-s de shopt, activa (set) esta capacidad. Del mismo modo, cuando utilizamos -u, la desactiva, y por eso esa orden está permitida, porque es un programa de ejemplo.

Para todo lo demás, incluso nos podemos servir de expresiones regulares para Bash, o incluso la llamada a una función.

Y, para terminar, vamos a hacer sustituciones de órdenes dentro de un comando. O lo que es lo mismo, cambiar el comando que vamos a ejecutar. Podríamos afectar a cualquier parte de la orden ejecutada, aunque para hacer un sencillo ejemplo, vamos a cambiar las llamadas a sudo por gksudo, así vemos un bonito entorno gráfico y será más llevadero para el usuario:

Incorporar a nivel de sistema estos scripts

Los scripts que hagamos utilizando estas técnicas podemos cargarlos a nivel de usuario, introduciendo:

en el archivo ~/.bashrc o a nivel de sistema añadiendo esa línea en /etc/profile , /etc/bash.bashrc , o, mejor aún, en un nuevo archivo dentro de /etc/profile.d/

En las próximas semanas quiero publicar algunos ejemplos en los que veremos, aún más, la potencia de estas acciones.

También podría interesarte...


Volver a la Portada de Logo Paperblog