lunes, 28 de enero de 2013

Nada que no pueda hacer un script de bash (parte 2)

Siguiendo con esta idea de utilizar bash para combinar algunas de las cientos de pequeñas herramientas que se encuentran en casi cualquier GNU/Linux para automatizar tareas varias, en este segundo post traigo algunos ejemplos adicionales. Hay que aclarar, que al igual que en la primer parte, los ejemplos no están explicados 100%, sino que se comenta rápidamente qué hacen y cómo. Un lector con algunos conocimientos mínimos de programación y del uso de la linea de comandos debería poder entender los ejemplos, pero seguramente necesitará googlear un poco más o consultar los manuales para poder modificar estas ideas para sus propias necesidades. De todas formas, el objetivo era solo tirar la primera piedra, para despertar un poco la curiosidad, y si les interesa podrán encontrar fácilmente mucho más material.

Para empezar, un script que utilizaba con la vieja interfaz de sourceforge para ver si había novedades en los foros. Lo traigo a colación para mencionar un par de aplicaciones para interactuar con la Web desde un script:
   DIR=http://sourceforge.net/projects/pseint/forums
   FNAME=$HOME/.pseint_forums
   lynx --dump $DIR | grep "Last Action" > ${FNAME}_temp
   if ! diff -y --left-column ${FNAME}_last ${FNAME}_temp; then
      cp -f ${FNAME}_temp ${FNAME}_last
   fi
La tercer linea invoca a un navegador de consola (sí, esas cosas existen en el mundo GNU/Linux, todo existe en para la consola), y le pide que descargue la página de los foros. lynx (para ver páginas como texto plano) y wget (para descargar archivos) son dos herramientas muy muy útiles cuando la web está en el medio de la tarea. Luego, el script busca las lineas que tienen las fechas de los últimos mensajes de cada foro (las que dicen "Last Action") y las guarda en un archivo. Esto lo compara con el mismo archivo de la vez anterior. "diff" sirve para comparar dos archivos de texto (es genial para comparar código por ejemplo, y es lo que usa ZinjaI desde su menú de herramientas). Con las opciones "-y --left-column" le pide formatear la salida para que sea en dos columnas, la primera con el contenido del archivo de referencia (el viejo), y la segunda con las lineas que cambiaron (del nuevo). Así, si la segunda columna está en blanco, no hay novedades, y sino, dice qué foros ir a ver. El if actualiza el archivo de referencia para que la próxima vez no vuelva a mostrar las mismas novedades.

Pare el siguiente ejemplo les cuento que estoy como ayudante de cátedra en una materia donde utilizamos mucho OpenGL y GLUT para las prácticas y ejemplos. Entonces, es muy común que quiera compilar pequeños programas realizados con estas bibliotecas, tan pequeños que constan de un solo cpp y por eso ni me gasto en armar un proyecto. El siguiente script de una linea compila mediante MinGW32 y wine la versión para Windows, tomando como entrada el nombre del cpp:
   wine d:\\mingw\\bin\\mingw32-g++ $1 -o $(echo $1 | sed 's/.cpp/.exe/') -DFREEGLUT_STATIC -lfreeglut_static -lglu32 -lopengl32 -lwinmm -lgdi32 -Is:\\mingw\\opengl\\include -Ld:\\mingw\\opengl\\lib -static-libgcc
La parte interesante es la de "-o $(echo $1 | sed 's/.cpp/.exe/')". Con -o le digo al compilador el nombre del archivo de salida, que quiero que sea el mismo que el de entrada. Para ello, ejecuto el comando echo para imprimir el nombre del archivo de entrada (argumento del script) y redirecciono esto al comando "sed" que permite hacer operaciones de todo tipo con cadenas. Una de las operaciones más básicas es el reemplazo (que se indica con la "s" inicial del argumento). Entonces le pido que reemplace .cpp por .exe, y esta salida pasa a ser el argumentos para el compilador por estar encerrado entre "$(...)" paréntesis y precedido por el signo "$" como a comenté antes.

Hasta aquí vimos ejemplos con interfaces de linea de comandos (que son las más rápidas a mi gusto), pero ¿qué tal si quiero una interfáz más amigable?, ¿o que tal si el script se ejecuta en un entorno gráfico, sin terminal? Hay muchos programas cuya función es mostrar un pequeño cuadro de dialogo con un mensaje o una pregunta por ejemplo, que se configuran mediante argumentos desde la linea de comandos, y que retornan la selección de alguna forma que pueda tomar un script fácilmente. Por ejemplo, a la salida de error (que puede redireccionarse fácilmente), o como código de retorno. Para terminales de texto, la aplicación para ello es dialog, mientras que para interfaces gráficas hay varias, siendo las más comunes kdialog (de kde), y zenity (basada en gtk). Prueben pasarle "--help" como argumento a cualquiera de ellas para ver la cantidad de cuadros de diálogo que hay para elegir y qué se les puede configurar. Para que se den una idea de la potencia de estas cosas, el instalador de mi distribución favorita (Slackware) está echo mediante un montón de scripts de bash que utilizan dialog para la interfaz. Un ejemplo propio puede ser este:
   ZSDTIME=$(kdialog --title Shutdown --inputbox 'Cuando?' '+45')
   sudo shutdown -h $ZSDTIME
que muestra un cuadro de diálogo donde ingresar un tiempo y programa el apagado de la PC dentro de ese tiempo. kdialog retornará mediante la salida estándar el valor ingresado por el usuario en la ventana gráfica, por eso lo puedo tomar con $(..) y guardar en una variable. Luego, uso esta variable para los argumentos del comando "shutdown".

Otro ejemplo interesante de interfaz (no interactiva) de consola, donde se combinan un montón de pequeñas utilidades, es el siguiente:
   mount /mnt/cd
   export SZ_FULL=$(echo $(df | grep mnt/cd) | cut -d " " -f 2)
   umount /mnt/cd
   dd if=/dev/sr0 of=$1.iso conv=noerror,sync bs=2M &
   export PID=$!
   while ps -p $PID &>/dev/null; do
      export SZ_DONE=$(ls -s $1.iso | cut -d " " -f 1)
      export PERC=$(echo scale=2\;$SZ_DONE\*100/$SZ_FULL | bc)
      echo -n -e \\r "$PERC % done "
      sleep 3
   done
Este script guarda una imágen de un cd o dvd en el archivo que recibe como argumento (en "$1.iso"). Para crear la imágen uso el comando "dd" (data dump) que sirve para copiar datos en crudo de cualquier lugar a cualquier lugar, en este caso del dvd a un archivo. Luego de lanzar "dd", uso la variable especial "$!" para obtener su id de proceso (la variable guarda siempre el id del último proceso lanzado). El while muestra el porcentaje de avance de la copia, actualizándolo cada 3 segundos. Con el "ls -s" extraigo el tamaño del archivo (la parte que lleva copiada) en la variable "SZ_DONE". Con "df" extraigo el tamaño total (en las tres primeras lineas, hay que montarlo para que df lo muestre) en la variable "SZ_FULL". Luego, con "bc" (una genial calculadora con precisión arbitraria), calculo el porcentaje con 2 decimales (por eso el "scale=2;"). Este porcentaje se muestra en la linea actual, por eso el "echo" empieza con "-n -e \\r", para que vuelva al comienzo de esa linea ("\\r"), y para que no avance de linea luego de mostrar el porcentaje ("-n").

Pero continuando con esto de las interfases, siendo los sistemas actuales tan multitareas, probablemente no querramos estar mirando todo el tiempo la terminal para ver la barra de progreso, sino seguir trabajando en otra cosa y volver cuando alla terminado. Una forma de saber "cuando haya terminado" en el entorno gráfico es recibir una notificación en la barra de tareas, y una forma de hacerlo desde un script es mediante la línea:
   notify-send -t 4000 "Terminó!"
Esta linea muestra la notificación con el mensaje "Terminó!" en la barra de tareas durante 4 segundos (por eso el -t 4000). Si no encuentran la instrucción "notify-send" instalen el paquete "libnotify-bin".

Y para cerrar voy a comentar cómo era hace ya bastante tiempo el instalador de PSeInt para GNU/Linux. Sí, tenía instalador en lugar de ser un simple tgz que hay que descomprimir a mano. La idea del instalador la tomé del instalador que hace nvidia para sus drivers de video. Lo que hacía era crear un tgz con todos los archivos y anexarlo al final de un script que los extrae, o sea un archivo autoextraíble. El script que instala tenía una línea como esta:
   tail $0 -c TAMANIODELTGZ | tar -zxC $HOME/.pseint-installer;
que extrae del final ("tail") del archivo del propio script  ("$0") los archivos de pseint, colocándolos en un directorio temporal dentro del home del usuario. Luego el script debe mover estos archivos a donde corresponda y asignarles los permisos necesarios. Esto se adornaba con mensajitos de colores gracias a "echo -e", se le agregaba un "exit 0" al final para que no intente ejecutar los datos comprimidos, y quedaba bastante bien presentado. El script para crear el instalador era más o menos así:
   cat script.sh | replace TAMANIODELTGZ $(du pseint.tgz -b | replace pseint.tgz "") > pseint-l6-$(cat ../version).sh
   cat pseint.tgz >> pseint-l6-$(cat ../version).sh
La primer linea copia el script instalador y coloca dentro del mismo el tamaño del archivo tgz que vamos a concatenar (nota: en lugar de "replace", conviene usar "sed" como vimos antes en otros ejemplo, ya que "replace" viene con mysql, mientras que "sed" está casi siempre). La segunda línea concatena al final del script el comprimido tgz con los archivos del programa a instalar.

Ya sé que nadie va a usar estas líneas como están porque son en su mayoría casos muy particulares, pero espero que les sirva de ejemplo para ver lo fácil que es combinar pequeñas utilidades para resolver problemas. En estos pocos y concisos ejemplos hay salidas formateadas, procesamiento de cadenas, redirecciones entre procesos y con archivos, uso de variables, pasaje de argumentos, comodines y otros símbolos especiales, condiciones y bucles, sustitución de comandos, lanzamientos de procesos en paralelo, cuadros de diálogo y notificación, etc. En fin, un muestrario amplio (aunque para nada exhaustivo) de cosas que se pueden hacer con bash y todas esas pequeñas utilidades de consola que hay en cualquier GNU/Linux, listas para combinar entre sí.

Este post es continuación de Nada que no pueda hacer un script de bash (parte 1).

1 comentario:

  1. Se parecen a los scrips que yo utilizo, los mas complicados que tengo son los que empleo para revisar los logs del servidor de internet, a menudo los termino con un "| wc -l" y despues de una cosa que parece aterradora e incomprensible, el prompt me dice: "7" (espero hacerme entender). Saludos.

    ResponderEliminar