Mostrando entradas con la etiqueta Conversaciones con HAL. Mostrar todas las entradas
Mostrando entradas con la etiqueta Conversaciones con HAL. Mostrar todas las entradas

viernes, 2 de octubre de 2009

Los abundantes rizos de HAL

De las cuatro partes en que se dividía nuestro problema inicial queda la última, la de concatenar los pdfs generados por cada página del libro en un único pdf allí donde la obra escaneada consta de varias páginas. Tampoco esta tarea reviste dificultad especial. La herramienta adecuada a esta fin es, como comentamos entonces, pdftk.

Un rápido vistazo por la página de manual de pdftk nos da la solución. Pongamos, por ejemplo, que queremos obtener un único pdf, con el nombre Dowland_The-Frog-Galliard.pdf a partir de estos tres:

Dowland_The-Frog-Galliard_1.pdf
Dowland_The-Frog-Galliard_2.pdf
Dowland_The-Frog-Galliard_3.pdf

La operación se realiza con la siguiente instrucción, que asume que en nuestro directorio de hojas escaneadas no hay otra cosa que pdfs ---se recordará que el guión escanear-a-pdf, diseñado en el artículo anterior, suprimía debidamente los ficheros del mismo nombre que no fueran pdfs:

pdftk Dowland_The-Frog-Galliard_* cat output Dowland_The-Frog-Galliard.pdf

Con esta expresión pedimos a pdftk que tome como entrada todos los ficheros que empiezan por Dowland_The-Frog-Galliard_ (por tanto, todas las páginas correspondientes a esta obra de Dowland), los concatene (cat) y produzca como resultado (output) el fichero Dowland_The-Frog-Galliard.pdf. (Es importante destacar que pdftk acepta varios pdfs como entrada y que podemos referirnos a un conjunto de ellos mediante el comodín '*'. Este tipo de abreviatura, que hemos visto de pasada en alguna otra ocasión, resulta de enorme utilidad cuando el objetivo es ejecutar operaciones para un conjunto de ficheros cuyos elementos no pueden especificarse completamente a priori. De hecho, la solución que veremos a continuación aprovecha plenamente esta característica de la lengua de HAL.)

El artículo y la conversación podrían terminar aquí, si no fuera porque el lector ya no es un usuario principiante de la consola y porque no debería conformarse sino con una solución general y automática para todos los casos posibles.

Una solución automática no podemos obtenerla para la primera tarea, más allá, de donde lo hemos logrado. Es decir, no es practicable producir un pdf para toda hoja del libro o, lo que es lo mismo, dejar el libro en el escáner y regresar a las tres horas para apagar el aparato con la certeza de que éste habrá digitalizado todas sus páginas. Aquí, naturalmente, hay un límite físico, pues no disponemos del hardware que sea capaz de tal hazaña. Aunque existen robots que escanean libros ---que pasan las hojas por sí solos antes de escanearlas--- su precio está muy por encima de nuestro presupuesto, y plantearse construir uno supera nuestra destreza ingenieril. No queda más remedio que realizar a mano la tarea de selección de la hoja, de su ubicación en el escáner y de su procesamiento mediante nuestro guión escanear-a-pdf. Pero el límite físico desaparece en el mismo momento en que HAL está en posesión de todos los ficheros digitales necesarios. Ahí ya no tenemos disculpa.

Imaginemos, por ejemplo, que tras una sesión de escaneo, hemos obtenido los siguientes ficheros correspondientes a las páginas de dos obras de Dowland:

Dowland_The-Frog-Galliard_1.pdf
Dowland_The-Frog-Galliard_2.pdf
Dowland_The-Frog-Galliard_3.pdf
Dowland_A-Fancy_1.pdf
Dowland_A-Fancy_2.pdf
Dowland_A-Fancy_3.pdf
Dowland_A-Fancy_4.pdf

Lo que, naturalmente, desearíamos es que HAL ejecutase, sin nuestra intervención, estas dos instrucciones:

pdftk Dowland_The-Frog-Galliard_* cat output Dowland_The-Frog-Galliard.pdf
pdftk Dowland_A-Fancy_* cat output Dowland_Fantasia.pdf

La cosa no parece difícil. Tenemos en general un enunciado con esta forma:

pdftk obra_* cat output obra.pdf

donde el texto en cursiva corresponde a una variable ---un pronombre, si se quiere aplicar la metáfora lingüística--- que estará en cada caso por el nombre de la obra correspondiente.

Podemos pergeñar, sin rebanarnos los sesos, un procedimiento para manejar esta clase de situación, lo hemos hecho en otros momentos. Podríamos seguir, por ejemplo, la siguiente estrategia:

  1. Diseñar una instrucción que lea todos los ficheros del directorio que contiene las hojas escaneadas y extraiga el nombre de cada obra distinta.

  2. Crear una variable, digamos, la variable OBRAS, y asignarle como valor los nombres de obras producidos en la instrucción anterior.

  3. Recorrer cada nombre almacenado en la variable OBRAS y ejecutar para cada uno de ellos la instrucción pdftk propuesta antes.


Si aplicamos las herramientas de manipulación de ficheros y flujos de texto conocidas de anteriores conversaciones, podemos llegar a diseñar una instrucción de extracción como la que se indica en el primer paso de nuestro proceso. Podríamos, por ejemplo, construir por medio de tuberías, una instrucción, cada una de cuyas secciones realizase estas operaciones:

  1. Listar todos los ficheros del directorio que contiene las hojas escaneadas en pdf.

  2. Seleccionar de la lista las hojas que constituyen páginas de una misma obra, o sea, aquellas que terminen con un número (el número de página). Nótese que puede haber nombres de fichero sin número al final, que corresponderían a las obras formadas por una sola hoja o página.

  3. Extraer de esos nombres de fichero la parte relativa al nombre de la obra, esto es, el nombre del fichero sin el número de página.

  4. Eliminar las repeticiones en la lista obtenida en los pasos anteriores.


Una instrucción como la siguiente refleja esta serie de manipulaciones:

ls | grep -E '.*_[[:digit:]]+.pdf' | cut -d'_' -f1,2 | sort -u

Va de suyo que la instrucción funciona ---en concreto, el grep y el cut---, porque asumimos un determinado formato para el nombre de nuestros ficheros, a saber:

autor_nombre-de-obra_página.pdf

Gracias a él, el patrón de expresión regular para grep puede contar con que el número de página, de haberlo, está justo antes de la especificación de extensión y después del resto del nombre del fichero. Asimismo cut puede utilizar el carácter '_' como delimitador de campos y devolver los campos primero y segundo del nombre del fichero proporcionado como entrada ---o sea, los que contienen el nombre del autor y de la obra, sin número de página (campo tercero)---, y que identifican exhaustivamente el nombre de la obra del caso.

En general, y cuando se trata de manipular nombres de ficheros, conviene siempre establecer un formato uniforme fácilmente manipulable por las herramientas habituales con las que contamos. Esto nos librará de un montón de quebraderos de cabeza.

Asignar el resultado de esta instrucción a la variable OBRAS es trivial. De hecho, sabemos ya que tanto la instrucción anterior como la asignación se pueden realizar en un único paso, por medio de la sustitución de órdenes:

OBRAS=$(ls | grep -E '.*_[[:digit:]]+.pdf' | cut -d'_' -f1,2 | sort -u)

Lo que resta es recorrer el contenido de la variable OBRAS. Tras la operación anterior dicha variable será una lista de los nombres de obras que constan de varias páginas. Nada mejor, pues, que nuestro conocido for para aplicar sobre cada miembro de esa lista la operación de concatenación de pdfs sugerida al principio:

for obra in $OBRAS
do
pdftk ${obra}_* cat output ${obra}.pdf
done

Es fácil tratar de sacar más partido del for y, por ejemplo, eliminar las páginas independientes, una vez producida la partitura completa, o, incluso, guardar en un fichero de registro referencia de las obras procesadas y del número de páginas de las que consta cada una de ellas. El guión completo, con estos añadidos y comentarios al código, sería el siguiente:

# Lista de las obras que contienen varias páginas. El formato de
# fichero de cada página es autor_titulo-de-la-obra_<n>, donde <n>
# es el número de página
OBRAS=$(ls | grep -E '.*_[[:digit:]].pdf' | cut -d'_' -f1,2 | sort -u)

for obra in $OBRAS
do
# número de páginas de la obra procesada
npags=$(ls ${obra}_* | wc -l)
# concatena las páginas de la obra
pdftk ${obra}_* cat output ${obra}.pdf
# elimina las páginas sueltas
rm ${score}_*
# añade una línea de registro
echo "$obra: $npags páginas" >> pdftk_log
done

Como se ve, tareas cuya automatización podría resultar intratable con herramientas gráficas superpotentes, se resuelven en pocos pasos a través de la humilde consola.

jueves, 1 de octubre de 2009

HAL también escanea (II)

Provistos de las herramientas adecuadas y consultadas someramente sus respectivas páginas de manual, construir el esquema de nuestro guión para escanear cada página y obtener un pdf ajustado a nuestros intereses es trivial [En cursiva se presentan el nombre del fichero que corresponde a la página escaneada; con rojo y verde indicamos, respectivamente, los ficheros de entrada y de salida implicados en cada parte del proceso]:

scanimage > fichero
unpaper fichero fichero.pbm
convert fichero.pbm fichero.pdf

Se observará que en la primera instrucción hemos omitido la extensión del tipo de fichero (pbm) por mera cuestión de conveniencia. Como sabemos que el fichero es un pbm y, puesto que unpaper producirá también un pbm, evitamos algo más prolijo como:

scanimage > fichero-version-inicial.pbm
unpaper fichero-version-inicial.pmb fichero-version-retocada.pbm
...

Este esquema se puede enriquecer con opciones adecuadas para cada una de las instrucciones que lo componen:

scanimage --resolution 600dpi --mode Lineart --progress > fichero
unpaper --size a4 fichero fichero.pbm
convert -compress Group4 fichero.pbm fichero.pdf

Las opciones de scanimage tienen el siguiente significado:

--resolution 600dpi

Define la resolución que tendrá la imagen resultante. En nuestro caso 600 puntos por pulgada, una resolución bastante alta. Resoluciones inferiores como 300dpi suelen ser suficientes y tienen la ventaja de acelerar la duración del proceso físico del escaneo.

--mode Lineart

Determina la sensibilidad del escáner a las diferencias de matiz o color. En este caso interesa considerar sólo las diferencias entre blancos y negros (modo Lineart), la opción común en páginas de libros convencionales.

--progress

Presenta en la consola o terminal una barra visual del progreso del proceso físico del escaneo.


Por su parte, la opción de unpaper es comprensible de suyo:

--size a4

Retoca el fichero dado como primer argumento para que se adapte a las dimensiones de un tamaño a4. unpaper es un programa sofisticado con un buen número de opciones, aquí nos limitamos a un uso muy básico.


Finalmente, en cuanto a convert tenemos la siguiente opción:

-compress Group4

Determina el algoritmo de compresión que se aplicará en la conversión. Con este algoritmo conseguimos, por ejemplo, que el pdf de una página de nuestro libro ronde los 100k sin apenas pérdida de calidad. Dicho sea de paso, Imagemagick, que es la herramienta general a la que pertenece convert, es una utilidad tremendamente especializada y eficaz en el tratamiento de imágenes. [Más información en http://www.imagemagick.org].


Definida la sucesión de instrucciones que constituye el proceso completo de transformación de la página del libro en un fichero pdf, queda construir un guión que pueda aplicarse a cualquier página en general. No es algo distinto de lo que ya hemos hecho en otras ocasiones (ver, en particular, este artículo).

Por tanto, se trata de añadir lo que sea necesario para poder pedirle a HAL algo como esto:

./escanear-a-pdf Dowland_The-Frog-Galliard_1

donde Dowland_The-Forg-Galliard-1 es el nombre del fichero (sin la extensión .pdf) que alojará el pdf de la hoja del libro correspondiente a la primera página de la pieza de Dowland titulada The Frog Galliard, y donde, por descontado, escanear-a-pdf es el nombre de la instrucción o guión que vamos a diseñar.

El artículo citado antes nos da la pista necesaria para que sea cosa de niños la generalización que deseamos. Basta, como se ve, con sustituir la variable escrita anteriormente con el nombre fichero por un parámetro posicional:

scanimage --resolution 600dpi --mode Lineart --progress > "$1"
unpaper --size a4 "$1" "$1".pbm
convert -compress Group4 "$1".pbm "$1".pdf

(Las comillas que rodean al parámetro posicional ---se recordará--- sirven para evitar problemas cuando el nombre de fichero elegido contiene espacios.)

Podemos, ya que estamos, incluir al final una instrucción de limpieza que elimine los ficheros intermedios:

scanimage --resolution 600dpi --mode Lineart --progress > "$1"
unpaper --size a4 "$1" "$1".pbm
convert -compress Group4 "$1".pbm "$1".pdf
rm "$1" "$1".pbm

Con todo ello estamos en condiciones de producir los pdfs deseados de cuantas páginas del libro vayamos a escanear, por ejemplo:

./escanear-a-pdf Dowland_The-Frog-Galliard-1
./escanear-a-pdf Dowland_The-Frog-Galliard-2
./escanear-a-pdf Dowland_The-Rrog-Galliard-3
./escanear-a-pdf Dowland_Fantasía-1
...


Como decíamos, cosa de niños.

miércoles, 30 de septiembre de 2009

HAL también escanea

No hace mucho ;-) dejamos a nuestro HAL entregado a sus tareas rutinarias ---el sueño también forma parte de ellas---, mientras nosotros nos tomábamos unas largas vacaciones. Y es que la inmersión realizada el año pasado fue tan acelerada que había que salir a flote como fuera. Mejor retomar el hilo con algo más de moderación y de iniciar nuevas conversaciones sólo cuando la ocasión lo pida o lo permita, es decir, sin horario ni planificación prefijada.

Una ocasión de este tipo le ha surgido al autor hace poco, y lo que de ella se sigue será útil para completar lo que ya vimos sobre repeticiones y bucles, aparte de para seguir demostrando con un caso real la multiplicidad de posibles aplicaciones de la lengua de HAL a nuestras tareas habituales.

Se recordará que la instrucción for nos sirvió entonces para ordenar a HAL que recorriese las líneas de un fichero y aplicase un mismo bloque de instrucciones a cada una de ellas. En la siguiente conversación veremos un uso semejante, probablemente más frecuente, de for, en el que las unidades textuales recorridas no son líneas de ficheros, sino nombres de fichero.

Aunque ésta es probablemente la forma más frecuente de realizar tareas repetitivas en la lengua de HAL, es decir, la de recorrer unidades textuales (sean, por ejemplo, las líneas de un fichero o los nombres de una serie de nombres de fichero), hay situaciones en que es más práctico, más fácil o, simplemente, más del gusto del usuario, repetir una serie de instrucciones un número determinado de veces. Y esto es justo lo que vamos a tratar de conseguir en la conversación que hoy iniciamos.

Dedicaremos este primer artículo a especificar la tarea que motivó la conversación y la forma en que puede diseñarse un procedimiento para ejecutarla. De paso, aprenderemos a escanear un documento sin salir de la consola, cosa que quizá muchos, acostumbrados a las aplicaciones gráficas, consideren poco menos que magia negra. Aunque ---todo hay que decirlo--- esta clase de sortilegios, lanzados desde la negra interfaz de la consola, es algo a lo que deberíamos ya estar acostumbrados a estas alturas de nuestras charlas.

La tarea que nos proponemos realizar es la siguiente. Contamos con un libro voluminoso que contiene la obra completa de obras para laúd solo del gran compositor renacentista John Dowland y necesitamos extraer de él unas cuantas piezas que quepan fácil y livianamente en nuestro atril. Nos interesa, en particular, generar un fichero pdf para cada una de esas piezas.

Resulta evidente que aquí nos enfrentamos a dos problemas diferentes, en primer lugar, el de escanear las correspondientes páginas del libro y, en segundo lugar, el de generar tantos pdfs como obras queremos extraer. Nótese, en especial, que puede haber obras que consten de una sola página y obras que consten de varias páginas, y que lo que pretendemos es producir un pdf por obra y no por página.

Además, y puesto que sabemos ---el autor lo sabe, el lector debe asumirlo como presupuesto--- que HAL es capaz generar eficazmente un pdf por obra si cuenta con un pdf por página, el primero de los problemas se bifurca, a su vez, en otros dos, el del escaneo propiamente dicho y el de la conversión en pdf del resultado de dicho escaneo.

Hay que añadir, además, que, a causa tanto del tamaño del libro como de sus características físicas, será necesario retocar la imagen inicialmente escaneada para que el pdf que resulte de su conversión se adecue mejor a su posterior impresión en papel A4.

Por tanto, nuestro problema completo constará, al menos, de las siguientes partes:

  1. Escanear las páginas del libro.

  2. Retocar el resultado como convenga.

  3. Convertir los ficheros resultantes en pdfs.

  4. Unir en un único pdf los ficheros de cada página, cuando la obra conste de varias.


Podemos ser todavía más precisos si tenemos en cuenta las especificidades del proceso de escaneo. En concreto, será necesario producir una imagen donde se tengan en cuenta sólo las diferencias entre blanco y negro (lo que se llama modo Lineart) y no los tonos de grises o las divergencias de colores, que en nuestro libro no existen. Esto producirá un formato de imagen pbm. Este tipo de fichero es que el que tendremos que retocar y convertir a pdf, antes de pasar a la concatenación de los diversos pdfs. Podríamos, pues, plantear un esquema de las operaciones implicadas y los datos que se procesan dentro de ellas [Por cada página se presenta la serie de procesos por la que atravesará y los formatos de ficheros resultantes]:

/-------\ pbm /-------\ pbm /----------\ pdf
P1->|escaneo|---->|retoque|---->|conversión|-----\
\-------/ \-------/ \----------/ |
|
/-------\ pbm /-------\ pbm /----------\ pdf | /-------------\
P2->|escaneo|---->|retoque|---->|conversión|-----|--->|concatenación|->pdf
\-------/ \-------/ \----------/ | \-------------/
|
/-------\ pbm /-------\ pbm /----------\ pdf |
Pn->|escaneo|---->|retoque|---->|conversión|-----/
\-------/ \-------/ \----------/

Una inmersión en las interioridades del sistema de paquetes de HAL y de las páginas del manual ---cosas, ambas, que ya tratamos en la primera parte de esta serie--- nos permite descubrir de qué posibles herramientas disponemos. En concreto [señaladas bajo cada caja]:

/-------\ pbm /-------\ pbm /----------\ pdf /-------------\
Pn->|escaneo|---->|retoque|---->|conversión|---->|concatenación|->pdf
\-------/ \-------/ \----------/ \-------------/
scanimage unpaper convert pdftk

Cada una de estas herramientas ---o, si se quiere, de estos ayudantes de HAL--- forma parte de un paquete. A saber, en sistemas basados en Debian:

scanimage
paquete sane-utils

unpaper
paquete unpaper

convert
paquete imagemagick

pdftk
paquete pdftk


En los próximos días nos las apañaremos para construir un par de guiones que generen pdfs a partir de las páginas escaneadas y que procedan, en su caso, a su posterior concatenación. Será con la venia de estos bien predispuestos ayudantes.

HAL está esperando :-)

sábado, 6 de junio de 2009

HAL y la burocracia (XII - limpieza)

Los buenos profesionales de la construcción, la fontanería o de cualquier otra actividad que interviene en la infraestructura de nuestra casa no dan por finalizado su trabajo hasta haber borrado completamente la huellas de su actividad reformadora. Hoy en día, urgidos por el afán de amasar dinero fácil o, simplemente, carentes de toda ética profesional, son habituales los chapuceros que nos acaban dejando la casa hecha un zaguán.

No vayamos a emular esta desagradable actitud y procuremos que tras la ejecución de nuestro guión todo quede limpio y bien dispuesto.

Por lo general, un guión genera dos tipos de basura: basura permanente, compuesta por ficheros temporales, ya sea creados por nosotros mismos o por las órdenes que lo componen, y basura en pantalla, esto es, mensajes de información que se producen al ejecutar dichas órdenes. Los primeros se eliminan fácilmente con la orden rm; los segundos, redirigiéndolos al agujero negro de HAL.

Estas operaciones de limpieza deben efectuarse solamente una vez que se ha comprobado fehacientemente la corrección de nuestro guión. De hecho, los ficheros temporales y los mensajes producidos en su ejecución son información valiosa para descubrir la fuente de errores potenciales. Y ---conviene recordarlo--- no es probable que un guión esté libre de fallos en su primeras redacciones.

El paso de eliminar los ficheros temporales es, a estas alturas, pan comido. Los ficheros temporales creados por nuestro guión son:

  • Los ficheros objetivos_tmp.tex, contenidos_tmp.tex y trabajo_tmp.tex

  • Los ficheros producidos por pdflatex, cuyo nombre es el mismo que el del fichero pdf resultante: $ALUMNO.pdf, pero con extensiones diferentes: $ALUMNO.tex, $ALUMNO.aux, etc.


La orden para eliminar los ficheros citados en primer lugar puede ser ésta, que borra todos los ficheros cuyo nombre termina en _tmp.tex:

rm *_tmp.tex

Para eliminar los ficheros citados en segundo lugar, podemos utilizar una orden como la siguiente:

rm $(ls ${ALUMNO}* | grep -v '\.pdf')

Esta orden envía la lista de todos los ficheros cuyo nombre comienza por $ALUMNO a grep, que devuelve todos menos el que termina con la extensión pdf ---para eso sirve la opción -v de grep---, y lo filtrado por grep se lo da como argumento a rm.

Para que los mensajes de información emitidos por las órdenes que se ejecutan dentro del guión no aparezcan en pantalla, el recurso habitual es redirigirlos a un fichero especial, llamado /dev/null, un agujero negro dentro de HAL que succiona todo lo que llega a él y lo hace desaparecer en la nada. De las órdenes incorporadas en nuestro guión, pdflatex va a producir siempre mensajes informativos, como habrá comprobado el lector que haya puesto a prueba las versiones del guión dadas hasta ahora. Si no queremos molestar al usuario con estos mensajes, podemos añadir la redirección a /dev/null a dicha orden:

pdflatex ${ALUMNO}.tex > /dev/null

La nueva versión del guión, generar_informes-5, con estas tres medidas higiénicas incluidas, queda así:

# 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

# Fichero sed para limpiar etiquetas LaTeX
NOLATEX=limpiar_tex.sed

for linea in $(cat $NOTAS)
do
if [ $(echo $linea | cut -d':' -f3) -lt $NOTA_MINIMA ]
then
# Nombre y curso del alumno tal como constan en el fichero $NOTAS
ALUMNO=$(echo $linea | cut -d':' -f1)
CURSO=$(echo $linea | cut -d':' -f2)

# El nombre del alumno y curso con el formato que tendrán en la salida impresa
ALUMNO_OUTPUT=$(echo $ALUMNO | sed -e 's/-/ /g')
CURSO_OUTPUT=$(echo $CURSO | sed -e 's/\([[:digit:]]*\)-\(.*\)/\1\.º \2/' \
-e 's/ge/EE/' \
-e 's/gm/EP/')

# Ficheros que contienen los objetivos y contenidos del curso
# en que el alumno está matriculado
F_OBJETIVOS=${PROGRAMACION}/${CURSO}-$OBJETIVOS
F_CONTENIDOS=${PROGRAMACION}/${CURSO}-$CONTENIDOS

# Los objetivos y contenidos en un formato apto para la plantilla
# a partir de la que se genera el informe
sed -f $NOLATEX $F_OBJETIVOS > objetivos_tmp.tex
sed -f $NOLATEX $F_CONTENIDOS > contenidos_tmp.tex

# 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{objetivos_tmp}/' \
-e 's/CONTENIDOS/\\input{contenidos_tmp}/' \
-e 's/RECUPERACIÓN/\\input{trabajo_tmp}/' $PLANTILLA > ${ALUMNO}.tex

pdflatex ${ALUMNO}.tex > /dev/null

# Eliminación del los ficheros temporales creados
rm *_tmp.tex
rm $(ls ${ALUMNO}* | grep -v '\.pdf')

fi
done

Las tres únicas diferencias dignas de mención respecto de la versión propuesta al principio de esta serie son:

  • El valor de la variable PROGRAMACION, un valor que lógicamente dependerá del lugar donde el usuario tenga almacenados los ficheros de objetivos y mínimos.

  • La instrucción lp ${ALUMNO}.pdf > /dev/null, que envía el informe del alumno directamente a la impresora, en caso de que el usuario disponga de un sistema de impresión basado en CUPS, lo normal estos días en las distribuciones GNU/Linux.

  • El enigmático #!/bin/sh, que nos acompaña desde el pasado de estas conversaciones, que no es estrictamente necesario en la mayoría de los casos, y cuyo sentido tocará explicar en su momento.


Si el lector ha sido capaz de alcanzar esta cota sin desfallecer, puede considerarse un usuario avanzado y, si le apetece verlo de esta otra forma, un programador en ciernes. Enhorabuena, Sancho, te subiremos la nota.


Resumen:

  • Cuando la corrección de un guión se ha comprobado suficientemente, es recomendable limpiar el directorio desde el que se ejecuta de todos los ficheros temporales generados y suprimir de la interfaz del usuario los mensajes informativos que las órdenes aplicadas pueden generar.

  • La opción -v de grep invierte el sentido de la búsqueda, esto es, devuelve las líneas de la entrada que no encajan con el patrón suministrado como primer argumento.

  • El fichero especial /dev/null existe para redirigir a él cualquier salida de nuestras órdenes que queremos hacer desaparecer del universo visible.

viernes, 5 de junio de 2009

HAL y la burocracia (XI)

Entre el guión original y la última versión obtenida en el proceso de reconstrucción que estamos siguiendo, hay ya muy pocas diferencias, todas ellas relativamente triviales.

En primer lugar, y con objeto de aliviar la exposición, hemos partido de un presupuesto falso, a saber, que los ficheros reales de objetivos y mínimos ---los del autor--- contenían sólo la etiqueta de LaTeX \item. La realidad es un poco más compleja. En tales ficheros aparecían, además, las etiquetas: \section, \subsection, \subsubsection, \paragraph, \begin{itemize} y \end{itemize}. Ahora bien, la plantilla a partir de la cual se generan los informes particulares de cada alumno cuenta con que los ficheros incluidos mediante \input en los entornos Objetivos y Contenidos constan de líneas precedidas por la etiqueta \item y que ninguna otra etiqueta LaTeX está presente. Esto significa que los ficheros originales curso-objetivos.tex y curso-minimos.tex deben sufrir un proceso de depuración antes de ser incluidos mediante \input. Una forma sencilla de lograrlo es utilizar una instrucción sed que elimine todas las etiquetas mencionadas, excepto \item:

sed -e '/\\.*section/d' \
-e '/\\.itemize/d' \
-e '/\\paragraph/d' ${CURSO}-objetivos.tex

sed -e '/\\.*section/d' \
-e '/\\.itemize/d' \
-e '/\\paragraph/d' ${CURSO}-minimos.tex

Puesto que el proceso de depuración debe aplicarse, como acabamos de ver, tanto al fichero de objetivos como al de mínimos, y puesto que, como aprendimos ayer, hay que rehuir de las redundancias, es oportuno utilizar aquí un fichero que contenga las expresiones sed anteriormente definidas y que se pase como argumento a sed (mediante su opción -f) en sendas órdenes de depuración.

Creamos, pues, el fichero que llamaremos limpiar_tex.sed, cuyo contenido será:

/\\.*section/d
/\\.*itemize/d
/\\paragraph/d

Y, dado que el fichero en cuestión es un dato de entrada al mismo nivel que el fichero $NOTAS o el directorio $PROGRAMACION, una variable inicial que se refiera al nuevo fichero parece lo más indicado:

# Fichero sed para limpiar etiquetas LaTeX
NOLATEX=limpiar_tex.sed


Ahora podemos pasar ese fichero como argumento de la opción -f de sed para obtener los ficheros de objetivos y mínimos que se incluirán en los ficheros .tex resultantes de las conversiones realizadas sobre la plantilla:

sed -f $NOLATEX ${CURSO}-$OBJETIVOS > objetivos_tmp.tex
sed -f $NOLATEX ${CURSO}-$CONTENIDOS > contenidos_tmp.tex

Se habrá notado que el resultado se redirige a un par de ficheros temporales, que serán, en último término, los que se incluirán en el fichero .tex de cada informe. Es evidente que es esto lo que queremos y no, por ejemplo, sobreescribir los ficheros originales de objetivos y mínimos.

Para mayor claridad y versatilidad, se pueden definir dos variables puestas al valor del nombre del fichero de objetivos y mínimos de cada caso, variables que pueden a su vez pasarse como argumentos de las órdenes sed anteriormente citadas en el lugar de los propuestos antes:

# Ficheros que contienen los objetivos y contenidos del curso
# en que el alumno está matriculado
F_OBJETIVOS=${PROGRAMACION}/${CURSO}-$OBJETIVOS
F_CONTENIDOS=${PROGRAMACION}/${CURSO}-$CONTENIDOS


# Los objetivos y contenidos en un formato apto para la plantilla
# a partir de la que se genera el informe
sed -f $NOLATEX $F_OBJETIVOS > objetivos_tmp.tex
sed -f $NOLATEX $F_CONTENIDOS > contenidos_tmp.tex


Lo que resta es, simplemente, modificar adecuadamente, las expresiones correspondientes de la última orden sed, que, claramente, acaba adquiriendo un aspecto mucho más amigable (sin las comillas dobles, sin variables y sin la triple barra invertida):

# Versión previa
sed -e "s/ALUMNO/$ALUMNO_OUTPUT/" \
-e "s/CURSO/$CURSO_OUTPUT/" \
-e "s/OBJETIVOS/\\\input{${CURSO}-${OBJETIVOS}}/" \
-e "s/CONTENIDOS/\\\input{${CURSO}-${CONTENIDOS}}/" \
-e 's/RECUPERACIÓN/\\input{trabajo_tmp}/' $PLANTILLA > ${ALUMNO}.tex

# Nueva versión
sed -e "s/ALUMNO/$ALUMNO_OUTPUT/" \
-e "s/CURSO/$CURSO_OUTPUT/" \
-e 's/OBJETIVOS/\\input{objetivos_tmp}/' \
-e 's/CONTENIDOS/\\input{contenidos_tmp}/' \
-e 's/RECUPERACIÓN/\\input{trabajo_tmp}/' $PLANTILLA > ${ALUMNO}.tex

La nueva versión del guión, generar_informes-4, con estas incorporaciones de última hora, queda como sigue:

# 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

# Fichero sed para limpiar etiquetas LaTeX
NOLATEX=limpiar_tex.sed


for linea in $(cat $NOTAS)
do
if [ $(echo $linea | cut -d':' -f3) -lt $NOTA_MINIMA ]
then
# Nombre y curso del alumno tal como constan en el fichero $NOTAS
ALUMNO=$(echo $linea | cut -d':' -f1)
CURSO=$(echo $linea | cut -d':' -f2)

# El nombre del alumno y curso con el formato que tendrán en la salida impresa
ALUMNO_OUTPUT=$(echo $ALUMNO | sed -e 's/-/ /g')
CURSO_OUTPUT=$(echo $CURSO | sed -e 's/\([[:digit:]]*\)-\(.*\)/\1\.º \2/' \
-e 's/ge/EE/' \
-e 's/gm/EP/')

# Ficheros que contienen los objetivos y contenidos del curso
# en que el alumno está matriculado
F_OBJETIVOS=${PROGRAMACION}/${CURSO}-$OBJETIVOS
F_CONTENIDOS=${PROGRAMACION}/${CURSO}-$CONTENIDOS


# Los objetivos y contenidos en un formato apto para la plantilla
# a partir de la que se genera el informe
sed -f $NOLATEX $F_OBJETIVOS > objetivos_tmp.tex
sed -f $NOLATEX $F_CONTENIDOS > contenidos_tmp.tex


# 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{objetivos_tmp}/' \
-e 's/CONTENIDOS/\\input{contenidos_tmp}/' \
-e 's/RECUPERACIÓN/\\input{trabajo_tmp}/' $PLANTILLA > ${ALUMNO}.tex

pdflatex ${ALUMNO}.tex
fi
done

HAL y la burocracia (X - redundancia)

Si recurríamos a los bucles para evitar la repetición innecesaria del mismo código sobre los elementos de una lista de datos, carece de sentido mantener en el interior del bucle construcciones redundantes. Y nuestra última redacción posee un grado de redundancia intolerable:

...
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

Una forma de evitar estas inoperantes y fastidiosas redundancias es convertir el código que se repite en el valor de una variable. Con ello logramos una serie de beneficios claros:

  • El código en cuestión se ejecuta una única vez, en lugar de varias, lo que implica un uso más eficiente de los recursos.

  • La complejidad disminuye, porque donde había una larga tubería habrá ahora un simple nombre de variable.

  • El resultado es más manejable, puesto que si se decide modificar el guión, bastará hacerlo en un solo sitio y no en varios.

  • El resultado es más legible, pues el nombre de la variable, si está bien elegido, será mucho más fácilmente comprensible que la cadena de órdenes por la que está.


Procedamos a empaquetar el código que extrae el primer y segundo campo de cada línea del fichero $NOTAS, nombre de alumno y curso, respectivamente, en una variable adecuada:

# Nombre y curso del alumno tal como constan en el fichero $NOTAS
ALUMNO=$(echo $linea | cut -d':' -f1)
CURSO=$(echo $linea | cut -d':' -f2)

El siguiente paso es incorporar las definiciones de estas variables y modificar las líneas del guión que contenían lo que ahora es el valor de tales variables. Recordemos, por otra parte, que cuando el valor de una variable se ha de manipular dentro de una tubería por un filtro que exige como entrada una cadena de caracteres se lo pasamos como argumento a echo:

for linea in $(cat $NOTAS)
do
if [ $(echo $linea | cut -d':' -f3) -lt $NOTA_MINIMA ]
then
# Nombre y curso del alumno tal como constan en el fichero $NOTAS
ALUMNO=$(echo $linea | cut -d':' -f1)
CURSO=$(echo $linea | cut -d':' -f2)

# El nombre del alumno y curso con el formato que tendrán en la salida impresa
ALUMNO_OUTPUT=$(echo $ALUMNO | sed -e 's/-/ /g')
CURSO_OUTPUT=$(echo $CURSO | 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{${CURSO}-${OBJETIVOS}}/" \
-e "s/CONTENIDOS/\\\input{${CURSO}-${CONTENIDOS}}/" \
-e 's/RECUPERACIÓN/\\input{trabajo_tmp}/' $PLANTILLA > ${ALUMNO}.tex

pdflatex ${ALUMNO}.tex
fi
done

Veamos qué dice diff si guardamos esta nueva versión con el nombre generar_informes-3:

diff -Bbi -u generar_informes-2 generar_informes-3

Su respuesta es la siguiente:

--- generar_informes-2 2009-06-05 15:20:33.000000000 +0200
+++ generar_informes-3 2009-06-05 21:38:40.000000000 +0200
@@ -20,9 +20,13 @@
do
if [ $(echo $linea | cut -d':' -f3) -lt $NOTA_MINIMA ]
then
+ # Nombre y curso del alumno tal como constan en el fichero $NOTAS
+ ALUMNO=$(echo $linea | cut -d':' -f1)
+ CURSO=$(echo $linea | cut -d':' -f2)
+
# 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/' \
+ ALUMNO_OUTPUT=$(echo $ALUMNO | sed -e 's/-/ /g')
+ CURSO_OUTPUT=$(echo $CURSO | sed -e 's/\([[:digit:]]*\)-\(.*\)/\1\.º \2/' \
-e 's/ge/EE/' \
-e 's/gm/EP/')

@@ -32,11 +36,10 @@

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
+ -e "s/OBJETIVOS/\\\input{${CURSO}-${OBJETIVOS}}/" \
+ -e "s/CONTENIDOS/\\\input{${CURSO}-${CONTENIDOS}}/" \
+ -e 's/RECUPERACIÓN/\\input{trabajo_tmp}/' $PLANTILLA > ${ALUMNO}.tex

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


Resumen

  • Por regla general, las redundancias deben ser evitadas en la escritura de guiones o programas.

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.

HAL y la burocracia (VIII - decisiones)

La vida es inconcebible como camino de dirección única. A cada paso se nos abre un sinfín de posibilidades, muchas veces incompatibles, ante las cuales no nos queda más remedio que elegir. Los senderos se bifurcan en el ambiguo jardín de la existencia. Pero nuestras elecciones no son, o no deberían ser, completamente caprichosas. La consideración reflexiva de las opciones presentes en las encrucijadas guían nuestra conducta. Por eso, una lengua que careciese de oraciones condicionales no sería humana o, cuando menos, no sería aplicable más que a un sector insignificante de la realidad vital.

Tampoco a la lengua de HAL le son ajenas las peripecias de la decisión, pues siempre llega el caso en que, dadas ciertas circunstancias, toca desviarse de la senda principal e iniciar una nueva. Por suerte, los "si es" y "si no es" de HAL son mucho más fáciles de aprender que las normas de la consecutio temporum, y las ambigüedades en las alternativas que presentan están terminantemente prohibidas, so pena de naufragar en resultados ininteligibles o simplemente abortados.

Consideremos un ejemplo trivial, para irnos acostumbrando a esta nueva rama de nuestro acervo lingüístico:

Si estoy en casa, di "hola"; si no, di "adiós".

Pongamos cada frase en una línea aparte para más claro entendimiento:

Si estoy en casa
di hola
si no
di adios

Las oraciones imperativas sabemos cómo expresarlas, son el meollo de todo lo que hemos pronunciado hasta ahora:

Si estoy en casa
echo "hola"
si no
echo "adiós"

También podemos construir un equivalente en lengua de HAL para la condición estipulada, la de estar en casa. Decir que estamos en casa es, visto desde la perspectiva de HAL, lo mismo que decir que nuestro directorio actual de trabajo es igual que nuestro directorio home:

Si $(pwd) = $HOME
echo "hola"
si no
echo "adiós"

Es fundamental advertir que el '=' en la frase $(pwd) = $HOME debe estar separado de los términos que compara por un espacio y que es, a pesar de su apariencia, completamente distinto del '=' de una expresión de asignación de valor a una variable (por ejemplo, ESCUDERO='Sancho Panza') donde los espacios de separación están prohibidos. La primera expresión compara dos cadenas de caracteres por su igualdad, mientras que la segunda asigna un valor a una variable.

El resto es el inglés corriente y moliente:

if $(pwd) = $HOME
echo "hola"
else
echo "adiós"

Finalmente, unas pocas normas ortográficas más: la condición va entre corchetes y a un espacio de distancia por delante y por detrás de ellos; tras el "si" (el if) ha de haber un "entonces" (un then); para terminar, la frase entera se cierra graciosamente con un if invertido (fi):

if [ $(pwd) = $HOME ]
then
echo "hola"
else
echo "adiós"
fi

Y, como convención de estilo, un bello espaciado que destaque la diferente función de cada ingrediente en la frase:

if [ $(pwd) = $HOME ]
then
echo "hola"
else
echo "adiós"
fi

El lector puede copiar este fragmento de código, pegarlo en su línea de órdenes y ejecutarlo. Si su directorio actual es su HOME, recibirá un caluroso saludo; en caso contrario, un fría despedida.

La facultad de construir oraciones condicionales abre un campo enorme de nuevas posibilidades para nuestros guiones y para nuestras conversaciones con HAL.

La primera aplicación es que en el guión simplificado que hemos producido estos días ya no tenemos por qué seguir suponiendo artificialmente que la nota de nuestro alumno es suspenso. Dicho de otra forma, podemos solicitar a HAL que realice las operaciones establecidas ---que en la redacción actual realiza en todo caso posible--- sólo si la calificación del alumno que consta en el fichero notas es inferior a la nota mínima necesaria para aprobar.

Construyamos, en primer lugar, la condición:

el campo tercero de la línea que consta en el fichero 'notas' ---el que almacena la calificación--- es menor que la nota mínima

En lengua de HAL:

$(cut -d':' -f3 $NOTAS) -lt $NOTA_MINIMA

El operador -lt, abreviatura de less than (menor que), se utiliza precisamente para este tipo de comparaciones aritméticas.

La frase condicional entera sería:

if [ $(cut -d':' -f3 $NOTAS) -lt $NOTA_MINIMA ]
then
ÓRDENES
fi

Como no necesitamos realizar ninguna operación en el caso de que la nota sea superior a la mínima, no es necesario crear una cláusula else para nuestra frase.

El guión completo con la condición añadida quedaría ahora como sigue:

# 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

if [ $(cut -d':' -f3 $NOTAS) -lt $NOTA_MINIMA ]
then

# El nombre del alumno y curso con el formato que tendrán en la salida impresa
ALUMNO_OUTPUT=$(cut -d':' -f1 $NOTAS | sed -e 's/-/ /g')
CURSO_OUTPUT=$(cut -d':' -f2 $NOTAS | 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{1-ge-objetivos}/' \
-e 's/CONTENIDOS/\\input{1-ge-minimos}/' \
-e 's/RECUPERACIÓN/\\input{trabajo_tmp}/' $PLANTILLA > Sancho-Panza.tex

pdflatex Sancho-Panza.tex
fi

Si lo ponemos a prueba, no habrá ningún cambio en su comportamiento. El informe Sancho-Panza.pdf se generará idéntico al del día pasado. Pero si cambiamos la nota de Sancho y le damos un aprobado, el guión terminará en silencio sin haber realizado ninguna acción, aparte de la asignación temporal de los valores de las variables, cosa que carece de efectos visibles para nosotros.

Resumen

  • La lengua de HAL dispone de la posibilidad de construir frases condicionales mediante la sintaxis:
    if [ condición ] 
    then
    acciones si la condición se cumple
    else
    acciones si la condición no se cumple
    fi

  • El operador '=' sirve para asignar valores a variables (ejemplo: VAR=val) o, inserto en expresiones condicionales, para comparar por su identidad cadenas de caracteres (ejemplo: cadena1 = cadena2).

  • El operador '-lt' dentro de expresiones condicionales sirve para verificar si un valor aritmético es menor que otro.

miércoles, 3 de junio de 2009

HAL y la burocracia (VII - interactividad)

En un remoto pasado de nuestras conversaciones, enredados en las peculiaridades digestivas de HAL, jugábamos a procurar el alimento de órdenes como grep o cut por la vía directa de lo que llamamos entonces la cañería principal ---el standard input (STDIN), para los redichos--- conectada por defecto con nuestro terminal o consola. En aquel momento se trataba sólo de un curioso experimento preparatorio para comprender el significado y función de las imprescindibles tuberías. El pasatiempo de aquellos primeros contactos con las maravillas de la anatomía de HAL nos vendrá ahora de perlas para añadir interactividad a nuestro guión.

Volvamos a reproducir el experimento, pero ahora con otra orden capaz igualmente de alimentarse a través de la entrada estándar, la orden cat (no confundir con cut). cat es una abreviatura de concatenate y su función inicial es la de concatenar los ficheros que se le den como argumentos e imprimirlos línea a línea en pantalla. Si se le da un único fichero lo imprime tal cual. ¿Pero, qué sucede si no le damos ningún fichero como argumento? Probémoslo:

cat

Tras pulsar 'Enter', nos encontramos en una situación ya familiar, la de poder emitir líneas de texto en la entrada estándar para que cat haga lo que habitualmente hace con las entradas que se le proporcionan, devolverlas tal cual a la salida estándar, que por defecto también está conectada con nuestro terminal. El juego continúa sin fin hasta que solicitamos salir de él con Ctrl+D:

hola
hola
cat
cat
Ctrl+D

Hasta aquí nada nuevo. cat se comporta de una manera bastante estúpida ---pensaría alguno---, se limita a reproducir cada línea introducida, como si se tratase de un echo interactivo.

Parece que algo tan tonto no puede dar mucho de sí. Por el contrario, la utilidad de cat para nuestro propósito se desvela si añadimos a la orden una redirección:

cat >conversacion

Lo único que hace este añadido es redirigir la salida al fichero conversacion. Por tanto, si reproducimos la anterior sesión interactiva, donde la salida de cat ya no la veremos en pantalla ---porque se redirige a ese fichero--- estaremos copiando en él tal cual las líneas que hayamos escrito antes de pulsar Ctrl+D.

Inmediatamente se comprende la utilidad que este comportamiento de cat puede tener para nuestro guión.

Habíamos dejado pendiente la sección correspondiente a las propuestas de actividades que el alumno debe realizar para superar los objetivos y contenidos. Puesto que estas propuestas ---vamos a suponer---, no pueden, en principio, establecerse de antemano, sino que tienen que redactarse de modo expreso para cada alumno suspenso, proporcionar al guión por medio de cat un acceso a la entrada estándar, nos permitirá realizar la redacción de tales propuestas antes de que las sustituciones en la plantilla informe_plantilla.tex tengan efecto.

Esto nos lleva a modificar el guión obtenido el día pasado del modo siguiente [en rojo las nuevas 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

# El nombre del alumno y curso con el formato que tendrán en la salida impresa
ALUMNO_OUTPUT=$(cut -d':' -f1 $NOTAS | sed -e 's/-/ /g')
CURSO_OUTPUT=$(cut -d':' -f2 $NOTAS | 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{1-ge-objetivos}/' \
-e 's/CONTENIDOS/\\input{1-ge-minimos}/' \
-e 's/RECUPERACIÓN/\\input{trabajo_tmp}/' $PLANTILLA > Sancho-Panza.tex

pdflatex Sancho-Panza.tex

La instrucción echo produce un mensaje para el usuario, mientras que la instrucción cat abre la entrada estándar para que el usuario introduzca los datos que se almacenarán en el fichero trabajo_tmp.tex. Este fichero es luego incluido en el fichero Sancho_Panza.tex por obra de la última expresión del sed final.

Se habrá advertido que el usuario debe introducir las propuestas precedidas de '\item', puesto que el entorno LaTeX del que acabarán formando parte es un entorno lista. Esta última complicación se puede evitar fácilmente de forma que al usuario le baste introducir cada propuesta como un línea única (sin el \item). En tal caso, el \item debería añadirlo el propio guión, justo después del cat, con una instrucción como ésta:

sed -i 's/^\(.*\)/\\item \1/' trabajo_tmp.tex

Que la versión presentada en el primer capítulo de esta serie no contenga la simplificación aludida aceleró unos segundos la redacción del guión, aunque a expensas de complicar en parte la interfaz del usuario. Es lo que tiene el modo de obrar "rápido y sucio".

Queda, finalmente, añadir la sección relativa a las propuestas en nuestra plantilla:

...
\begin{document}
\begin{datosAlumno}
Alumno: ALUMNO & CURSO
\end{datosAlumno}
\begin{Objetivos}
OBJETIVOS
\end{Objetivos}
\begin{Contenidos}
CONTENIDOS
\end{Contenidos}
\begin{Propuestas}
RECUPERACIÓN
\end{Propuestas}

\end{document}

A estas alturas, el lector no debería hallar dificultad alguna en realizar por su cuenta las modificaciones que convengan y volver a poner a prueba el guión mejorado.

Resumen

  • La orden cat concatena los ficheros que se le proporcionan como argumentos y los imprime en la salida estándar, por defecto, la pantalla. Si no recibe argumentos toma su entrada de la entrada estándar.

  • Una de las aplicaciones comunes de la orden cat es permitir la creación interactiva de un fichero.

martes, 2 de junio de 2009

HAL y la burocracia (VI)

Antes de dar un paso más en la elaboración de nuestro guión, conviene que el lector lo ponga a prueba, si no lo ha hecho ya por cuenta propia.

Creamos, para empezar un directorio informe_suspensos bajo nuestro subdirectorio de guiones:

mkdir ~/guiones/informe_suspensos

Conviene recordar los ficheros recién creados para nuestra prueba [en caso de que el lector no los haya creado todavía, deberá hacerlo ahora con cualquier editor de texto plano]:

  • informe_plantilla.tex (la plantilla para generar el fichero .tex por alumno [Añado un preámbulo adecuado, modifico ligeramente la sección correspondiente a los datos del alumno y suprimo la sección correspondiente a las propuestas, que ha quedado pendiente, como se recordará]):

    \usepackage[spanish]{babel}
    \usepackage[T1]{fontenc}
    \usepackage[utf8]{inputenc}
    \usepackage{bera}
    \usepackage[usenames,dvipsnames]{color}
    \usepackage{framed}
    \usepackage{enumitem}

    \newenvironment{datosAlumno}%
    {\begin{framed}%
    \begin{center}%
    \begin{tabular*}{\textwidth}%
    {@{\extracolsep{\fill}}lr}}%
    {\end{tabular*}\end{center}\end{framed}}

    \newenvironment{Objetivos}%
    {\begin{framed}%
    \section*{Objetivos}%
    \end{framed}%
    \begin{framed}%
    \begin{itemize}[label=\fbox{\textcolor{white}{X}}]}%
    {\end{itemize}\end{framed}}

    \newenvironment{Contenidos}%
    {\begin{framed}%
    \section*{Contenidos}%
    \end{framed}%
    \begin{framed}%
    \begin{itemize}[label=\fbox{\textcolor{white}{X}}]}%
    {\end{itemize}\end{framed}}

    \newenvironment{Propuestas}%
    {\begin{framed}%
    \section*{Propuestas}%
    \end{framed}%
    \begin{framed}%
    \begin{itemize}}%
    {\end{itemize}\end{framed}}

    \begin{document}
    \begin{datosAlumno}
    Alumno: ALUMNO & CURSO
    \end{datosAlumno}
    \begin{Objetivos}
    OBJETIVOS
    \end{Objetivos}
    \begin{Contenidos}
    CONTENIDOS
    \end{Contenidos}

  • 1-ge-objetivos.tex (el fichero con los objetivos del curso 1-ge):

    \item Reconocer la diferencia entre los refranes y el buen decir.
    \item Adquirir el hábito del buen decir.
    \item Refrenar el hábito malsano del refraneo sin ton ni son.

  • 1-ge-minimos.tex: (el fichero con los contenidos mínimos del curso 1-ge):

    \item El refrán y sus muchos peligros.
    \item El decir del caballero versus el decir del bufón.
    \item Síntomas alarmantes de la verborrea refranil.

  • notas: (el fichero con las notas):

    Sancho-Panza:1-ge:3

  • generar_informes: (el guión propiamente dicho [Unimos lo que obtuvimos en el análisis de datos y en el esbozo del modelo simple, con las comillas corregidas]):

    # 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

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


    sed -e "s/ALUMNO/$ALUMNO_OUTPUT/" \
    -e "s/CURSO/$CURSO_OUTPUT/" \
    -e 's/OBJETIVOS/\\input{1-ge-objetivos}/' \
    -e 's/CONTENIDOS/\\input{1-ge-minimos}/' $PLANTILLA > Sancho-Panza.tex

    pdflatex Sancho-Panza.tex

(Nótese que en este último fichero hemos modificado la variable PROGRAMACIÓN para que tenga en cuenta la nueva ubicación de nuestros ficheros de objetivos y mínimos, aunque no la vayamos a usar en las órdenes que hemos producido hasta ahora. También hemos añadido una línea al final, para que, al ejecutarse el guión, pdflatex procese el fichero .tex resultante de la orden sed que le precede.)

Todos estos ficheros deben estar ahora bajo el subdirectorio $HOME/guiones/informe_suspensos

Queda cambiar el modo del guión:

chmod u+x informe_suspensos

y ejecutarlo:

./informe_suspensos

El resultado, aparte de otros ficheros temporales generados por pdflatex, es el fichero Sancho-Panza.pdf, que, en un lector de pdfs como okular, se muestra con el siguiente aspecto:

HAL y la burocracia (V - las comillas)

Como advertíamos ayer, por los pequeños detalles merodea el diabólico encantamiento. Para desenmascararlo, vamos a realizar unos pocos experimentos.

En primer lugar, crearemos un variable y le daremos como valor una cadena:

ESCUDERO='Sancho Panza'

Nótese que las comillas son aquí necesarias. De no haberlas puesto, el espacio, un carácter especial en la lengua de HAL (el delimitador que sirve, por ejemplo, para determinar donde termina una orden y empiezan sus argumentos) hubiese sido interpretado por HAL como un separador. Más exactamente, si hubiésemos escrito:

ESCUDERO=Sancho Panza

HAL hubiese creado la variable ESCUDERO con el valor Sancho y hubiese tratado de ejecutar la inexistente orden Panza, con el consabido resultado de command not found. Ni siquiera la variable ESCUDERO hubiese retenido su valor, Sancho, porque, como sabemos desde hace mucho, la definición de variables en la misma línea y justo delante de una orden sólo afecta a la ejecución de esa orden.

Por tanto, las comillas en la anterior definición tienen una función importantísima, permitir que el espacio se interprete literalmente y no como un signo con un valor sintáctico especial. Lo que, en ese ejemplo concreto, implica que el valor de la variable ESCUDERO será Sancho Panza (espacio incluido), como podemos comprobar con un simple:

echo $ESCUDERO

que devuelve:

Sancho Panza

¿Hubiese sido diferente utilizar dobles comillas en lugar de comillas simples?

Lo probamos:

CABALLERO="Don Quijote"
echo $CABALLERO

Que devuelve, de forma semejante:

Don Quijote

Parece, pues, que no hay ninguna diferencia entre las comillas simples y las dobles comillas, que es cosa más de gusto que de gramática. Pero eso, justamente, es lo que quieren que creamos los pérfidos encantadores. De hecho, nosotros hemos utilizado hasta ahora ambos tipos de comillas sin mayor cuestionamiento. Pero las apariencias engañan. Veámoslo:

echo 'El compañero de $CABALLERO es $ESCUDERO'

produce:

El compañero de $CABALLERO es $ESCUDERO

Es algo que deberíamos haber esperado. Las comillas preservan también aquí el significado literal del carácter '$' y evitan que se interprete como un signo sintáctico especial, a saber, el de proporcionar el valor de la variable que le sigue.

Sin embargo,

echo "El compañero de $CABALLERO es $ESCUDERO"

devuelve, sorprendentemente:

El compañero de Don Quijote es Sancho Panza

La conclusión de nuestro experimento parece obvia, las comillas simples son rejas más seguras que las dobles. En general, de las comillas simples ningún carácter se evade de ser interpretado literalmente, mientras que hay caracteres especiales que no se doblegarán a las comillas dobles y seguirán exigiendo ser interpretados como tales signos sintácticos especiales. Entre estos pocos signos que han conseguido la prevalencia de su característica identidad sintáctica está nuestro querido '$'.

Lo que tan interesante conocimiento implica para el código propuesto al final del día pasado es que pone a las claras el origen de su error. Recordemos cuál era el código:

sed -e 's/ALUMNO/$ALUMNO_OUTPUT/' \
-e 's/CURSO/$CURSO_OUTPUT/' \
-e 's/OBJETIVOS/\\input{1-ge-objetivos}/' \
-e 's/CONTENIDOS/\\input{1-ge-minimos}/' $PLANTILLA > Sancho-Panza.tex

Las dos primeras expresiones de la orden sed contienen el signo especial '$'. Ahora bien, la expansión del valor de las variables ALUMNO_OUTPUT y CURSO_OUTPUT no se producirá, puesto que '$' se interpretará literalmente. Acabaremos con un fichero Sancho-Panza.tex con las líneas:

\begin{datosAlumno}
Alumno: $ALUMNO_OUTPUT
Curso: $CURSO_OUTPUT
\end{datosAlumno}

La solución, tras lo aprendido hoy, es bien simple, utilizar dobles comillas en las líneas díscolas:

sed -e "s/ALUMNO/$ALUMNO_OUTPUT/" \
-e "s/CURSO/$CURSO_OUTPUT/" \
-e 's/OBJETIVOS/\\input{1-ge-objetivos}/' \
-e 's/CONTENIDOS/\\input{1-ge-minimos}/' $PLANTILLA > Sancho-Panza.tex

Un lector espabilado podría imaginar una solución incluso más sencilla. Ya que las comillas esconden tantas triquiñuelas, ¿por qué no suprimirlas directamente?:

sed -e s/ALUMNO/$ALUMNO_OUTPUT/ \
-e s/CURSO/$CURSO_OUTPUT/ \
...

Desgraciadamente, no funcionaría, aunque sí funcionaría la siguiente solución:

sed -e s/ALUMNO/"$ALUMNO_OUTPUT"/ \
-e s/CURSO/"$CURSO_OUTPUT"/ \
...

Dejamos como ejercicio para el lector la experimentación y comprensión de las causas del fracaso y el éxito de las dos últimas propuestas.

Resumen

  • Las comillas simples permiten que todos los caracteres especiales de la lengua de HAL encerrados en ellas se interpreten literalmente.

  • Las comillas dobles hacen que casi todos los caracteres especiales de la lengua de HAL se interpreten literalmente, salvo algunos pocos (en especial, el carácter '$'), que seguirán manteniendo su peculiar función sintáctica.

domingo, 31 de mayo de 2009

HAL y la burocracia (IV)

Nuestro siguiente paso es imaginar un proceso que, indefectiblemente, nos lleve desde los ficheros de entrada descritos ayer a los informes en pdf requeridos. No es nada de orden diferente a lo que hemos venido haciendo desde el principio de estas conversaciones. En realidad, se trata siempre de lo mismo: pensar en las transformaciones que deben sufrir ciertos datos de entrada para que de ello surja una determinada salida y aplicar las herramientas que HAL pone a nuestra disposición para efectuar dichas transformaciones.

Podemos, pues, como primer acercamiento, aplicar sin miedo el principio explicado hace tiempo: reducir el problema a un caso muy simple y crear un modelo de solución para ese caso.

Asumamos que, en lugar de diferentes informes para múltiples alumnos, queremos generar un único informe para un único alumno ---que sabemos suspenso de antemano. Dicho de otra forma, supongamos que nuestro fichero notas contiene esta única entrada:

Sancho-Panza:1-ge:3

Supongamos, además, que los únicos ficheros de objetivos y contenidos mínimos con los que contamos son ---¡bendita la suerte!--- los de 1-ge, es decir:

1-ge-objetivos.tex
1-ge-contenidos.tex

Supongamos, finalmente ---y para hacer más divertido el ejemplo--- que el contenido de tales ficheros es el siguiente:

  • Contenido de 1-ge-objetivos.tex

    \item Reconocer la diferencia entre los refranes y el buen decir.
    \item Adquirir el hábito del buen decir.
    \item Refrenar el hábito malsano del refraneo sin ton ni son.

  • Contenido de 1-ge-minimos.tex

    \item El refrán y sus muchos peligros.
    \item El decir del caballero versus el decir del bufón.
    \item Síntomas alarmantes de la verborrea refranil.

Para generar con pdflatex un pdf como el que se muestra en la imagen propuesta en el primer artículo de esta serie ---actualizado con los objetivos y contenidos que acabamos de presentar---, deberíamos disponer de un fichero .tex, con un aspecto semejante al siguiente [El preámbulo se omite, y se presupone, sin mayor explicación, que los entornos Objetivos, Contenidos y Propuestas han sido definidos en dicho preámbulo como tipos especiales de listas. (Ver este artículo para refrescar la memoria sobre los entornos y listas en LaTeX)]:

\begin{document}
\begin{datosAlumno}
Alumno: Sancho Panza
Curso: 1.º EE
\end{datosAlumno}

\begin{Objetivos}
\input{1-ge-objetivos}
\end{Objetivos}

\begin{Contenidos}
\input{1-ge-minimos}
\end{Contenidos}

\begin{Propuestas}
???
\end{Propuestas}
\end{document}

Compárese con la plantilla redactada el día pasado:

\begin{document}
\begin{datosAlumno}
Alumno: ALUMNO
Curso: CURSO
\end{datosAlumno}

\begin{Objetivos}
OBJETIVOS
\end{Objetivos}

\begin{Contenidos}
CONTENIDOS
\end{Contenidos}

\begin{Propuestas}
RECUPERACIÓN
\end{Propuestas}
\end{document}

Resulta evidente, que salvo para el caso de las propuestas de recuperación [en púrpura], que discutiremos más adelante, nuestro trabajo, esto es, el trabajo que HAL debería realizar para obtener el fichero .tex del informe de Sancho Panza, digamos, Sancho-Panza.tex, es simplemente sustituir lo que en la plantilla aparece en rojo por lo que, en el mismo color, debe aparecer en el citado informe.

Representemos en lengua de HAL las sustituciones deseadas:

sed 's/ALUMNO/Sancho Panza/' informe_plantilla.tex > Sancho-Panza.tex
sed 's/CURSO/1.º EE/' informe_plantilla.tex > Sancho-Panza.tex
sed 's/OBJETIVOS/\\input{1-ge-objetivos}/' informe_plantilla.tex > Sancho-Panza.tex
sed 's/CONTENIDOS/\\input{1-ge-minimos}/' informe_plantilla.tex > Sancho-Panza.tex

Si utilizamos la opción -e de sed podemos enviar varias órdenes, una tras otra, a sed y reducir la redundancia en las líneas anteriores. Podemos, además, referirnos al fichero que contiene la plantilla a través de la variable PLANTILLA que con ese propósito creamos el día pasado:

sed -e 's/ALUMNO/Sancho Panza/' \
-e 's/CURSO/1.º EE/' \
-e 's/OBJETIVOS/\\input{1-ge-objetivos}/' \
-e 's/CONTENIDOS/\\input{1-ge-minimos}/' $PLANTILLA > Sancho-Panza.tex

Mientras que las dos últimas ordenes de sustitución de la instrucción anterior son válidas tal cual, las dos primeras exigen un tratamiento especial. Las cadenas de caracteres "Sancho Panza" y "1.º EE" no forman parte de nuestros datos de entrada. Debemos, pues, obtenerlas a partir de ellos mediante alguna clase de manipulación. El nombre y curso del alumno, como ya hemos comentado, forman parte del contenido del fichero notas, concretamente, son su primer y segundo campo, respectivamente, donde cada campo está separado por el carácter ':'. Para una operación de extracción de este tipo podemos utilizar, como sabemos, una orden como cut. Así, el campo del nombre se obtendría mediante la orden:

cut -d':' -f1 notas

O, aplicando la variable NOTAS, tal como hemos hecho con la variable PLANTILLA en la instrucción sed anterior:

cut -d':' -f1 $NOTAS

Mutatis mutandis, la siguiente orden extrae del fichero notas el campo relativo al curso:

cut -d':' -f2 $NOTAS

Ahora bien, el contenido de estos campos no es exactamente el que buscamos. El contenido es, respectivamente, Sancho-Panza y 1-ge, pero en el informe deseamos que, en lugar del guión, sea un espacio el que separe nombre y apellido del alumno y que, en lugar de 1-ge aparezca la expresión 1º. EE como identificador del curso ('EE' y 'EP' están por enseñanzas elementales y profesionales, nueva denominación oficial de lo que hace pocos años se denominaban grado elemental y medio, para inri del hacedor de guiones).

sed puede ayudarnos de nuevo a la hora de efectuar las conversiones aludidas.

's/-/ /g'

sustituiría los guiones por espacios en la línea de entrada.

Por su parte la más compleja serie de órdenes

's/\([[:digit:]]*\)-\(.*\)/\1\.º \2/'
's/ge/EE/'
's/gm/EP/'

convertiría 1-ge en 1.º EE [La tercera sustitución, que convierte gm en EP no es necesaria en el caso simple actual, pero la incluimos ya desde ahora mismo con el fin de evitar referirnos a un proceso tan elemental de sustitución en explicaciones posteriores].

Las dos últimas órdenes de esta operación de conversión son obvias, pero la primera puede resultar algo difícil de entender a simple vista. Valga su explicación de repaso de algunas minucias de sed.

Cualquier serie de números que aparezca en la línea de entrada encajará con la expresión '[[:digit:]]*' y cualquier serie de caracteres con la expresión '.*'. Por tanto, el molde '[[:digit:]]*-.*' nos permitirá "cazar" cualquier serie de caracteres constituida por cualquier sucesión de números, seguida de guión y seguido éste último por cualesquiera otros caracteres. En consecuencia, algo como nuestra cadena "1-ge" encajará con ese molde. Puesto que, en la sustitución, queremos reutilizar, por un lado, el número inicial y, por otro, lo que sigue al guión para su posterior conversión mediante las órdenes de sed subsiguientes, guardamos lo que encaje con ambas expresiones en registros temporales, esto es, encerramos entre paréntesis convenientemente escapados los correspondientes moldes, y aplicamos, luego, en el patrón de sustitución, las construcciones especiales \1 y \2 para recuperar lo guardado en dichos registros. El resto es también trivial, el guión acaba sustituido por ".º ", donde el punto debe escaparse, al tratarse de una carácter especial de sed.

Si reunimos todas las piezas, los cut que extraen el nombre y curso del alumno, tal como aparecen en el fichero notas, y los sed que convertirían tales datos al formato que tendrán en el fichero Sancho-Panza.tex de salida, obtenemos estas dos órdenes complejas:

cut -d':' -f1 $NOTAS | sed -e 's/-/ /g'

que devolverá:

Sancho Panza

y la más engorrosa:

cut -d':' -f2 $NOTAS | sed -e 's/\([[:digit:]]*\)-\(.*\)/\1\.º \2/' \
-e 's/ge/EE/' \
-e 's/gm/EP/'

que devolverá:

1.º EE

El paso final está a un tiro de piedra. El resultado de estas dos órdenes debe aparecer como segundo miembro en las instrucciones sed de sustitución propuestas al principio. Este resultado se puede obtener mediante el recurso sintáctico de la sustitución de órdenes. O sea, que nuestro esbozo:

sed -e 's/ALUMNO/Sancho Panza/' \
-e 's/CURSO/1.º EE/' \
-e 's/OBJETIVOS/\\input{1-ge-objetivos}/' \
-e 's/CONTENIDOS/\\input{1-ge-minimos}/' $PLANTILLA > Sancho-Panza.tex

debe completarse como es debido de la forma siguiente:

sed -e 's/ALUMNO/$(cut -d':' -f1 $NOTAS | sed -e 's/-/ /g')/' \
-e 's/CURSO/$(cut -d':' -f2 $NOTAS | sed -e 's/\([[:digit:]]*\)-\(.*\)/\1\.º \2/' \
-e 's/ge/EE/' \
-e 's/gm/EP/')
/' \
-e 's/OBJETIVOS/\\input{1-ge-objetivos}/' \
-e 's/CURSO/\\input{1-ge-minimos}/' $PLANTILLA > Sancho-Panza.tex

¡Buf! algo difícil de digerir de una tacada. Conviene reducir la complejidad a la vista mediante el uso de variables cuyos valores sean las sustituciones de órdenes que acabamos de encajar con calzador:

ALUMNO_OUTPUT=$(cut -d':' -f1 $NOTAS | sed -e 's/-/ /g')
CURSO_OUTPUT=$(cut -d':' -f2 $NOTAS | sed -e 's/\([[:digit:]]*\)-\(.*\)/\1\.º \2/' \
-e 's/ge/EE/' \
-e 's/gm/EP/')

Lo que permite obtener esta expresión final algo más digerible:

sed -e 's/ALUMNO/$ALUMNO_OUTPUT/' \
-e 's/CURSO/$CURSO_OUTPUT/' \
-e 's/OBJETIVOS/\\input{1-ge-objetivos}/' \
-e 's/CONTENIDOS/\\input{1-ge-minimos}/' $PLANTILLA > Sancho-Panza.tex

No estáría mal para un entremés, Sancho amigo, si no fuera porque contiene un grave error. Y es que no hay que descuidarse de los encantamientos que, agazapados en nuestros más inocuos utensilios de cocina, pueden amargar hasta la olla mejor cocinada.