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