jueves, 4 de junio de 2009

HAL y la burocracia (IX - repeticiones)

Quizá la ventaja más señalada de un computador respecto del humano es su capacidad de repetir una y otra vez las mismas operaciones sin acusar aburrimiento o cansancio alguno. No hace falta escarbar en las zonas profundas e invisibles de su trastienda para asistir a un aquelarre de incontables repeticiones, incluso la mayoría de las órdenes amigables que hemos aprendido no son sino procesos en los que una misma operación se repite hasta agotar todos los datos de la entrada. Un grep, por ejemplo, no es otra cosa que la repetición de la misma operación, la de buscar en una línea la presencia de un patrón textual, aplicada a todas las líneas de los ficheros que se le dan como argumentos.

Aunque nuestros guiones no han aplicado conscientemente hasta ahora esta facultad repetitiva, el guión del que nos ocupamos estos días es un buen pretexto para conocer algunas herramientas lingüísticas nuevas que permitan programar repeticiones o bucles, como se denominan técnicamente.

Veamos, para empezar, un ejemplo fácil de bucle.

Supongamos que queremos que HAL realice una copia de los ficheros que existen en nuestro subdirectorio actual (~/guiones/informe_suspensos). Nuestro objetivo, en concreto, es que para cada fichero del subdirectorio se obtenga una copia con el nombre fichero.copia. Carece de sentido hacerlo a mano:

cp 1-ge-minimos.tex 1-ge-minimos.tex.copia
cp 1-ge-objetivos.tex 1-ge-objetivos.tex.copia
...

En lugar de ello, podemos utilizar una orden de este tipo:

Para cada 'fichero' en la lista de todos los ficheros del directorio actual, haz una copia con el nombre 'fichero.copia'

O, dispuesta cada expresión en una línea independiente:

Para cada 'fichero' en la lista de todos los ficheros del directorio actual
haz
una copia con el nombre 'fichero.copia'

Podemos traducir, sin más, parte de las expresiones actuales:

Para cada 'fichero' en $(ls *)
haz
cp 'fichero' fichero.copia

La palabra entrecomillada ('fichero') tiene todo el aspecto de ser una variable temporal, pues su valor cambiará según el fichero del caso. Por tanto, podemos confiar en que la siguiente versión es adecuada:

Para fichero en $(ls *)
haz
cp $fichero $fichero.copia

Aunque adecuada, la última línea de esta traducción contiene una ambigüedad. ¿Cuál es el nombre de la variable que aparece en último lugar? Para HAL será todo lo que hay después del signo '$', o sea, fichero.copia. Sin embargo, lo que pretendemos es que la variable sea fichero y .copia un sufijo que se añadirá a cada nombre de fichero concreto. En estos casos, se utilizan llaves para delimitar con precisión absoluta el nombre de la variable: ${fichero}.copia. Mantener la ambigüedad aquí no va a tener efecto negativo, pero conviene irse acostumbrando a estas sutilezas, cuya omisión puede ser letal en otras ocasiones. El fragmento anterior queda, pues, suprimida la ambigüedad, de esta forma:

Para fichero en $(ls *)
haz
cp $fichero ${fichero}.copia

Lo nuevo es poco más que inglés de andar por casa:

for fichero in $(ls *)
do
cp $fichero ${fichero}.copia

un pequeño elemento ortográfico más, el done con que se cierra el bucle:

for fichero in $(ls *)
do
cp $fichero ${fichero}.copia
done

y el, aunque opcional, siempre recomendable, toque de estilo:

for fichero in $(ls *)
do
cp $fichero ${fichero}.copia
done

Para el caso concreto del ejemplo, el código se puede abreviar, eliminando la sustitución de comandos y utilizando únicamente el comodín en su lugar, que en un bucle for tienen el mismo significado [Ver, además, la matización de Vicho en el primer comentario al artículo, si se quiere redondear la sintaxis definitiva]:

for fichero in *
do
cp $fichero ${fichero}.copia
done

Volvamos a nuestro guión provistos de estos nuevos conocimientos.

Se recordará que el modelo para el caso simple contaba con que el fichero notas sólo contuviera una única línea. Vamos a eliminar esta última restricción y aceptar cualquier número posible de alumnos registrados. Digamos que nuestro fichero notas contiene las siguientes líneas:

Don-Quijote-de-la-Mancha:6-gm:10
Sancho-Panza:1-ge:3
Sansón-Carrasco:3-gm:4
Dulcinea-del-Toboso:1-gm:5

El objetivo es que para cada línea del fichero, se ejecuten las instrucciones contenidas en el modelo simple. Recordemos que la versión final de este modelo simple realizaba, por este orden, las siguientes operaciones:

# Verificar que el campo tercero ---la nota--- de la línea del
# fichero sea menor que la nota mínima.
if [ $(cut -d':' -f3 $NOTAS) -lt $NOTA_MINIMA ]
...

### ( En el caso de que se cumpla la condición )
# Convertir el campo primero ---el nombre del alumno--- de la línea
# del fichero al formato que tendrá en la salida impresa y
# guardarlo en la variable 'ALUMNO_OUTPUT'.
ALUMNO_OUTPUT=$(cut -d':' -f1 $NOTAS | sed -e 's/-/ /g')

# Convertir el campo segundo ---el código del curso--- de la línea
# del fichero al formato que tendrá en la salida impresa y
# guardarlo en la variable 'ALUMNO_OUTPUT'.
CURSO_OUTPUT=$(cut -d':' -f2 $NOTAS | sed ...)

# Abrir una sesión interactiva para crear el fichero temporal
# 'trabajo_tmp.tex' que contenga las propuestas de actividades.
...
cat > trabajo_tmp.tex

# Realizar las sustituciones necesarias en el fichero '$PLANTILLA'
# y generar como resultado el fichero 'Sancho-Panza.tex'.
sed ... $PLANTILLA > Sancho-Panza.tex

# Procesar el fichero que resulta del paso anterior con pdflatex.
pdflatex Sancho-Panza.tex

Comencemos, pues, las modificaciones.

El primer paso es crear el bucle que permita realizar esta serie de procesos para cada línea del fichero $NOTAS:

for linea in $(cat $NOTAS)
do

acciones
done

Se observará que para que el guión pueda leer cada línea del fichero y actuar sobre cada una de ellas, tenemos primero que darle acceso a su contenido. cat es una orden que se puede utilizar para este propósito.

El segundo paso es cambiar adecuadamente las entradas de las órdenes que, en nuestra versión previa, accedían al fichero $NOTAS. Puesto que ahora deben ejecutarse no para el fichero en cuanto unidad cerrada, sino para cada una de sus líneas, tendremos que cambiar el método de entrada del dato, ya no vale utilizar el nombre del fichero como argumento, sino que se requiere pasar cada línea a la orden correspondiente mediante una tubería. Veamos las órdenes afectadas en su versión original (en verde) y en su versión modificada (en rojo).

### 1.
# Versión previa
if [ $(cut -d':' -f3 $NOTAS) -lt $NOTA_MINIMA ]

# Nueva versión
if [ $(echo $linea | cut -d':' -f3) -lt $NOTA_MINIMA ]

### 2.
# Versión previa
ALUMNO_OUTPUT=$(cut -d':' -f1 $NOTAS | sed -e 's/-/ /g')

# Nueva versión
ALUMNO_OUTPUT=$(echo $linea | cut -d':' -f1 | sed -e 's/-/ /g')

### 3.
# Versión previa
CURSO_OUTPUT=$(cut -d':' -f2 $NOTAS | sed ...)

# Nueva versión
CURSO_OUTPUT=$(echo $linea | cut -d':' -f2 | sed ...)

Una tercera modificación obligatoria en sustituir en las órdenes correspondientes los nombres de ficheros que aludían a Sancho Panza por una expresión que devuelva el nombre del alumno según la línea del fichero que en ese momento de ejecución del bucle se esté procesando:

### 1.
# Versión previa
sed ... -e 's/RECUPERACIÓN/\\input{trabajo_tmp}/' $PLANTILLA > Sancho-Panza.tex

# Nueva versión
sed ... -e 's/RECUPERACIÓN/\\input{trabajo_tmp}/' $PLANTILLA > $(echo $linea | cut -d':' -f1).tex

### 2.
# Versión previa
pdflatex Sancho-Panza.tex

# Nueva versión
pdflatex $(echo $linea | cut -d':' -f1).tex

El último paso para la generalización del guión es procurar un nombre genérico para los ficheros de objetivos y contenidos mínimos, de forma que se acceda al fichero que corresponda al curso del alumno cuya línea se esté procesando en cada momento. Ya, de paso, recurriremos, para construir el nombre genérico de dichos ficheros, a las variables iniciales OBJETIVOS y CONTENIDOS, no referidas en el guión hasta ahora:

# Versión previa
sed ... -e 's/OBJETIVOS/\\input{1-ge-objetivos}/' \
-e 's/CONTENIDOS/\\input{1-ge-minimos}/' ...

# Nueva versión
sed ... -e "s/OBJETIVOS/\\\input{$(echo $linea | cut -d':' -f2)-${OBJETIVOS}}/" \
-e "s/CONTENIDOS/\\\input{$(echo $linea | cut -d':' -f2)-${CONTENIDOS}}/" ...

Se advertirá que esta modificación en la secuencia de reemplazo de las ordenes sed nos obliga a utilizar comillas dobles. Además, puesto que el carácter '\', al igual que '$', mantiene su significado especial cuando va seguido de '\' y dentro de comillas dobles, es necesario volver a escaparlo, lo cual produce el triple '\\\' (!)

Si unimos todas las modificaciones obtenemos esta versión inicial, realmente abigarrada, de nuestro guión generalizado para un fichero $NOTAS con un número arbitrario de líneas:

# Directorio que contiene la programación del curso
PROGRAMACION="$HOME/guiones/informe_suspensos"

# Fichero que contiene la plantilla LaTeX del informe
PLANTILLA=informe_plantilla.tex

# Fichero que contiene las notas de los alumnos
NOTAS=notas

# Sufijo de los ficheros de objetivos por cursos
OBJETIVOS=objetivos.tex

# Sufijo de los ficheros de contenidos por cursos
CONTENIDOS=minimos.tex

# La nota mínima para aprobar el curso
NOTA_MINIMA=5

for linea in $(cat $NOTAS)
do

if [ $(echo $linea | cut -d':' -f3) -lt $NOTA_MINIMA ]
then
# El nombre del alumno y curso con el formato que tendrán en la salida impresa
ALUMNO_OUTPUT=$(echo $linea | cut -d':' -f1 | sed -e 's/-/ /g')
CURSO_OUTPUT=$(echo $linea | cut -d':' -f2 | sed -e 's/\([[:digit:]]*\)-\(.*\)/\1\.º \2/' \
-e 's/ge/EE/' \
-e 's/gm/EP/')

# Las propuestas de trabajo se deben introducir interactivamente
echo "Introduzca propuestas de trabajo para $ALUMNO_OUTPUT [\item ... (^D para salir)]: "
cat > trabajo_tmp.tex

sed -e "s/ALUMNO/$ALUMNO_OUTPUT/" \
-e "s/CURSO/$CURSO_OUTPUT/" \
-e "s/OBJETIVOS/\\\input{$(echo $linea | cut -d':' -f2)-${OBJETIVOS}}/" \
-e "s/CONTENIDOS/\\\input{$(echo $linea | cut -d':' -f2)-${CONTENIDOS}}/" \
-e 's/RECUPERACIÓN/\\input{trabajo_tmp}/' $PLANTILLA > $(echo $linea | cut -d':' -f1).tex

pdflatex $(echo $linea | cut -d':' -f1).tex
fi
done

El autor advierte que la versión no es definitiva, hay demasiadas repeticiones y demasiadas complejidades como para mantenerla así, por muy sucios y rápidos que pretendamos ser. En realidad, esta versión no es ni siquiera la primera que puede surgir en la mente de quien la crea. Como se verá, ante complejidades evidentes, la mente tiende a simplificar desde las fases iniciales.

Poner a prueba esta versión pasa, naturalmente, por disponer, en el directorio desde el se ejecuta el guión, de los ficheros de objetivos y mínimos correspondientes a los cursos en que están matriculados los alumnos suspensos. Así, para el quijotesco ejemplo de cuatro personajes que dimos hace un momento serían necesarios los citados ficheros para 1-ge, que ya habíamos redactado en anteriores sesiones, y para 3-gm, cuya redacción es tarea del lector.

Para finalizar la densa sesión de hoy puede ser interesante introducir una nueva orden de HAL que nos permita observar de un vistazo las modificaciones que vamos realizando en las distintas versiones de nuestros guiones. Una herramienta clásica de HAL para este propósito es diff. Otra, es colordiff, que, en sistemas Debian, es seguramente necesario instalar previamente. Si la primera versión completa del guión, la versión para el caso simple, la guardamos como generar_informes-1 y ésta nueva como generar_informes-2, las diferencias entre ambas versiones se puede mostrar mediante la instrucción [las opciones añadidas son las mismas que las de diff y se pueden consultar en la página de manual correspondiente]:

colordiff -u -Bbi generar_informes-1 generar_informes-2

Que produce el resultado que aparece en esta imagen:



Resumen

  • Los procesos repetitivos pueden requerir y muchas veces exigen el uso de construcciones especiales, denominadas bucles. Un bucle típico es el bucle for, que se atiene a la siguiente sintáxis:

    for variable in lista
    do
    acciones
    done

  • La orden diff ---o su versión coloreada, colordiff--- permite contemplar las diferencias existentes entre dos ficheros.

2 comentarios:

  1. Cuando una variable temporal lleva el nombre de un fichero es conveniente ponerla entre comillas al referenciarla. Siguiendo el ejemplo del artículo:

    for fichero in *
    do
    cp $fichero ${fichero}.copia
    done

    este for fallaría si uno de los ficheros del directorio contuviera espacios. La versión con comillas dobles hace lo indicado:

    for fichero in *
    do
    cp "$fichero" "${fichero}.copia"
    done

    En general, es buena idea entrecomillar las variables cuando hacen referencia a un archivo, pero esto no resuelve todos los problemas asociados con los nombres de archivo. Por ejemplo, no funcionaría si un archivo se llamara "-b" (o incluso peor, borrar sin cuidado un archivo llamado "-rf ..") , pero entrar en tanto detalle es excesivo. Que haya espacios en un archivo me parece más común :-)

    ResponderEliminar
  2. Me parece muy oportuno tu comentario, porque, aunque el guión que estamos escribiendo no manipula ningún fichero que lleve espacio ---se ha evitado conscientemente ;-)---, no es infrecuente este tipo de problemas. De hecho, ya nos encontramos con esta misma cuestión en este artículo, allí afectaba a un parámetro posicional, pero el mismo principio vale para para cualquier cosa, también nombres de ficheros. He sido, quizá demasiado puntilloso en lo de las llaves, que, con frecuencia, se omiten, cuando no generan problemas, pero menos explícito con las comillas. Hay unas cuantas sutilezas de esta clase en la lengua de HAL, como podrá ver el lector ;-)

    Dejo constancia en el cuerpo del artículo de tu comentario.

    ResponderEliminar