domingo, 1 de febrero de 2009

HAL y awk

Tras nuestras conversaciones anteriores hemos obtenido una imagen sintética, pero relativamente completa, de las características esenciales de la lengua de HAL. En especial, el conocimiento de las tuberías y de la sustitución de órdenes abren infinitas posibilidades en la experimentación de una comunicación compleja. Hemos dejado de considerar, eso sí, aspectos en muchos sentidos más sencillos que los tratados últimamente, pero que nos desviaban del objetivo básico, a saber, adquirir cuanto antes una visión panorámica de la mayor parte de las construcciones lingüísticas fundamentales. Sin duda, habrá que ir entrando en ellos cuando llegue la ocasión, pues no en vano son el punto de partida de muchos manuales sobre esta lengua fabulosa. Quizá entonces el lector se sentirá aliviado al reencontrarse con el HAL de los primeros capítulos, un HAL elemental y relativamente próximo a nuestra forma de hablar.

Antes de ese reposo en la vida simple de las órdenes elementales, no se puede dejar sin culminar este primer ascenso a la cima de la lengua, sin pisar, aunque sólo sea de puntillas, el terreno quizá más resbaladizo, por engorroso, que vamos a atravesar en nuestra aventura: el mundo de las expresiones regulares, auténticos trabalenguas, engendros tremendos y, a un tiempo, increíblemente eficaces, que hacen las delicias de los amigos más radicales y atrabiliarios de HAL.

Hay que insistir en que nuestro encuentro con esta clase de seres abominables tiene que ser fugaz a la fuerza. Detenernos aquí y tratar de penetrar a fondo en su potencia real y en su compleja anatomía nos llevaría a perdernos en un laberinto insondable del que difícilmente llegaríamos a salir indemnes. Hasta el autor ha tenido que ser sometido a un arduo proceso de autohipnósis para que accediese a mostrarnos estos lugares endiablados. Por suerte, acabó convencido de que el paso por la aparente fealdad del submundo era necesario para emprender sin miedo futuras expediciones.

Planteemos, como de costumbre, una tarea.

El día pasado descubrimos que dict no sólo permite consultar en varios diccionarios bilingües, sino también en diccionarios como foldoc, un diccionario estupendo sobre computación que contiene unos 13.000 artículos. [Si el lector quiere saber más sobre foldoc se lo puede preguntar a HAL con: dict -i foldoc].

Entre los diccionarios clásicos de los amigos de HAL uno de los más emblemáticos es el Jargon File, el compendio por excelencia sobre la jerga hacker. dict nos permite acceder a él por medio de nuestra querida consola. Veamos un ejemplo:

dict -d jargon hacker

Se puede obtener el primer párrafo, y formatearlo con un anchura menor de la original para que quepa holgadamente en pantalla, con una orden como ésta [que no me detengo a explicar ;-)]:

sed 's/^[[:space:]]*//g' jargon-hacker | awk -v RS='\n\n' 'FNR == 3' | fmt -w 58

Cuyo resultado es:

hacker n. [originally, someone who makes furniture
with an axe] 1. A person who enjoys exploring the
details of programmable systems and how to stretch
their capabilities, as opposed to most users, who
prefer to learn only the minimum necessary. 2. One
who programs enthusiastically (even obsessively) or
who enjoys programming rather than just theorizing
about programming. 3. A person capable of appreciating
{hack value}. 4. A person who is good at programming
quickly. 5. An expert at a particular program, or one
who frequently does work using it or on it; as in `a
Unix hacker'. (Definitions 1 through 5 are correlated,
and people who fit them congregate.) 6. An expert or
enthusiast of any kind. One might be an astronomy
hacker, for example. 7. One who enjoys the intellectual
challenge of creatively overcoming or circumventing
limitations. 8. [deprecated] A malicious meddler who
tries to discover sensitive information by poking
around. Hence `password hacker', `network hacker'. The
correct term for this sense is {cracker}.

Si observamos con atención el párrafo anterior toparemos con este tipo de líneas:

{hack value}. 4. A person who is good at programming
...
correct term for this sense is {cracker}.

En ellas aparecen términos o expresiones entre llaves. Estos términos son referencias cruzadas a otros artículos del mismo diccionario.

Sería magnífico disponer de una lista con las referencias que constan en los artículos que vamos leyendo y guardarla en un fichero. De esta forma podríamos profundizar más tarde en su sentido consultando los términos relacionados. Quizá podamos conseguir que HAL realice esta tarea, la creación de la lista de referencias, y nos descargue de un trabajo que sin su ayuda sería bastante desagradable.

Descompongamos la tarea en partes:

  1. Obtener la definición del término del diccionario apropiado, pongamos que es el término 'hacker' del diccionario jargon.

  2. Extraer de la definición resultante todas las expresiones entre llaves.

  3. Crear una lista con el formato:

    hacker:
    referencia-cruzada-1
    referencia-cruzada-2
    ...

  4. Guardar esa lista en un fichero, digamos jargon_ref.


Anotemos, también, las subtareas que ya sabemos hacer:

  • Obtener la definición:
        dict -d jargon hacker

  • Guardar en un fichero:
      [salida con la lista creada] >jargon_ref


De las dos tareas restantes la menos engorrosa es la tercera, la relativa a la creación de una lista a partir de los términos extraídos. Empecemos por ella.

Para modificar un flujo de texto ya vimos que sed, presentado hace unos días, podía ser de gran utilidad. No obstante, la modificación propuesta va a ser más profunda que las que hasta ahora hemos encomendado a sed, sobre todo porque va a introducir texto nuevo donde no lo había (la palabra 'hacker' al principio de la lista) y porque no conocemos de antemano el número de ítems en la lista. En casos de modificaciones serias de los flujos de texto puede ser recomendable recurrir a otros ayudantes aún más potentes y versátiles. Superayudantes como awk o perl son ideales en este tipo de situaciones. El problema de estos superayudantes es que, como contrapartida, son mucho más complejos y su dominio requiere un estudio muy intenso y muchas horas de dedicación. Se trata, en realidad, de lenguajes de programación en toda regla, cuyo tratamiento, aun somero, está más allá de lo que cabe esperar de esta serie de artículos. No obstante, acudir puntualmente a ellos y sólo para ciertos casos concretos evitará los galimatías que tendríamos que plantear en caso de limitarnos únicamente a sed. Bastará conocer la parte de la lengua del superayudante que nos venga bien y aprender nuevas partes a medida que lo necesitemos. No es diferente de tratar de explicar a un esquimal el color de la nieve en nuestro país en un idioma como el inglés, suponiendo que él lo entienda. Merece la pena aprender las palabras esquimales correspondientes para el blanco, aunque no sepamos, por el momento, ninguna otra.

Dirijámonos, por tanto, al Polo Norte de HAL en compañía de awk. El extraño nombre de este superayudante procede de la inicial del apellido de sus creadores (Alfred Aho, Peter Weinberger, and Brian Kernighan) y está especialmente indicado para una situación como la que tenemos delante.

La parte de la lengua de awk que nos interesa conocer en este preciso instante se limita a que nos entienda en una expresión como ésta:

awk, imprime con el siguiente formato la lista de palabras que te voy a dar:
hacker:
palabra-1
palabra-2
...


Lo primero que vamos a hacer es simplificar esta expresión e imaginar que conocemos el número exacto de palabras de los que consta la lista. Supongamos que es una sola palabra. En tal caso la orden anterior sería exactamente esta:

awk, imprime con el siguiente formato la palabra que te voy a dar:
hacker:
palabra-1


¿Por qué simplificar la expresión? Por dos razones. La primera es que ambigüedades como los puntos suspensivos que aparecen en la primera orden son intolerables para mentes precisas como la de awk y cualquier nativo del universo de HAL. Especialmente con los números hay que ser muy cauto. Mentes matemáticas como las suyas exigen cantidades numéricas perfectamente definidas. La segunda razón es metodológica y es una máxima que habrá que tener muy en cuenta en el futuro. Es un complemento necesario al proceso de análisis-síntesis descrito el día pasado. Merece una consideración aparte.

La complejidad de una tarea depende no sólo del número de elementos básicos en que puede descomponerse, sino también de la dificultad intrínseca de cada una de esas tareas atómicas. La ejecución de ciertas tareas requiere de aproximaciones sucesivas. Tales aproximaciones suelen caracterizarse por un incremento progresivo de la extensión del dominio al que se aplica la solución: se parte del caso más simple posible y se construye un modelo de solución que funcione para él. Este modelo se va enriqueciendo o modificando adecuadamente para adaptarse a casos más complejos y, finalmente, a todos los casos posibles.

Este el sentido de que hayamos elegido el caso más simple con el que awk se puede encontrar para construir nuestra orden. Lo que resta es saber cómo se expresaría la orden entera.

La cosa es relativamente fácil en un caso básico como éste.

En primer lugar awk toma el flujo de texto de un fichero o, si el fichero no se especifica, del dispositivo de entrada. El mismo comportamiento, pues, que el de sed, grep y otros ayudantes de HAL, comportamiento que ya conocemos y que implica que awk puede tomar como su entrada la salida de otra orden. Para poder trabajar sobre este flujo, awk va depositando partes de su corriente (cada línea del flujo, si no se indica otra cosa) en zonas de trabajo que awk llama registros (records). Cada registro lo divide a su vez en lo que llama campos (fields). Los campos serían, si no se especifica otra cosa, las palabras de esa línea. En nuestro caso básico de una sola palabra, hay un único registro y un único campo. Nos referimos a cada campo por su posición: al primer campo se le denomina $1 ---el único de nuestro caso básico---; al segundo, $2; etc.

Una vez analizados e identificados los ingredientes del flujo entrante, awk ejecutará las acciones que le indiquemos. La sintaxis para especificar las acciones varía según el tipo de acción. En todo caso, cada acción debe estar separada de la siguiente por un signo de puntuación, el punto y coma (;) que pone fin a cada acción (aunque está permitido y es una convención común que la última acción carezca de ese signo). Finalmente, el conjunto o bloque de todas las acciones se enmarca entre llaves.

La acción que queremos que awk ejecute en el caso simple que hemos propuesto es la de imprimir (print). ¿Qué es lo que queremos exactamente que imprima awk? En realidad, son dos cosas: la expresión "hacker:" y, en otra línea y a un espacio de distancia del margen izquierdo, la palabra proporcionada como flujo de entrada.

Cuando lo que va imprimir awk es una expresión literal, es decir, un conjunto de caracteres fijo y determinado unívocamente desde el principio, como "hacker:", dicha expresión se sitúa entre dobles comillas y se le da a print como su argumento. (Por defecto, print imprimirá también un salto al final de la expresión):

print "hacker:"

Cuando lo que awk ha de imprimir es el contenido de un campo y, por tanto, algo que variará según el campo de que se trate, el argumento de print es el identificador del campo:

print $1

print puede imprimir varias expresiones a la vez, recibir varios argumentos. Por ejemplo, si queremos imprimir un espacio (o sea, un valor fijo y unívocamente determinado) y, además, el valor del primer campo, diríamos:

print " " $1

Con todos estos conocimientos sobre la sintaxis de awk y de su acción print, podemos construir el bloque de acciones necesario para cumplir nuestro objetivo en el caso propuesto:

{print "hacker:"; print " " $1}.

Finalmente, y para que awk haga caso a lo que HAL, como intermediario nuestro, le vaya a pedir, entrecomillamos el bloque anterior y se lo damos a HAL como su primer argumento. El flujo de entrada sobre el que awk va actuar puede proceder ---como dijimos--- de la salida de otra orden. Por tanto, un ejemplo concreto que se adapta a todo lo explicado y resuelve el problema más simple que tenemos entre manos, sería:

echo 'adiós' | awk '{print "hacker:"; print " " $1}'

que, naturalmente, da como resultado:

hacker:
adiós


Enhorabuena, lector, si has llegado hasta este punto del camino con la mente despejada. Y si no, respira profundamente, que queda poco para terminar hoy. Si te ha resultado demasiado densa la explicación anterior sobre awk, sigue leyendo y repasa lo dicho aquí el día que regresemos al tema, que será pronto; recuerda que sólo hemos logrado un modelo básico, una primera aproximación, de la orden awk que necesitamos.

Estamos en disposición de montar provisionalmente las piezas del rompecabezas. Recordemos las tareas parciales que hemos de resolver, indicando las que hemos realizado o están en camino de ser realizadas y las que no:

  1. Obtener la definición del término del diccionario apropiado, pongamos que es el término 'hacker' del diccionario jargon:
      dict -d jargon hacker

  2. Extraer de la definición resultante todas las expresiones entre llaves. [Es la única tarea no iniciada].
      ????

  3. Crear una lista con el formato especificado:
      awk '{print "hacker:"; print " " $1}'


  4. Guardar esa lista en un fichero, digamos jargon_ref.
      >jargon_ref


Y, finalmente, montamos las piezas como ya sabemos:

dict -D jargon hacker | ???? | awk '{print "hacker:"; print " " $1}' >jargon_ref

La pieza que falta es precisamente aquella que iba a propiciar nuestro encuentro con las expresiones regulares. Se suponía que era hoy cuando las atisbaríamos, pero nos hemos entretenido recorriendo un sendero lateral ---será que el miedo nos atenaza. Por no prolongar la ya muy larga caminata de hoy, dejaremos a los monstruos tranquilos hasta el día que viene.


Resumen:

  • awk es un superayudante de HAL, un lenguaje de programación en sí mismo, especializado en la manipulación de texto. Su uso es muy recomendable cuando se trata de construir un resultado formateado y con texto añadido a partir de un texto con una estructura característica pero con un contenido variable.

  • awk divide el flujo de texto entrante en registros y éstos, a su vez, en campos, que se identifican con el signo $n, donde n es un número de campo.

  • awk ejecuta sobre el flujo de texto entrante las acciones que le indiquemos. Estas acciones se agrupan en un bloque de acciones delimitado por llaves, donde cada acción esta separada de la siguiente por el signo ';'.

  • La acción print de awk imprime en la salida el argumento que se le da.

  • La complejidad de un problema se debe tanto al número de problemas parciales en que puede descomponerse como a la dificultad intrínseca de los problemas atómicos que lo componen.

  • Una forma fundamental de tratar la complejidad de un problema elemental, pero intrínsecamente difícil, es construir un modelo de solución para los casos más simples en que dicho problema se manifiesta e ir progresivamente refinando ese modelo hasta que se adapte a todos los casos posibles.

No hay comentarios:

Publicar un comentario