Renuncia de responsabilidad:
La versión original de este artículo fue publicada por IBM
developerWorks y es propiedad de Westtech Information Services. Este
documento es una versión actualizada del artículo original y contiene
mejoras introducidas por el Equipo de Documentación de Gentoo.
Este documento carece de soporte activo.
|
Awk mediante ejemplos, Parte 2
1.
Registros, bucles y matrices
Registros compuestos de múltiples líneas
Awk es una herramienta excelente para leer y procesar datos
estructurados, tales como el contenido del fichero de sistema
/etc/passwd. /etc/passwd contiene la base
de datos de usuarios en UNIX, y es un fichero de texto con campos
delimitados por el signo de dos puntos. Además contiene información
importante como las cuentas de usuario existentes y los números de ID
de los usuarios, entre otras cosas. En mi artículo anterior, mostré
como awk puede procesar este fichero de forma rápida. Todo lo que se
necesitaba era establecer la variables de separador de campos FS al
carácter ":".
Al establecer la variable FS de forma correcta, awk puede ser
configurado para interpretar casi cualquier bloque de datos
estructurado, siempre que haya un registro por línea. Sin embargo, FS
por sí sola no nos solucionará el problema si necesitamos procesar
registros que ocupen varias líneas. En estas situaciones, también
necesitaremos modificar la variable RS, que es el separador de
registro. Esta variable le dice a awk cuando se acaba un registro y
cuando comienza el siguiente.
Como ejemplo, podemos considerar la tarea de procesar la lista de
direcciones de los participantes en el programa federal de protección
de testigos:
Listado de Código 1.1: Registros de ejemplo para la lista del programa federal de protección de testigos |
Jimmy the Weasel
100 Pleasant Drive
San Francisco, CA 12345
Big Tony
200 Incognito Ave.
Suburbia, WA 67890
|
Lo ideal sería que awk reconociera cada grupo de tres líneas como un
registro, en lugar de reconocer cada línea como un registro
individual. Nuestro código sería mucho más simple si awk reconociera
la primera línea del registro como el primer campo ($1), la dirección
como el segundo campo ($2), y la ciudad, estado y código postal como
el tercer campo ($3). Y eso lo conseguimos con el código siguiente:
Listado de Código 1.2: Extrayendo un campo de la dirección |
BEGIN {
FS="\n"
RS=""
}
|
Al establecer FS a "\n" le decimos a awk que cada campo aparecerá en
su propia línea. Estableciendo RS a "" le decimos a awk que cada
registro está separado por una línea en blanco. Una vez que awk sabe
como la entrada está formateada, puede realizar todo el trabajo por sí
solo, y el resto del guión es simple. Veamos como quedaría un guión
completo para procesar esta lista de direcciones e imprimir cada
registro completo en una sola línea, en campos separados por comas.
Listado de Código 1.3: Guión completo |
BEGIN {
FS="\n"
RS=""
}
{ print $1 ", " $2 ", " $3 }
|
Si el guión se salvó como address.awk, y la información
sobre las direcciones está en un fichero llamado
address.txt, puedes ejecutar este guión escribiendo
awk -f address.awk address.txt. Este código producirá esta
salida:
Listado de Código 1.4: La salida del guión |
Jimmy the Weasel, 100 Pleasant Drive, San Francisco, CA 12345
Big Tony, 200 Incognito Ave., Suburbia, WA 67890
|
OFS y ORS
En la instrucción print se puede ver que awk une cadenas de caracteres
que son colocadas juntas en una línea. Hemos usado esta característica
para insertar una coma y un espacio (", ") entre los tres campos de
datos de cada línea. Ahora, si bien este método funciona, no es el más
elegante. En lugar de insertar la cadena fija (", ") entre los campos,
podremos indicarle a awk que lo haga por nosotros. Ésto lo haremos
mediante otra variable, que se llama OFS. Mira esta muestra de código.
Listado de Código 1.5: Código de ejemplo |
print "Hola,","Jim!"
|
Las comas en esta línea no son parte de la cadena literal. En su
lugar, ellas le dicen a awk que "Hello", "there", y "Jim!" son campos
separados, y que la variable OFS deberá imprimirse entre ambas
cadenas. Por defecto, awk produce esto:
Listado de Código 1.6: Salida de awk |
Hola, Jim!
|
Esto nos enseña, que, por defecto, OFS es igual a " ", un solo espacio
en blanco. Sin embargo, podemos redefinirlo de forma fácil para que
inserte nuestro separador de cambio personalizado. Aquí está una
versión actualizada del guión original address.awk que
usa ÖFS para añadir estos separadores intermedios (", ").
Listado de Código 1.7: Redefinir OFS |
BEGIN {
FS="\n"
RS=""
OFS=", "
}
{ print $1, $2, $3 }
|
Awk también tiene una variable especial ORS, llamada "separador de
campos de salida". Ésta se establece por defecto a ("\n"), con ella
podemos controlar qué separador se va a imprimir al final de cada
print. Por defecto viene a una sola línea, si lo queremos con doble
espaciado, entonces podemos establecer ORS para que sea igual a
"\n\n". O, si queremos que los registros estén separados por un simple
espacio (sin salto de línea), entonces asignaríamos un espacio en
blanco " " a ORS.
De múltiples líneas a tabulado
Imaginemos que lo que queremos es escribir un guión que convierta los
registros que ocupan varias líneas en otros que tengan una sola línea
cuyos campos vayan separados por tabuladores para importarlos en una
hoja de cálculo. Tras usar una versión ligeramente modificada de
address.awk, quedaría claro que nuestro programa solo
funciona para direcciones compuestas por tres líneas. Si el programa
encontrara el siguiente registro, la cuarta línea sería descartada
silenciosamente:
Listado de Código 1.8: Ejemplo de registro |
Cousin Vinnie
Vinnie's Auto Shop
300 City Alley
Sosueme, OR 76543
|
Para manejar situaciones como esta, sería bueno que nuestro código
pudiera ver el número de campos por registro y tenerlo en cuenta para
imprimirlos más tarde en orden. Ahora mismo, el código solo imprime
los tres primeros campos de la dirección. El siguiente código hace
justo lo que queremos:
Listado de Código 1.9: Código mejorado |
BEGIN {
FS="\n"
RS=""
ORS=""
}
{
x=1
while ( x<NF ) {
print $x "\t"
x++
}
print $NF "\n"
}
|
Primero establecemos el separador de campo FS a "\n" y el separador de
registros RS a "" para que awk interprete las direcciones multilínea
de forma correcta, como antes. Entonces ponemos la variable separadora
del registro de salida ORS a "", lo cual causará que la instrucción
print no escriba un carácter de nueva línea tras cada registro. Esto
significa que si queremos comenzar una nueva línea, tendremos que
imprimirla explícitamente con print "\n".
En el bloque de código principal, creamos una variable x que contiene
el número de campo actual. Inicialmente se pone a 1. Entonces usamos
un bucle while (idéntico al while del lenguaje C) para iterar a través
de todos los campos excepto el último, imprimiendo el campo
correspondiente en cada vuelta, y además un carácter de
tabulación. Finalmente imprimimos el último campo y un carácter de
salto de línea. De nuevo, como ORS está puesto a "", print no
imprimirá los caracteres de nueva línea por nosotros. La salida del
programa se verá de esta forma, que es exactamente lo que queríamos:
Listado de Código 1.10: La salida deseada. No es bonita, pero al estar delimitada por tabuladores es fácil de importar en una hoja de cálculo |
Jimmy the Weasel 100 Pleasant Drive San Francisco, CA 12345
Big Tony 200 Incognito Ave. Suburbia, WA 67890
Cousin Vinnie Vinnie's Auto Shop 300 City Alley Sosueme, OR 76543
|
Estructuras de bucle
Ya hemos visto la construcción while de awk, que es idéntica a la de
C. Awk también tiene una construcción "do...while", que evalúa la
condición al final del bloque de código, en lugar de al principio como
se haría en un bloque while estándar. Es similar al "repeat...until"
que podemos encontrar en otros lenguajes. Aquí hay un ejemplo:
Listado de Código 1.11: ejemplo de do...while |
{
count=1
do {
print "Me imprimo una vez al menos"
} while ( count != 1 )
}
|
Como la condición es evaluada tras el bloque, un bucle "do...while",
al contrario que un bucle while normal, siempre se ejecuta al menos
una vez. Por contra, un bucle normal no se ejecutará jamás si la
condición inicial es falsa.
bucles for
Awk permite la creación de bucles for, que, tal y como los while, son
idénticos a sus versiones en C:
Listado de Código 1.12: Bucle de ejemplo |
for ( initial assignment; comparison; increment ) {
code block
}
|
Aquí hay un ejemplo rápido:
Listado de Código 1.13: Ejemplo rápido |
for ( x = 1; x <= 4; x++ ) {
print "vuelta",x
}
|
Este código imprimirá lo siguiente:
Listado de Código 1.14: Salida del código de arriba |
vuelta 1
vuelta 2
vuelta 3
vuelta 4
|
Break y continue
De nuevo, al igual que en C, awk provee instrucciones break y
continue. Dichas sentencias nos brindan un mejor control sobre varios
tipos de bucle en awk. Aquí se expone un pedazo de código que necesita
desesperadamente una instrucción break:
Listado de Código 1.15: Ejemplo de código que necesita una instrucción break |
while (1) {
print "forever and ever..."
}
|
Como 1 siempre es cierto, este bucle while duraría para siempre. Aquí
hay un bucle que se ejecutaría solo 10 veces:
Listado de Código 1.16: Bucle que se ejecuta 10 veces |
x=1
while(1) {
print "vuelta",x
if ( x == 10 ) {
break
}
x++
}
|
Aquí, el break se usa para salir del bucle más interior. Se sale del
bucle y la ejecución continúa en la línea justo después del bloque del
bucle.
La instrucción continue complementa a break, y funciona de esta forma:
Listado de Código 1.17: La instrucción continue complementando a break |
x=1
while (1) {
if ( x == 4 ) {
x++
continue
}
print "vuelta",x
if ( x > 20 ) {
break
}
x++
}
|
Este código imprimirá desde "iteration 1" hasta "iteration 21",
exceptuando a "iteration 4". En la vuelta número 4, x es incrementado
y la instrucción continue es ejecutada, lo cual provoca el salto a la
siguiente iteración sin ejecutar ni una línea más de la vuelta
actual. La instrucción continue funciona para todos los tipos de bucle
interactivo en awk, al igual que break. Cuando se usa en el cuerpo de
un bucle for, continue provoca el incremento de la variable de
control. Veamos el equivalente en un bucle for:
Listado de Código 1.18: Bucle for equivalente |
for ( x=1; x<=21; x++ ) {
if ( x == 4 ) {
continue
}
print "vuelta",x
}
|
No es necesario incrementar x tras el continue, porque el bucle for,
al contrario que el while, la incrementa de forma automática.
Matrices
Te gustará saber que awk es capaz de manejar matrices. Sin embargo,
aquí comienzan en 1 en lugar de 0:
Listado de Código 1.19: Ejemplo de matriz en awk |
myarray[1]="jim"
myarray[2]=456
|
Cuando awk encuentra la primera asignación de myarray, la matriz es
creada, y myarray[1] pasa a contener "jim". Al pasar a la siguiente
asignación, la matriz pasa a contener dos elementos diferenciados.
has two elements.
Una vez definida la matriz, awk tiene mecanismos para iterar sobre sus
elementos de la forma siguiente:
Listado de Código 1.20: Iterando sobre matrices |
for ( x in myarray ) {
print myarray[x]
}
|
Este código imprimirá todos los elementos dentro de myarray. Al usar
esta forma especial "in" en un bucle for, awk asignará todos y cada
uno de los índices existentes en myarray a x (la variable de control
del bucle) en sucesivos turnos, ejecutando el código del bucle una vez
tras cada reasignación de x. Si bien se trata de una peculiaridad
bastante cómoda en awk, también es cierto que tiene una parte negativa
-- cuando awk cicla entre los índices de la matriz, no sigue ningún
orden particular. Esto quiere decir que no tenemos forma de saber si
la salida del código de arriba será:
Listado de Código 1.21: Salida del código de arriba |
jim
456
|
o
Listado de Código 1.22: Otra salida para el código de arriba |
456
jim
|
Parafraseando libremente a Forrest Gump, iterar sobre los elementos de
una matriz en awk es como abrir una caja de bombones -- nunca sabes lo
que te va a tocar. Todo esto se debe al carácter encadenado de las
matrices en awk, aspecto que veremos ahora.
Naturaleza de cadena de las matrices
En mi artículo anterior,
ya comenté que awk almacena las variables numéricas como cadenas de
caracteres. Si bien awk realiza las conversiones para que todo
funcione, también es verdad que abre las puertas a la escritura de
código de aspecto extraño:
Listado de Código 1.23: Código de aspecto extraño |
a="1"
b="2"
c=a+b+3
|
Tras de que este código se ejecute, c es igual a 6 ya que awk suma las
cadenas "1" y "2" tal y como si fueran los números 1 y 2. En ambos
casos, awk solucionará el problema correctamente. Esta naturaleza de
awk es bastante intrigante -- te preguntarás que es lo que pasará si
usamos índices de cadena para las matrices, por ejemplo, en este
código:
Listado de Código 1.24: Código de ejemplo |
myarr["1"]="Mr. Whipple"
print myarr["1"]
|
Tal y como esperarías, este código imprime "Mr. Whipple". Pero ¿y si
omitimos las comillas alrededor del segundo 1?
Listado de Código 1.25: Quitando las comillas en el print |
myarr["1"]="Mr. Whipple"
print myarr[1]
|
Adivinar el resultado de esto es más complicado. ¿Considerará awk que
myarr["1"] y myarr[1] son dos elementos distintos de la matriz? ¿o
quizás los considere como referencias al mismo elemento? La respuesta
correcta es que awk los considerará como el mismo elemento, y por
tanto, imprimirá "Mr. Whipple", tal y como hizo en el otro
ejemplo. Aunque parezca extraño, awk ha estado usando internamente
índices de cadena todo el rato.
Y tras conocer este comportamiento, alguien estará tentado de ejecutar
algo como esto:
Listado de Código 1.26: Código extraño |
myarr["name"]="Mr. Whipple"
print myarr["name"]
|
Y no solo no provoca un error, sino que además funciona de forma
idéntica a los ejemplos anteriores, e imprimirá también el mismo
resultado, "Mr. Whipple". Como ves, awk no nos limita al uso de
índices enteros, podemos usar cadenas sin problema alguno. Cuando
usamos una cadena a modo de índice como en myarr["name"], estamos
usando matrices asociativas. Técnicamente awk no está haciendo nada
distinto en la trastienda ya que incluso cuando usamos un entero, awk
lo trata como si éste fuera una cadena. Sin embargo, aún deberíamos
llamarlas matrices asociativas -- suena bien e impresionará a tu
jefe. El índice numérico que en realidad es una cadena será nuestro
pequeño secreto. ;)
Utilidades de matrices
Cuando nos referimos a matrices, awk nos ofrece bastante
versatilidad. Podemos usar índices de cadena y no estamos obligados a
tener secuencias de índices numéricos continuos (por ejemplo, podemos
definir myarr[1] y myarr[1000], y dejar los demás sin definir). Si
bien todo esto puede ser útil, en algunas circunstancias puede crear
confusión. Afortunadamente, awk ofrece un par de ventajas que hacen
que las matrices sean algo más manejables.
Podemos borrar elementos de una matriz, por ejemplo, para borrar un
elemento haríamos:
Listado de Código 1.27: Borrando elementos de la matriz |
delete fooarray[1]
|
Si quieres ver si un elemento particular existe, puedes usar el
operador lógico especial "in" tal y como sigue:
Listado de Código 1.28: Comprobar si un elemento existe |
if ( 1 in fooarray ) {
print "Si! Aquí está."
} else {
print "No! No se pudo encontrar."
}
|
La próxima vez
Hemos cubierto mucha materia en este artículo. La próxima vez,
redondeare un poco el conocimiento sobre awk enseñándote como usar
funciones matemáticas y de cadenas en awk, y verás como crear tus
propias funciones. También te guiaré sobre la creación de un programa
de gestión contable. Mientras tanto, te recomiendo que escribas tus
propios programas en awk, y también los siguientes recursos en la web:
2.
Recursos
-
Lee otros artículos de Daniel sobre awk publicados en
developerWorks: Awk mediante ejemplos, Parte 1 y Parte
3.
-
Si prefieres un buen libro a la vieja usanza, el sed & awk, 2ª
Edición de O'Reilly es una buena opción.
-
Asegúrate también de examinar comp.lang.awk
FAQ. También contiene varios enlaces sobre awk.
-
El tutorial
de awk de Patrick Hartigan viene con varios guiones
interesantes de awk.
-
El Compilador TAWK de
Thompson compila guiones de awk transformándolos en binarios
ejecutables. Hay versiones para Windows, OS/2, DOS, y UNIX.
-
La
guía de usuario de GNU Awk también está disponible para
referencia en línea.
|