miércoles, 4 de febrero de 2009

Condimento al gusto de HAL (I)

Ya vislumbramos el final del largo periplo que comenzamos hace días para obtener nuestra lista de las referencias cruzadas contenidas en el artículo hacker del Jargon File.

Aunque pueda resultar repetitivo, vamos una vez más a repasar nuestra lista de tareas inicial y el estado de su solución:

  1. Obtener la definición del término hacker que contiene el Jargon File [Terminada]:
      dict -d jargon hacker

  2. Extraer de la definición resultante todas las expresiones entre llaves [Terminada].
      grep -E -o '[{][^[:punct:]]+}'

  3. Crear una lista con el formato adecuado [Sin terminar: modelo creado]:
      awk '{print "hacker:"; print " " $1}'

  4. Guardar esa lista en un fichero [Terminada]:
      >jargon_ref


Tenemos incluso la orden completa diseñada [en rojo va lo que queda sin terminar. Nótese además el signo '\' que permite escribir en varias líneas una orden muy larga, como ésta y las que vendrán. Lo uso sin más comentario a partir de ahora]:

dict -d jargon hacker | grep -E -o '[{][^[:punct:]]+}' \
| awk '{print "hacker:"; print " " $1}' >jargon_ref


Lo que resta es refinar la orden awk para que dé el formato previsto a la salida de grep.

Refinar una solución es el último paso en la culminación de una tarea. Si la tarea es muy compleja puede resultar necesario llevarlo a cabo en una fase intermedia. En cualquier forma, es un procedimiento necesario del que pueden surgir incluso nuevas ideas. Refinar una solución implica en no pocas ocasiones refinar el propio planteamiento del problema y, en consecuencia, ampliar o reformular el esquema de su solución. Una tarea nunca está acabada del todo, del mismo modo que la perfección es utópica y ucrónica. Conviene acostumbrarse a esta idea, porque es central en todo proceso de resolución de problemas.

Consideremos nuestro caso actual, el modelo de instrucción awk que esbozamos al principio.

Ahora ya sabemos lo que nos devuelve grep: una lista de palabras entre llaves, cada una en una línea. Podemos preguntarnos si esta salida se ajusta a lo que awk espera.

Recordemos que awk dividía su flujo de entrada en registros y campos [El lector debe releer, si no lo recuerda bien, el artículo precedente sobre el tema]. Dijimos, más o menos de pasada, que, si no se especifica lo contrario, el salto de línea es el indicador del final de un registro y el comienzo del siguiente, o sea, el carácter que separa un registro de otro, y que el espacio cumple idéntica función como separador de los campos. También se recordará que la segunda acción print de nuestro modelo para la orden awk imprimía un espacio seguido del primer campo. Por tanto, la salida de grep:

{hack value}
{cracker}
{the network}
{Internet address}
{hacker ethic}
{bogus}
{geek}
{wannabee}

será analizada por nuestra orden provisional awk de modo que cada conjunto de palabras entre llaves será un registro y cada fragmento de texto separado por espacio será un campo. No es de extrañar entonces que nuestra orden sea totalmente inadecuada para nuestro propósito, puesto que para cada registro (cada conjunto de palabras entre llaves) ejecutará las acciones indicadas y producirá este resultado:

hacker:
{hack
hacker:
{cracker}
hacker:
{the
hacker:
{Internet
hacker:
{hacker
hacker:
{bogus}
hacker:
{geek}
hacker:
{wannabee}

Una forma de empezar a resolver el problema es redefinir la variable interna de awk que establece el carácter separador de campos. Esta variable recibe el nombre FS (Field Separator) y su valor por defecto es el espacio.

Si el valor de FS fuese, en lugar del espacio, el salto de línea, awk analizaría la lista que produce grep de forma que cada cada registro fuese una línea de esa lista y, a la vez, el primer y único campo del registro. De esta manera la acción print " " $1 imprimiría por cada línea (por cada registro) el grupo de palabras entre llaves (el campo $1 de cada registro). Y eso es justo lo que nos interesa.

En consecuencia, debemos incluir dentro de nuestra orden awk una expresión que asigne a FS el valor del salto de línea. Este tipo de expresión de asignación de valor se escribe así (donde la secuencia '\n' representa el salto de línea):

FS="\n"

awk nos proporciona la opción -v para incluir redefiniciones de sus variables internas. Por tanto, podríamos reescribir nuestra orden de la siguiente manera:

awk -v FS="\n" '{print "hacker:"; print " " $1}'

Y el resultado de ejecutarla sobre la salida de grep:

grep -E -o '[{][^[:punct:]]+}' jargon_hacker \
| awk -v FS="\n" '{print "hacker:"; print " " $1}'


sería éste:

hacker:
{hack value}
hacker:
{cracker}
hacker:
{the network}
hacker:
{Internet address}
hacker:
{hacker ethic}
hacker:
{bogus}
hacker:
{geek}
hacker:
{wannabee}

Es obvio, que queda un problema, lograr que la acción print "hacker:" se ejecute una sola vez y al principio. Tal como está ahora en nuestra orden provisional este print se ejecuta para cada registro (para cada línea de la salida de grep), con el resultado anterior, aún insatisfactorio.

Podemos conseguir el comportamiento deseado (imprimir una sola vez y al principio la expresión "hacker:"), si situamos la acción print "hacker:" en un bloque de acciones especial que se ejecuta antes de que awk analice su flujo de entrada. Este bloque de acciones se escribe delante del bloque de las restantes acciones precedido por la palabra BEGIN:

BEGIN {acciones_iniciales} {resto de acciones}

Este bloque inicial es, además, el sitio ideal para introducir las redefiniciones que establezcamos de las variables internas de awk ---al fin y al cabo redefinir es también un tipo de acción inicial. Es pues el mejor lugar para poner ahí nuestro FS="\n", con lo que nos evitamos situarlo como el argumento de la opción -v, que resulta, en consecuencia, innecesaria.

La orden awk nuevamente modificada quedaría de la siguiente manera:

awk 'BEGIN {FS="\n"; print "hacker:"} {print " " $1}'

No viene mal resumir, como colofón, los pasos que sigue awk para procesar su argumento:

  1. Ejecuta el bloque de acciones que empieza con BEGIN:

    1. Pone la variable FS ---que define el carácter que separa los campos--- al valor '\n' (salto de línea).

    2. Imprime la expresión "hacker:".

  2. Analiza el flujo de entrada:


    1. El valor de RS (separador de registros) no está modificado en el bloque BEGIN: los registros son las líneas del flujo entrante.

    2. El valor de FS (separador de campos) está modificado en el bloque BEGIN: los campos son las líneas del flujo entrante.

  3. Ejecuta el bloque de acciones restantes sobre cada registro:


    1. Imprime espacio seguido del valor del primer campo del primer registro.

    2. Repite n veces la acción anterior hasta agotar todos los registros.

  4. Devuelve el resultado al dispositivo de salida.


Es hora de poner a prueba la orden remozada:

grep -E -o '[{][^[:punct:]]+}' jargon_hacker \
| awk 'BEGIN {FS="\n"; print "hacker:"} { print " " $1 }'


El resultado, finalmente, cumple nuestras expectativas:

hacker:
{hack value}
{cracker}
{the network}
{Internet address}
{hacker ethic}
{bogus}
{geek}
{wannabee}

Tarea terminada, sí. Podemos felicitarnos. Hasta puede que HAL guste en su interior del sabor de nuestra cocina, quién sabe ;-)
Mas, si sintiese algo parecido, seguro que nos reprocharía no haber sacado más beneficio de nuestro esfuerzo. Tal vez la tarea esté abierta todavía a mejoras no sospechadas. Lo veremos a continuación ... [el próximo día]

Resumen:

  • Para disponer una orden en varias líneas se usa el signo '\' como señal de final de línea. Es útil cuando la orden es muy larga y queremos que sea fácil de leer y escribir.

  • La secuencia de caracteres '\n' representa el salto de línea.

  • Parte esencial en la resolución de problemas es el proceso de refinar las soluciones provisionales o modelos de solución obtenidos en las estadios iniciales de dicho proceso.

  • Los valores de las variables internas de awk son susceptibles de recibir asignaciones diferentes a las establecidas por defecto. Tal es el caso, por ejemplo, de las variables RS y FS que especifican las caracteres que awk interpreta como separador de registros y separador de campos, respectivamente.

  • awk permite definir acciones iniciales, que se ejecutan con anterioridad al análisis que se realiza del flujo de texto entrante. Tales acciones se identifican con un bloque preliminar de acciones precedido de la palabra clave BEGIN

No hay comentarios:

Publicar un comentario