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 :-)

miércoles, 2 de septiembre de 2009

Stroustrup y la enseñanza de la programación ...

... o la ciencia de la vida.

Sí, tal rimbombante extensión del título de esta entrada ha pasado por mi cabeza, aunque la he omitido no tanto por pretenciosa, que lo es, cuanto por evitar una cabecera demasiado larga para Blogger.

¿A qué viene este embrollo? La cosa es que durante algunos días de este verano y, aprovechando, cómo no, el tan preciado descanso, me ha dado por hincar ligeramente el diente al nuevo libro del maestro Bjarne Stroustrup. Para quien no sepa quién es Stroustrup, baste decir que no sólo es el creador del lenguaje de programación C++, sino seguramente una de las personalidades más valoradas del mundillo de la informática.

Pues bien, resulta que Stroustrup ha sacado tiempo, entre sus muchas dedicaciones y responsabilidades de experto, para escribir nada menos que una introducción a la programación, un buen tocho de más de 1000 páginas titulado Programming -- Principles and Practices using C++. No he leído más que la primera parte, algo más de unas 300 páginas espléndidas en más de un sentido.

No es mi propósito hacer una reseña del libro ---por lo demás, imposible, dado que sólo he leído un fragmento---, ni siquiera una reseña de esta primera parte, que ya de por sí la merecería. Lo que, más bien, me interesa es reflexionar sobre mi experiencia al topar con sus capítulos sexto y séptimo (especialmente, el sexto).

Téngase en cuenta que mi punto de vista no es sólo el del entusiasta en estos asuntos técnicos, ni siquiera el del propenso a encontrar placer en la didáctica de la programación, sino más bien el de quien enseña algo, si bien de una índole en apariencia muy diferente. Porque, al cabo, el problema del aprendizaje va mucho más allá del objeto de la enseñanza, o sea, de la materia que se imparte, y se concentra, en particular ---si se me permite el juego de palabras, en su objetivo; y éste, cuando se entiende en su mayor generalidad, es común probablemente, a todas las formas de aprendizaje.

Pues bien, los capítulos en cuestión enseñan cosas como:

  • Diseño de una gramática abstracta para interpretar el input que procesará un programa de cálculo aritmético (una calculadora).

  • Implementación de esa misma gramática a través de funciones recursivas, mutuamente relacionadas.

  • Creación de tipos en C++ y manipulación del input a través de un tipo stream creado por el usuario.

  • etc.



Sorprende, ciertamente, que un libro de introducción ponga sobre la mesa del aprendiz una tarea de esta complejidad a unas pocas páginas de distancia de su presupuesta total ignorancia sobre la programación. Es evidente, sin duda, que Stroustrup trata a sus alumnos como adultos capaces de asumir retos difíciles desde el primer momento.

Pero lo verdaderamente sorprendente es la forma como Stroustrup plantea el ataque a este problema de programación, el diseño e implementación de una calculadora. Muchos se quedarán en la superficie y verán tan sólo un caso más ---no muy frecuente, por cierto, en textos introductorios--- de la práctica de un diseño e implementación incrementales. Pero la cosa, a mi modo de ver va más allá.

Hay algo en esas páginas que trasciende la cuestión relativa a conceptos y técnicas de programación. Dicho en pocas palabras, lo que Stroustrup una y otra vez trata de hacernos comprender es que el verdadero meollo de la programación no es otra cosa que el de pensar a fondo sobre un problema, el de acercarse, mientras hacemos el camino, a la naturaleza misma del problema inicial, cuyo sentido y complejidad se va haciendo cada vez más presente ---aunque, quizá, me atrevería a decir, nunca completa y enteramente presente---, gracias a nuestros ingenuos errores iniciales, a nuestros pasos en falso y a toda esa serie de tentativas aparentemente infructuosas y, sin embargo, imprescindibles, de las que consta todo proceso de investigación lanzado hacia lo nuevo, hacia lo desconocido.

La excelencia del maestro se mide no por la cantidad o, incluso, calidad de las cosas que enseña ---que también---, sino sobre todo porque nos hace comprender que el aprendizaje no termina nunca y que, nosotros, por muy arriba que estemos en nuestra destreza y conocimientos, somos esencialmente aprendices a la hora de enfrentarnos a nuevos retos ---los únicos que al cabo interesan---, esencialmente falibles, incorrectos, tentativos.

No es diferente lo que los grandes maestros han venido enseñando desde los albores de nuestra civilización. Piénsese, por ejemplo, en el método platónico-socrático del elenchos y la dialéctica; piénsese en el proceso de aprendizaje de cualquier artesanía compleja.

... Y piénsese ---por qué no--- en las propias cuestiones de la vida, las que nos azoran de continuo, desconcertantes, irresueltas y, a un tiempo y por ello mismo, fascinantes.

Mucho se gana ---diría, incluso, que todo se gana--- cuando se comprende que el experto no es nunca el infalible, sino el que asume con plena conciencia hasta qué punto el camino no es, ni debe ser, rectilíneo, sino más bien sinuoso, espiral incluso; y que, lejos de que ello suspenda el entusiasmo y el juicio, nos ata a la aventura del descubrimiento con la misma poderosa fuerza que el aprendiz siente en sus primeras andaduras.

Gracias, maestro Stroustrup, por recordarlo, incluso allí donde el lego quizá esperase, basado en peligrosos lugares comunes, justamente lo contrario.

miércoles, 1 de julio de 2009

Ocultamientos y desvelamientos

Hoy he tenido que realizar una estúpida tarea que me ha llevado más tiempo del debido. La causa: la peligrosa simplificación de la realidad en aras de la facilidad y en perjuicio de la difícil sencillez de la verdad.

El motivo ha sido el siguiente. Una amiga me ha pedido que le eche una mano con su iPod. El problema es que era incapaz de transferir las pistas desde su ordenador (con Linux) al iPod. Un problema no infrecuente a lo que parece, basta echar a vistazo a google.

He probado con el, por otra parte, estupendo Amarok. Amarok se limitaba a informar de que la transferencia no podía realizarse porque el sistema de ficheros del iPod estaba montado en modo de solo lectura. Nada se podía escribir en él. Lo primero, y casi lo único, que se tiende a pensar en una caso como éste es que hay algún problema con los permisos. Pero no había ningún problema ahí.

Por suerte, se me ocurrió, después de otras investigaciones infructuosas, echar un vistazo a los mensajes del kernel tal como se generaban al montar el dispositivo:

dmesg | tail -f

La verdad era diáfana y simple:

hfs: Filesystem was not cleanly unmounted, running fsck.hfsplus is recommended. mounting read-only.

Mi amiga, a buen seguro, había desenchufado el iPod a lo bruto, cosa a la que seguramente ni siquiera había prestado atención. Y, claro, el sistema de ficheros estaba tan sucio que el bueno del kernel se negaba a dar permiso de escritura.

Nótese hasta qué punto el mensaje del kernel es significativo. No sólo presenta sin ambigüedad posible la razón de su comportamiento, sino que propone una solución: chequear el sistema de ficheros con la herramienta adecuada.

No necesité otra cosa que instalar esa herramienta:

aptitude install hfsprogs

y ejecutar fsck.hfsplus sobre cada una de las particiones del dispositivo en cuestión (/dev/sde). La tercera partición /dev/sde3 era la corrupta. En pocos segundos, se resolvió el problema que nos había tenido entretenidos más de la cuenta y pudimos realizar la transferencia de ficheros.

Moraleja: No hay que fiarse totalmente de las aplicaciones gráficas. En su afán de evitar mensajes crípticos, pueden ocultar datos decisivos de la realidad.

Moraleja2: Si es necesario el proceso descrito para resolver un problema tan simple y, probablemente, frecuente, ¿no es posible pensar que Linux (o, quizá, todo sistema complejo) no es todavía lo suficientemente amigable como para que lo pueda usar mi tía María?

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.