jueves, 3 de noviembre de 2016

Cómo "actualizar" la terminal con solo recompilar

Supongamos que estoy dando clases, resolviendo un ejemplo en el proyector. Me gusta armar las soluciones en vivo, para que se vea el proceso, las decisiones, y hasta las equivocaciones. Entonces, supongamos que empiezo a resolver una partecita de un problema y la pruebo. Si anda paso a otra, si falla retoco el código y vuelvo a probar. En cualquier caso, el ejemplo suele requerir unas cuantas ejecuciones de ese mismo código que va evolucionando de a poco. Estaría bueno tener en ZinjaI una facilidad como la PSeInt: que si mantengo la terminal abierta, al cambiar el código el programa se re-ejecuta automáticamente en esa terminal, "actualizando" el resultado.

No quiero algo tan complejo como esto o esto, ni cerca, sino algo bien bien simple, que no requiera nada de instrumentación, y que tampoco agregue ruido a la clase. Con ZinjaI y la ayuda de bash, es posible, hoy, sin esperar a actualizaciones.

La primer solución es simple: me hago un script de bash con un loop infinito que tenga dos pasos, primero ejecutar mi programa, luego esperar a que el ejecutable cambie. Si tengo eso corriendo en una consola, desde ZinjaI solo mando a compilar luego de cada cambio (shift+F9), y listo, el script lo detecta y actualiza la consola. Veamos cómo hacer eso, integrarlo en ZinjaI, y luego lo mejoramos.


Para saber cuando cambia el ejecutable voy a generar un archivo auxiliar al momento de ejecutar, y luego voy a comparar la fecha de modificación de ese archivo (que será más reciente que la del ejecutable ejecutado), con la del ejecutable. Cuando la del segundo sea más reciente, significará que habrá cambiado, que habrá sido recompilado. Si al script, que llamaré "runloop.sh", lo ejecuto en la carpeta de trabajo de mi programa, y le paso como argumento la ruta del binario y los argumentos de ejecución del mismo empezaría así:

   #!/bin/bash 
   AUX_FILE = /tmp/runloop.$$
   while true; do
      touch "$AUX_FILE"
      "$@"
      while [ ! "$AUX_FILE" -ot "$1" ]; do sleep 1; done
   done

Veamos qué hace. Primero define un nombre para el archivo auxiliar. En este ejemplo lo mandé a /tmp, y le agregué al final el ID del proceso ($$), para que pueda haber más de uno de estos scripts al mismo tiempo, y no se peleen por un mismo archivo. Luego, el loop principal e infinito, primero crea el archivo, luego ejecuta, tomando como comando todo lo que haya recibido el script como argumento desde la linea de comandos, y finalmente con un bucle interior, revisa cada un segundo si cambió el ejecutable.


Antes de mejorarlo, vamos a ponerlo a mano en ZinjaI. Para eso podemos usar las herramientas personalizadas (buscar en el menú "Herramientas"). Luego de darle permisos de ejecución al script y ponerlo en una carpeta de las que figuran en el PATH, mi configuración en ZinjaI quedó así:


Notar que le pedí que compile antes de ejecutar, que le pase como argumentos la ruta del ejecutable y sus propios argumentos de ejecución, que use como carpeta de trabajo para el script la del programa, que lo corra en una terminal y de modo asíncrono, y que quiero tener ese acceso directo en la barra de herramientas. Adicionalmente, luego pueden apretar Ctr+Alt+Z y asignarle un atajo de teclado.


Ahora en ZinjaI, ejecutamos la primera vez con esta herramienta, y luego solo recompilamos (shift+F9). Vamos a mejorarlo. Por un lado, la estética: quiero ponerle como título a la terminal el nombre del ejecutable, que me diga si la ejecución salió mal, y que borre la pantalla antes de comenzar una nueva ejecución. No hay problema, aquí está la nueva versión:

   #!/bin/bash 
   CAPTION="$(echo $1|rev|cut -d '/' -f 1|rev)
   echo -ne "\033]0;$CAPTION\007"
   AUX_FILE=/tmp/runloop.$$
   while true; do
      touch "$AUX_FILE"
      "$@" 
      RET=$?;
      if [ ! "$?" = "0" ]; then echo '<<cod de salida: '$RET'>>'; fi
      while [ ! "$AUX_FILE" -ot "$1" ]; do sleep 1; done
      clear; sleep .5;
   done

Para el tema del nombre, en $1 tengo toda la ruta. La di vuelta con "rev", corté con "cut" hasta el primer '/' (que antes de "rev" era el último), y la enderecé usando "rev" otra vez. Luego el "echo" usa una secuencia de escape mágica para poner ese resultado como título de ventana. Para saber si la ejecución terminó bien, miro el código de salida, que bash pone en $?. El "clear" es el que borra la pantalla, y el "sleep .5" espera medio segundo antes de volver a ejecutar, por dos motivos: para que visualmente se note que se reinició aunque la salida no cambie, y para darle tiempo al compilador de terminar de escribir el ejecutable, y no intentar correrlo cuando va por la mitad.


Finalmente, también quiero poder reiniciar aunque el ejecutable no haya cambiado. Para esto voy a cambiar el "sleep" por un "read". "read" sirve para hacer una lectura por consola, pero tiene un argumento opcional para definir un timeout: que si no leyó nada transcurrido cierto tiempo, se rinda. Y el código de retorno es diferente si leyó, o si se venció el plazo. Entonces, cambio el loop interior por:

   while [ ! "$AUX_FILE" -ot "$1" ] && ! read -t 1; do echo -n; done

El "echo -n" es lo mismo que nada, pero está porque no puedo dejar vacío el cuerpo del while. Ahora, si alguien presiona enter, ese while corta reiniciando así la ejecución aunque el ejecutable no haya sido modificado.


Copiando ese script y la configuración de la imagen ustedes también pueden poner un botón para abrir una vez una terminal de ejecución, y que se reinicie sola luego de cada recompilación. A diferencia de PSeInt, esto no mantiene las entradas de una ejecución a otra. En PSeInt tengo instrumentados al intérprete y a la consola para ello. Acá no puedo hacer algo así de forma transparente, pero el resultado me está sirviendo igual en muchos casos.

No hay comentarios:

Publicar un comentario en la entrada