jueves, 19 de febrero de 2009

HAL, awk, el entorno y otras disquisiciones

El día pasado nos quedamos con el siguiente problema sin resolver:

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

En lugar de ???? deberá haber una variable que contenga el valor de lo que damos como primer argumento a nuestra orden ./buscar_en_jargon. Sabemos que esta variable no puede ser simplemente $1, pues ella tiene ya un significado especial para awk y, dentro de awk pierde su significado como parámetro posicional.

El problema, que, en un principio, parece conducirnos a un callejón sin salida, puede afrontarse gracias al diseño de awk, que permite acceder a variables externas a él mismo, gracias a un recurso suyo especialmente diseñado para ese fin. Este recurso es, sencillamente, otra variable de awk, la variable ENVIRON.

Esta variable posee una característica que no hemos visto todavía en ninguna variable. Se trata de una variable que contiene, a su vez, variables. A este tipo de variables se las denomina técnicamente matriz (array, en inglés). En concreto, ENVIRON contiene todas las variables del entorno actual, de ahí su nombre, abreviatura de environment (entorno).

Por ejemplo, la siguiente expresión dentro de awk:

ENVIRON["LANG"]

devolvería el valor actual de nuestra variable de entorno LANG.

Por su parte, la expresión:

ENVIRON["PATH"]

devolvería el valor actual de nuestra variable de entorno PATH.

En general, para obtener desde awk el valor de una variable del entorno, se debe usar la expresión:

ENVIRON["variable_de_entorno"]

donde variable_de_entorno es el nombre de una variable del entorno. (Nótense los corchetes ---típicos cuando se accede a variables de una matriz--- y las comillas. Ambos son obligatorios).

Parecería, pues, que tenemos la solución a nuestro problema en la punta de los dedos:

awk 'BEGIN {FS="\n"; print ENVIRON["$1"] ":"} { print " " $1 }'

Desgraciadamente no funcionará. Y no lo hará, porque la variable $1 ---y, en general, cualquier parámetro posicional---, aun siendo una variable de HAL, no es una variable del entorno.

¡Es desesperante. La guerra que está dando el dichoso detallito!

Tranquilidad. Estamos muy cerca de ganar la batalla.

Pensemos con atención el problema y reconstruyamos lo que acabamos de decir. Necesitamos una forma de pasar el valor de la variable $1 a awk. awk puede recibir el valor de variables ajenas a él cuando ese valor es el valor de una variable de entorno. Si $1 fuera una variable de entorno el problema estaría resuelto, pero ningún parámetro posicional es una variable de entorno. Ergo ... ¿Llegamos a la conclusión? ¿Y si consiguiésemos crear nosotros mismos una variable de entorno nueva? En tal caso, podríamos pasarle a ella el valor de $1 para que awk obtuviese dicho valor a través de la nueva variable de entorno. ¿Pero podemos hacer tal cosa, crear nosotros mismos variables de entorno?

Por supuesto que podemos crear variables de entorno. El proceso consta de dos pasos:

  1. Crear la variable propiamente dicha

  2. Hacer que esta nueva variable se convierta en variable del entorno

Para crear una variable cualquiera, basta con esta instrucción:

NOMBRE_DE_LA_VARIABLE=valor_de_la_variable

Por ejemplo, podemos crear la variable AMIGO:

AMIGO="HAL9000"

Para ver su valor, basta, como sabemos, con un simple echo:

echo $AMIGO

El segundo paso es pedir a HAL que haga de esta variable una variable de entorno, lo que técnicamente se llama exportar la variable:

export AMIGO

Podemos comprobar que la variable forma parte del entorno con printenv:

printenv AMIGO

Por cierto, lo normal cuando se quiere crear una variable de entorno es crearla y exportarla en un solo paso:

export AMIGO="HAL9000"

Siguiendo este mismo procedimiento, es posible crear una variable de entorno dentro de nuestro guión y asignarle el valor de $1, digamos, ARTICULO. Reeditemos, pues, el guión para que su primera línea sea ahora ésta:

export ARTICULO=$1

Finalmente, y puesto que la variable ARTICULO formará parte del entorno antes de que awk ponga en marcha sus resortes, podemos pasarle su valor como hicimos como otras variables de entorno en ejemplos anteriores:

ENVIRON["ARTICULO"]

Por tanto, la instrucción awk de nuestro guión deberá modificarse de la siguiente manera:

awk 'BEGIN {FS="\n"; print ENVIRON["ARTICULO"] ":"} { print " " $1 }'

Finalmente, y puesto que el valor de $1 y el de ARTICULO son el mismo desde el comienzo del guión, resulta más homogéneo y estético dar a dict como argumento el valor de ARTICULO en lugar de $1. Y no se olviden las comillas, que deben estar ahora ahí por la razón que se explicó el día pasado:

dict -d jargon "$ARTICULO"

Nuestro guión completo, con los cambios anteriores, quedará, pues, así:

export ARTICULO=$1

dict -d jargon "$ARTICULO" \
| grep -E -o '[{][^[:punct:]]+}' \
| sort \
| awk 'BEGIN {FS="\n"; print ENVIRON["ARTICULO"] ":"} { print " " $1 }' \
| tee -a jargon_refs

Para cerciorarnos de que el problema ha sido resuelto, volvemos a ejecutar la orden:

./buscar_en_jargon "hacker ethic"

El resultado carece del desliz por el casi nos ganamos un capón el día pasado y por cuya solución hoy mereceríamos una felicitación de HAL:

hacker ethic:
{FidoNet}
{GNU}
{gray hat}
{samurai}
{superuser}
{tiger team}
{Usenet}

Pero no vamos a conformarnos tan fácilmente. Queremos ir a por nota.

Se habrá advertido tanto en la nueva forma que acabó adoptando nuestro guión buscar_en_jargon como en alguno de los guiones construidos en sesiones anteriores que un guión puede constar de órdenes diferentes y que, puesto que HAL ejecutará secuencialmente cada línea del guión (cada orden), ya no es necesario utilizar el signo '\' más que cuando la línea del caso sea muy larga y convenga fragmentarla por razones de legibilidad, aunque para HAL seguirá siendo una única línea. De hecho, hasta ahora hemos estado utilizado líneas en blanco (que HAL pasará por alto dentro de un guión) para separar unas instrucciones de otras.

Puesto que podemos incluir más órdenes en nuestro guión, quizá podamos sacar más provecho de él. Podemos replantear el problema inicial ---siguiendo la máxima M7 formulada aquí--- y modificarlo ligeramente para obtener un mayor beneficio del esfuerzo realizado:

Mostrar la definición de un artículo del Jargon File, así como la lista de sus referencias cruzadas y guardar estas últimas en el fichero jargon_refs.

La idea nueva en este replanteamiento del problema es que queremos que se muestre también en pantalla el contenido mismo del artículo y no sólo sus referencias.

Conseguirlo es fácil. Veámoslo.

Sabemos que el contenido del artículo lo devuelve la orden dict. Pero en nuestro guión actual es redirigido mediante una tubería a grep para que proceda a analizarlo como se le indica, de ahí que no aparezca en pantalla:

dict -d jargon "$ARTICULO" \
| grep -E -o '[{][^[:punct:]]+}'

Una solución sencilla es hacer que la salida de dict sea redirigida, no a grep, sino a la pantalla y también al fichero temporal jargon_tmp via tee y hacer, luego, que grep, ya fuera de la tubería, lea el contenido de ese fichero. Finalmente, y puesto que no vamos a reutilizar el fichero temporal creado, conviene borrarlo una vez se haya ejecutado el resto de instrucciones. El esquema de este procedimiento sería el siguiente:

dict ... | tee jargon_tmp
grep ... jargon_tmp
...
rm jargon_tmp

Rellenemos lo que falta e incluyámoslo en el guión:

export ARTICULO=$1

dict -d jargon "$ARTICULO" | tee jargon_tmp

grep -E -o '[{][^[:punct:]]+}' jargon_tmp \
| sort \
| awk 'BEGIN {FS="\n"; print ENVIRON["ARTICULO"] ":"} { print " " $1 }' \
| tee -a jargon_refs

rm jargon_tmp

Probemos la nueva versión:

./buscar_en_jargon console

El resultado es para dar saltos de alegría:

1 definition found

From Jargon File (4.3.1, 29 Jun 2001) [jargon]:

console n. 1. The operator's station of a {mainframe}. In times past,
this was a privileged location that conveyed godlike powers to anyone
with fingers on its keys. Under Unix and other modern timesharing OSes,
such privileges are guarded by passwords instead, and the console is
just the {tty} the system was booted from. Some of the mystique remains,
however, and it is traditional for sysadmins to post urgent messages to
all users from the console (on Unix, /dev/console). 2. On microcomputer
Unix boxes, the main screen and keyboard (as opposed to character-only
terminals talking to a serial port). Typically only the console can do
real graphics or run {X}.


console:
{mainframe}
{tty}
{X}

El uso de ficheros temporales dentro de guiones es muy frecuente. No obstante, conviene no abusar de ellos. Una buena recomendación es buscar soluciones que no incluyan ficheros temporales y sólo acudir a ellos cuando no se vea un camino natural y sencillo para hallar la solución.

Hemos logrado bastante con no demasiado trabajo. Merece la pena acabar refinando el guión para otorgarle su forma definitiva ---al menos, mientras no se nos ocurran nuevas mejoras.

Lo primero que podemos hacer es convertir todos los nombres de ficheros en variables iniciales (que no se exportarán y que sólo tendrán vigencia dentro del guión). Aquí, en este guión simple, este tipo de técnica no tiene ninguna incidencia real, pero es conveniente acostumbrase a evitar introducir en las propias órdenes nombres de ficheros y, en general, cualquier otro tipo de dato ajeno al sentido de la orden misma. Aparte de la cuestión estética, es mucho más fácil leer un guión en el que queda claro desde el principio el tipo de ficheros que se va a manejar. Por otra parte, los futuros cambios de esos nombres son mucho más sencillos de realizar si se encuentran situados en una parte visible del guión y no entremezclados con las órdenes propiamente dichas.

El guión reformado de acuerdo con esta recomendación quedaría así:

export ARTICULO=$1
F_TEMPORAL=jargon_tmp
F_REFERENCIAS=jargon_refs

dict -d jargon "$ARTICULO" | tee $F_TEMPORAL

grep -E -o '[{][^[:punct:]]+}' $F_TEMPORAL \
| sort \
| awk 'BEGIN {FS="\n"; print ENVIRON["ARTICULO"] ":"} { print " " $1 }' \
| tee -a $F_REFERENCIAS

rm $F_TEMPORAL

Finalmente, conviene añadir unos pocos comentarios explicativos que puedan servir al futuro lector ---y nosotros mismos podemos ser ese futuro lector, cuando dentro de unos meses olvidemos el qué y el porqué de nuestro hoy tan transparente guión. Por supuesto, a HAL ---sobra decirlo---, le importan un bledo los comentarios realizados por y para humanos, pero es que a HAL no se le olvidan ni se le ofuscan las cosas que le competen como a nosotros, pobres mortales.

## buscar_en_jargon - 20/02/09 - átopos
## Recibe como único argumento un artículo del 'Jargon File'
## y devuelve el contenido del artículo y sus referencias
## cruzadas. Estas últimas se almacenan en el fichero
## F_REFERENCIAS
export ARTICULO=$1 # Artículo del Jargon File
F_TEMPORAL=jargon_tmp # Fichero temporal
F_REFERENCIAS=jargon_refs # Referencias contenidas en ARTICULO

dict -d jargon "$ARTICULO" | tee $F_TEMPORAL

grep -E -o '[{][^[:punct:]]+}' $F_TEMPORAL \
| sort \
| awk 'BEGIN {FS="\n"; print ENVIRON["ARTICULO"] ":"} { print " " $1 }' \
| tee -a $F_REFERENCIAS

rm $F_TEMPORAL


Tras esta larguísima conversación, podemos ir a la cama, tanto HAL como nosotros, y dormir a pierna suelta, que ya es hora.


Resumen:

  • awk puede acceder a las variables del entrono mediante un variable interna de tipo array, denominada ENVIRON. Por ejemplo, con ENVIRON["PATH"] accedemos desde awk a la variable PATH del entorno.

  • Crear variables es un procedimiento habitual en cualquier tipo de guión. Una variable se crea asignando un nombre a un valor. Por ejemplo, para crear la variable AMIGO y darle el valor "HAL9000" se utiliza la expresión AMIGO="HAL9000".

  • La orden export permite convertir una variable en variable del entorno. La creación y exportación de la variable se suelen realizar en un único paso. Por ejemplo, export AMIGO="HAL9000", crea la variable AMIGO, le asigna un valor y la exporta al entorno.

  • En un guión puede haber varias órdenes distintas. HAL las leerá secuencialmente al interpretarlo.

  • El uso de ficheros temporales es frecuente dentro de los guiones. Estos ficheros suelen eliminarse una vez han cumplido su objetivo.

  • Todo dato externo a la lógica de una orden suele asignarse como valor a variables que se declaran y definen al principio del guión. Los nombres de ficheros son un ejemplo típico.

  • Es recomendable añadir breves comentarios a un guión, con el fin de que facilitar su comprensión a futuros lectores y a nosotros mismos.

No hay comentarios:

Publicar un comentario