lunes, 21 de enero de 2013

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

El título de este post es una frase que uso mucho. Bash, el shell más común en sistemas GNU/Linux, es un intérprete de comandos en modo consola. Soy de esos que usan mucho mucho la consola, porque creen que así las cosas se hacen más rápido. Mucho más rápido, solo que hay que saber un montón muy grande de trucos, nombres comandos, y argumentos, en apariencia crípticos. Por eso la gente prefiere las ventanitas gráficas, porque no hay que recordar nada, todo es intuitivo. Y las ventanas son geniales para eso, pero es ridículo a mi criterio (y el de otros, recomiendo mucho esta vieja lectura, también en español), comparar las limitaciones de una interfaz gráfica simple, con la potencia de una simple linea de comandos.

Pero tampoco es que tengamos que recordar tantos comandos. Diría que la mayoría de los que usamos las terminales usamos regularmente un 5% de los comandos disponibles, y solo sabemos un 3% de sus argumentos. Es así, se puede hacer mucho con poco. Y si somos haraganes, o nos gusta automatizar todo, podemos tomarnos el trabajo de armar un script con las partes difíciles, largas o tediosas, para luego nunca más tener que escribirlas otra vez, sino simplemente invocar al script. Yo tengo mi notebook llena de pequeños scripts, para todo. Y por eso en realidad no sé tanto de bash, porque las operaciones simples son pocas, y las complicadas están dentro de scripts, así que no necesito memorizarlas. Pero sí necesité ejemplos en primera instancia para hacer esos scripts. Y de eso se tratan estos posts. Pienso presentar algunos ejemplos cortos y explicarlos para que vean la potencia de bash y otras herramientas con las cuales se combina, y para que vean el tipo de cosas que se pueden hacer. Espero que aprendan algo, los motive a acercarse a las lineas de comandos, y se les ocurran ideas para sus propias tareas.

Las explicaciones van a ser rápidas. Voy a asumir que el que lea esto tiene una base de programación, y ha utilizado antes una terminal. El que tenga experiencia entenderá las ideas con solo leer los códigos. Para  el que esté empezando a interiorizarse, les doy el primer envión, y los detalles se los dejo a google y las páginas del manual (escriban "man man" en la consola). El que sepa realmente mucho, imaginará mejores formas de hacer las cosas, pero les recuerdo que ya advertí que yo no sabía tanto, sino que me alcanzaba con poco para rebuscármelas para hacer mucho.

Empecemos, para no romper la tradición, por un "Hola Mundo". Si crean un archivo de texto con cualquier editor llamado "hola.sh", y guardan esto allí:
    echo "Hola mundo"
al ejecutarlo en la consola con el comando "bash hola.sh" podrán ver qué hace. Y claro, ¿qué va a hacer? Escribir "Hola Mundo" en la pantalla. Pero vamos a mejorarlo un poco, que tal si en el archivo colocamos ahora:
    #!/bin/bash
    echo -n "Hola "
    echo -e "\033[32mMundo\033[0m"
¿Cómo funciona esto? La primer línea es un comentario, no se ejecuta. Todas las lineas que comienzan con # son comentarios, pero hay un caso especial: si la primer linea empieza con #! (conocido como sha-bang) indica con qué programa debe ejecutarse ese script si no aclaramos. Es decir, si el archivo tiene permisos de ejecución ("chmod a+x hola.sh") podemos invocarlo con el comando "./hola.sh", y entonces omitimos el "bash" del comienzo. Si omitimos el "bash", el sistema utiliza el shell por defecto del usuario, o el programa que indique esta primer linea. Entonces, con ese comentario especial, estamos aclarando que queremos que lo ejecute siempre con bash. La segunda linea agrega al "echo" un "-n" que indica que no debe saltar de linea después de escribir "Hola", de forma que el siguiente "echo" continuará escribiendo luego de la palabra "Hola " en la misma linea. La tercer linea del script escribe mundo en colores. Si agregamos al echo la opción -e habilitamos unas secuencias especiales (secuencias de escape ansi, las que empiezan con "\033[...") que permiten hacer cosas como cambiar los colores, mover el cursor, borrar la pantalla, cambiar el título de la terminal, etc. En fin, cosas para que el "programa" que hagamos en bash se vea más bonito. Tienen aquí una buena referencia al respecto.

Vamos ahora directo a casos reales. Por ejemplo, el script que uso para empaquetar zinjai para subirlo al sitio (zinjai/src_extras/zinjai-packer en los fuentes de zinjai), entre otras cosas comprime todos los archivos del paquete generando un tgz cuyo nombre se compone por "zinjai-" + tres letras que indican la arquitecutra + fecha de la versión + ".tgz". Con el comando "tar -czvf zinjai-xxx-yyyymmdd.tgz" genero el archivo comprimido, pero debo reemplazar xxx e yyyymmdd por lo que corresponda. Para ello utilizo estas instrucciones:
    VER=$(cat zinjai/src/version.h | head -n 1 | cut -d ' ' -f 3)
    if uname -a | grep x86_64; then ARCH=l64; else ARCH=l32; fi
    if ! tar -czvf zinjai-${ARCH}-${VER}.tgz zinjai; then exit; fi
Este se ve bastante enmarañado para quien no está acostumbrado a este tipo de lenguajes, pero veamos qué pasa línea por línea, ya verán que no es para tanto.

 La primer linea crea una variable llamada VER, y le asigna como contenido el resultado de la salida de un comando. Cuando ponemos un comando entre paréntesis y con un signo $ adelante, el comando se ejecuta y todo eso entre paréntesis se reemplaza por la salida del comando. El comando en cuestión en este caso es una cadena de tres programas. "cat" muestra el contenido del archivo "versión.h" del fuente de zinjai, donde está escrito con un #define la versión que vamos a empaquetar. La salida de cat es pasada como entrada al siguiente comando, lo cual se logra con el pipe (|). El siguiente comando es "head", que toma las primeras lineas de un archivo (el comando opuesto es "tail"), y con "-n 1" le decimos cuantas lineas, en este caso solo una. Esto se lo pasamos al tercer comando ("cut") que la corta en partes, delimitadas por espacios en blanco (por eso "-d ' '"), y de ellas toma solo la tercera ("-f 3"). Si la primer linea decía "#define VERSION 20120321", las partes serán "#define", "VERSION" y "20120321". Así, guardamos el número de la versión tomada de los fuentes de zinjai en la variable VER. Luego, donde escribamos $VER o ${VER} en el script se reemplazará por este número.

Para obtener la arquitectura, que en este caso solo puede ser l32 o l64 porque el script es solo para el paquete para GNU/Linux, llamamos al comando "uname", que muestra información del sistema y del núcleo, le pedimos toda la información posible con "-a"; luego en esta salida buscamos si aparece la cadena "x86_64" con el comando "grep", que obviamente sirve para buscar. Observen que esto está en un if. El comando grep muestra las coincidencias en pantalla, pero además retorna 0 o 1 según encuentre o no coincidencias, por lo cual sirve como condición de un "if". La estructura del "if" es: "if condicion ; then acciones por verdadero ; else acciones por falso ; fi". Entonces en este caso, asignamos "l32" o l64" según corresponda a la variable "ARCH". Luego, el último if, utiliza todas estas variables para pedirle al comando "tar" que empaquete y comprima todo donde corresponde, abortando el script (con el "exit 1") si ese comando falla. Si no falla, se continúa con el resto, que aquí no se muestra.

Veamos ahora un ejemplo de bucle for. El bucle for permite recorrer una lista de archivos muy fácilmente, y eso es algo que uso mucho. Por ejemplo, tengo un programa que analiza un archivo html de la referencia de wxWidgets para extraer de allí los métodos de una clase, para construir el indice de autocompletado para ZinjaI. Para correr este programa sobre todas las clases de la referencia (para todos sus archivos htmls), una por vez, utilizo lo siguiente:
    rm -f wxWidgets
    cat wx_classref.html| grep HREF | cut -d \" -f 2 | cut -d \# -f 1 > class_list
    for A in $(cat class_list); do
        wxAutocompClassExtractor.bin $A >> wxWidgets
    done
La primer linea elimina el indice viejo (para rehacerlo de cero), utilizando "-f" (de force) para que no pida confirmación. La segunda linea toma el contenido del html que tiene el índice de clases, busca los enlaces con "grep", y corta las direcciones de los archivos con "cut". El "> class_list" hace que la salida de esa secuencia de comandos vaya a parar al archivo "class_list" en lugar de a la pantalla. Luego viene un "for", que por cada archivo de class_list, ejecuta el programa que extrae los métodos, y su salida es guardada en el archivo "wxWidgets", pero agregandola al contenido que tenía el archivo previamente en lugar de reemplazarlo por completo (esta es la diferencia entre ">" y ">>").

Finalmente, un ejemplo que recibe argumentos (y que puede resultarles más útil). Al invocar un script puedo pasarle argumentos ("./hola algun_argumento otro_argumento _una mas") y utilizarlos dentro del mismo. Solo hay que tener en cuenta una cosa: bash reemplaza los comodines antes de ejecutar el script. Esto quiere decir que si ejecutamos "./hola *" bash reemplaza "*" por la lista de archivos del directorio actual, y lo que el script recibe son muchos argumentos (uno por archivo) con los nombres, en lugar del "*". Los argumentos dentro del script se referencian con las variables "$1", "$2", "$3", "$4", etc ("$0" sería el comando "./hola"). Pero hay un truco para recorrerlos todos que consiste en utilizar la instrucción "shift", que los rota a la izquierda ("$2" pasa a ser "$1", "$3" a "$2", "$4" a "$3" y así). Entonces, un script para descomprimir cualquier tipo de archivo que le pasemos sería:
    while [ ! "$1" = "" ]; do
        export TYPE=$(file "$1" | cut -d \: -f 2 | cut -d " " -f 2-3)
        case "$TYPE" in
            "7-zip archive")      7za x "$1"       ;;
            "Zip archive")        unzip -x "$1"    ;;
            "xz compressed")      tar -xJvf "$1"   ;;
            "bzip2 compressed")   tar -xjvf "$1"   ;;
            "gzip compressed")    tar -xzvf "$1"   ;;
            "RAR archive")        rar x -kb "$1"   ;;
            "ACE archive")        unace x "$1"     ;;
            "ARJ archive")        unarj x "$1"     ;;
            "Microsoft Cabinet")  cabextract "$1"  ;;
            *)  echo "Unrecognized file format: $(file -b $1)"  ;;
        esac
        shift
    done
Aquí se trabaja siempre con $1, que va rotando por el "shift", hasta que ya no quede nada. El "ya no quede nada" se escribe con un "while" cuya condición es "no $1 igual a nada". Dentro del while, uso el comando file, que analiza el contenido de un archivo y mágicamente (realmente usa magia, "man magic") adivina de qué tipo es. Luego con un case invoco al descompresor que corresponda según el tipo (es el "switch" de otros lenguajes, que aquí termina "esca" que es "case" al revés). Tengo este script denominado "ext" en una carpeta agregada a la variable "PATH" entonces para descomprimir archivos simplemente hago "ext lista_de_archivos", y no tengo que acordarme de cada comando para cada tipo y sus argumentos particulares.

Hay que aclarar que "grep", "cut", "cat", "tar", "head", "uname" y muchos otros no son comandos internos de bash, sino programas que suelen estar instalados en cualquier sistema GNU/Linux. Hasta en esas versiones de GNU/Linux para diskettes (¿se acuerdan de los diskettes?) tenemos usualmente estos comandos o versiones reducidas de ellos. La potencia de bash está entonces en su utilidad como lenguaje de pegamento, para juntar otras herramientas sueltas y construir entre todas algo útil. Eso, sumado a la filosofía Unix, nos da un abanico de posibilidades impresionante. Yo soy un usuario de bash bastante básico, (por ejemplo, jamás hago funciones), pero me alcanza para automatizar casi cualquier cosa en tres lineas. Y eso es genial, porque como siempre digo, para este tipo de tareas rutinarias, no hay nada que no pueda hacer un script de bash.

Este post continúa en Nada que no pueda hacer un script de bash (parte 1).

2 comentarios:

  1. No soy muy ducho en la linea de comandos pero siempre la utilizo, el libro de Neal Stephenson es muy recomendable, brillante ensayo y muy entretenido, lo debo haber leido dos o tres veces, aqui el link directo a la version en español biblioweb.sindominio.net/telematica/command_es/command_es.pdf
    Muy bueno el blog, estoy probando PseInt, saludos y gracias por todo

    ResponderEliminar