ant

AWK
4.14 I

sig

4.14.1 Que es awk y para que se usa

La palabra 'awk' se usa tanto para referirse a un lenguaje de manipulación de ficheros de datos como para referirse a su interprete.(Para los que se extrañen por ese peculiar nombre , este proviene de sus creadores originales Alfred Aho, Peter Weinberger y Brian Kernighan).

Dado que los SO tipo Unix incluido Linux acostumbran con mucha frecuencia a usar ficheros de configuración del sistema en formatos de de texto perfectamente legibles y editables se diseñó un lenguaje para poder procesar este tipo de ficheros de datos en formato de texto.

Cuando un programa necesita una velocidad muy alta para acceder a unos datos o para modificarlos se utilizan estructuras de datos más sofisticadas.

En muchos otros casos un fichero de configuración será accedido de forma muy ocasional, y resulta más interesante usar un formato de texto sencillo. Por ejemplo hay ficheros de configuración que solo se usan durante la carga de un programa. Algunos de estos programas suelen cargarse una sola vez mientras arranca el sistema y luego en condiciones normales permanecen arrancados todo el tiempo.

'awk' nació en 1978 como un lenguaje pequeño y sencillo pero desde entonces ha evolucionado mucho y en la actualidad se puede afirmar que es un lenguaje muy potente y versátil.

'awk' es un complemento muy bueno para su uso con shell-script. Nos vamos a centrar en el procesamiento de datos en los cuales cada línea estará estructurada en campos. Estos campos estarán delimitados entre si por algún carácter o por alguna secuencia especial de caracteres especialmente reservado para ello. Esta secuencia será el delimitador de campos y no debe aparecer en el interior de ningún campo. Cada línea equivale a un registro.

La mayoría de las bases de datos, y hojas de cálculo permiten volcar los datos en formato de texto para poder ser exportados entre distintas bases de datos. Estas salidas se pueden procesar fácilmente mediante 'awk'. También se puede usar 'awk' con la salida de diversos programas. Esto permite entre otras cosas usar 'awk' para acoplar una salida de un programa con la entrada de otro que necesite un formato muy distinto.

4.14.2 Forma de uso

'awk' suele estar instalado en la mayoría de los sistemas ya que su uso suele ser necesario. Por eso en Linux suele encontrarse entre los paquetes básicos del sistema en todas las distribuciones.

Se puede usar de varias formas. Tenemos que pasar a 'awk' el código del programa, y los datos. El primero se puede pasar bien como argumento o indicando -f nombre del fichero que contiene el código del programa. La entrada se puede pasar dando el nombre del fichero de entrada como último argumento o en caso contrario lo tomará por la entrada estándar.

$ ## Generamos en /tmp un par de ficheros
$ echo -e "\n" > /tmp/echo.out

$ echo '{ print "Hola mundo" }' > /tmp/ejemplo1.awk

$ ## Ejecutaremos el mismo programa de 4 formas distintas

$ echo -e "\n" | awk '{ print "Hola mundo" }'
Hola mundo
Hola mundo

$ awk '{ print "Hola mundo" }' /tmp/echo.out
Hola mundo
Hola mundo

$ echo -e "\n" | awk -f /tmp/ejemplo1.awk
Hola mundo
Hola mundo

$ awk -f /tmp/ejemplo1.awk /tmp/echo.out
Hola mundo
Hola mundo
El programa que acabamos de utilizar imprimirá el literal "Hola mundo" a cada línea de datos que procese. En este caso usamos solo un par de líneas vacías como entrada de datos.

Vamos a localizar el binario de 'awk'
$ whereis awk
/usr/bin/awk
Vamos a suponer que en su sistema se encuentre también en '/usr/bin'. Puesto que awk es un lenguaje interpretado perfectamente legible también podemos decir que los programas de awk son script. Para poder usarlos directamente podemos añadir una primera línea con número mágico y poner permiso de ejecución.
$ echo '#!/usr/bin/awk -f' > /tmp/ejemplo2.awk
$ echo '{ print "Hola mundo" }' >> /tmp/ejemplo2.awk
$ chmod +x /tmp/ejemplo2.awk
$ echo -e "\n" | /tmp/ejemplo2.awk
Hola mundo
Hola mundo

4.14.3 Estructura de un programa awk

Un programa 'awk' puede tener tres secciones distintas.
· Puede incluir una primera parte para que se ejecute antes de procesar ninguna de las líneas de entrada. Se usa para ello la palabra reservada BEGIN seguida de una o más instrucciones todas ellas englobadas dentro de un par de corchetes. '{' , '}'.

· Puede incluir una parte central que se procesará entera para cada linea de entrada de datos y que puede tener varios bloques '{' , '}'. Si uno de estos bloques contiene una expresión regular se procesará solo cuando la línea de entrada se ajuste al patrón de la expresión regular.

· Puede incluir una parte final que se procesará en último lugar una vez termine la lectura y procesado de todas las líneas de entrada. Se usa para ello la palabra reservada END seguida de una o más instrucciones todas ellas englobadas dentro de un par de corchetes. '{' , '}'.

El primer ejemplo que vimos anteriormente ("Hola mundo") solo tenía una de las tres partes. Concretamente era la parte central ya que no pusimos ninguna de las palabras reservadas BEGIN o END.

Vamos a poner ahora un ejemplo con las tres partes. Edite un fichero con nombre '/tmp/3partes.awk'
BEGIN { print "Erase una vez..." }
{ print "...y entonces bla, bla, bla ..." }
END { print "...y colorín colorado este cuento se ha acabado." }
Ejecutelo con:
$ echo -e "\n\n\n" | awk -f /tmp/3partes.awk

Erase una vez......
y entonces bla, bla, bla ......
y entonces bla, bla, bla ......
y entonces bla, bla, bla ......
y entonces bla, bla, bla ......
y colorín colorado este cuento se ha acabado.
Es importante que comprenda que la parte central se ejecutará tantas veces como líneas de datos existan. En nuestro ejemplo son cuatro líneas generadas por 'echo -e "\n\n\n" '. En cambio las partes 'BEGIN { ... }' y 'END { ... }' se ejecutan una sola vez. La primera antes de procesar la primera línea y la última después de procesar la última línea.

Los comentarios en 'awk' comienzan con un '#' y terminan al final de la línea.

4.14.4 Expresiones regulares

Algunas veces los datos pueden venir con algunas lineas que no interesa procesar o que se deben procesar de forma distinta. Podemos usar una expresión regular delimitada por el carácter '/' para seleccionar una acción especial. Vamos a editar otro ejemplo que llamaremos '/tmp/expreg.awk':
BEGIN { print "Erase una vez..." }
/^$/ { print "Linea vacía" }
/[0-9]+/ { print "Tiene un número" }
/\.$/ { print "Termina con punto" }
# Esto es un comentario
{ print "--------------------------------------" }
END { print "...y colorín colorado este cuento se ha acabado." }
Ahora editamos un segundo fichero '/tmp/expreg.dat':
Línea número 1.
Línea número 2

....
Fin de los datos
Ahora ejecute lo siguiente:
$ awk -f /tmp/expreg.awk /tmp/expreg.dat

Erase una vez...
Tiene un número
Termina con punto
--------------------------------------
Tiene un número
--------------------------------------
Linea vacía
--------------------------------------
Termina con punto
--------------------------------------
--------------------------------------
...y colorín colorado este cuento se ha acabado.
Vemos que cada línea de datos puede cumplir más de una regla y que cuando no ponemos una expresión regular siempre se ejecutará la acción. En este caso todas las líneas provocan la escritura de una línea de guiones '--------------------------------------'.

El uso de expresiones regulares puede ayudarnos a eliminar cabeceras, líneas vacías o incompletas o cosas así que no deseamos procesar.

4.14.5 Delimitadores de campos

No hemos tratado aún los campos de una línea. Una línea que tenga distintos campos debe usar alguna secuencia para delimitar los campos entre si.
Lo mismo para definir un delimitador que en cualquier otro caso donde se usen cadenas de caracteres podemos encontrarnos la necesidad de usar caracteres especiales que no pueden ser introducidos directamente. Para ello existen determinadas secuencias que empiezan por el carácter '\' y que tienen significado especial.

Caracteres de escape
\a Produce un pitido en el terminal
\b Retroceso
\f Salto de página
\n Salto de línea
\r Retorno de carro
\t Tabulador horizontal
\v Tabulador vertical
\dddCarácter representado en octal por 'ddd'
\xhex Carácter representado en hexadecimal por 'hex'
\c Carácter 'c'

El último caso se usa para eliminar el significado especial de un carácter en determinadas circunstancias. Por ejemplo para usar un '+' o un '-' en una expresión regular usaríamos '\+' o '\-'

Podemos elegir un solo carácter para separar campos. Hay ficheros de configuración como /etc/passwd, /etc/group, que usan un solo carácter para delimitar los campos. Por ejemplo los dos puntos ':' , el blanco '\ ', la coma ',' el tabulador '\t' etc...

'awk' permite usar como delimitador más de un carácter. Para ello se asignará a la variable 'FS' una cadena de caracteres que contenga una expresión regular . Por ejemplo para usar como delimitador el carácter ':' habría que incluir 'BEGIN { FS = ":" }'

Si no se especifica ningún delimitador se asumirá que los campos estarán delimitados por uno o más blancos o tabuladores consecutivos lo cual se expresa como "[\ \t]+". El carácter '\' debe usarse para escapar cualquier carácter con significado especial en una expresión regular y algunos caracteres normales precedidos de '\' se usan para representar caracteres especiales. '\t' es el tabulador.

En 'awk' se usa $1 para referenciar el campo 1, $2 para referenciar el campo 2, etc... y para referenciar el registro completo usaremos $0.

Edite el siguiente fichero '/tmp/delim1.awk'
{ print "+", $1, "+", $2, "+", $3, "+", $4, "+" }
$1, $2, $3, y $4 representan a los campos 1, 2, 3, y 4 respectivamente.

Edite el siguiente fichero de datos '/tmp/delim1.dat'
aaa bbb ccc     ddd      eee
111 222 333 444
En la primera línea debe introducir un blanco para separar los primeros blancos y una secuenciencia de ', , , ' para separar los dos últimos campos. Es importante que lo edite de esta forma porque el resultado de los ejemplos podría variar.

Ahora ejecute lo siguiente:
$ awk -f /tmp/delim1.awk /tmp/delim1.dat
+ aaa + bbb + ccc + ddd +
+ 111 + 222 + 333 + 444 +
Edite el siguiente fichero '/tmp/delim0.awk'
{ print "+", $3, "+", $4, "+", $1, "+", $2, "+" }
Ahora ejecute lo siguiente:
$ awk -f /tmp/delim0.awk /tmp/delim1.dat
+ ccc + ddd + aaa + bbb +
+ 333 + 444 + 111 + 222 +
Con ello hemos conseguido variar el orden de aparición de los campos, pero todavía no hemos especificado ningún delimitador. Por ello hemos asumido el delimitador por defecto. (uno o más blancos y tabuladores). Para especificar un delimitador distinto tenemos que asignar su valor a la variable FS y además tenemos que hacerlo antes de leer el primero registro por lo cual se incluirá la instrucción en la sección inicial precedida de BEGIN.

Edite el siguiente fichero '/tmp/delim2.awk'
BEGIN { FS = "\ " }
{ print "+", $1, "+", $2, "+", $3, "+", $4, "+" }
Estamos definiendo un único carácter blanco como separador.

Ahora ejecute lo siguiente:
$ awk -f /tmp/delim2.awk /tmp/delim1.dat
+ aaa + bbb + ccc + +
+ 111 + 222 + 333 + 444 +
Vamos a cambiar de delimitador.

Edite el siguiente fichero '/tmp/delim3.awk'
BEGIN { FS = "\t" }
{ print "+", $1, "+", $2, "+", $3, "+", $4, "+" }
Estamos definiendo un único carácter tabulador como separador.

Ahora ejecute lo siguiente:
$ awk -f /tmp/delim3.awk /tmp/delim1.dat
+ aaa bbb ccc + + ddd + +
+ 111 222 333 444 + + + +

4.14.6 Selección de registros por campo

Vamos a editar un fichero que simulará la salida de datos obtenida desde una base de datos relacional. Usaremos estos datos en varios ejemplos. Puede corresponder a una contabilidad de un alquiler de un piso. Lo llamaremos 'contabil.dat'.

     fecha|concepto|importe
----------+--------+-------
01-01-2004|-       |     96
16-12-2004|AGUA    | -14650
05-01-2004|LUZ     | -15797
24-01-2004|GAS     | -34175
27-01-2004|INGRESO | 141200
01-02-2004|MENS    | -96092
25-02-2004|LUZ     | -12475
01-03-2004|MENS    | -96092
06-03-2004|INGRESO | 101300
01-04-2004|MENS    | -96092
06-04-2004|AGUA    | -15859
07-04-2004|INGRESO | 134000
01-05-2004|MENS    | -98975
02-05-2004|LUZ     | -11449
09-05-2004|INGRESO |  95000
23-05-2004|GAS     | -21428
25-05-2004|GAS     | -16452
01-06-2004|MENS    | -98975
07-06-2004|INGRESO | 130000
01-07-2004|MENS    | -98975
04-07-2004|LUZ     | -12403
07-07-2004|AGUA    |  -5561
10-07-2004|INGRESO |  99000
24-07-2004|GAS     | -11948
01-08-2004|MENS    | -98975
10-08-2004|INGRESO | 122355
04-09-2004|LUZ     | -12168
10-09-2004|INGRESO | 129000
19-09-2004|AGUA    | -10529
28-09-2004|GAS     |  -2620
01-10-2004|MENS    | -98975
10-10-2004|INGRESO | 112000
(32 rows)

Lo primero que vemos es que tiene una cabecera de dos líneas inútiles y un final también inútil. Podemos asegurar que las líneas que deseamos procesar cumplirán un patrón de dos números guión dos números guión cuatro números y línea vertical. Vamos a editar un programa que llamaremos 'contabil1.awk'
BEGIN { FS="\|" }
/[0-9][0-9]\-[0-9][0-9]\-[0-9][0-9][0-9][0-9]\|/ {
print NR, ", ", $1, ", ", $2, ", ", $3 }
Vamos a ejecutar este ejemplo y vamos a ver su salida

$ awk -f contabil1.awk < contabil.dat


3 , 01-01-1999 , - , 96 4 , 16-12-1999 , AGUA , -14650 5 , 05-01-2000 , LUZ , -15797 6 , 24-01-2000 , GAS , -34175 7 , 27-01-2000 , INGRESO , 141200 8 , 01-02-2000 , MENS , -96092 9 , 25-02-2000 , LUZ , -12475 10 , 01-03-2000 , MENS , -96092 11 , 06-03-2000 , INGRESO , 101300 12 , 01-04-2000 , MENS , -96092 13 , 06-04-2000 , AGUA , -15859 14 , 07-04-2000 , INGRESO , 134000 15 , 01-05-2000 , MENS , -98975 16 , 02-05-2000 , LUZ , -11449 17 , 09-05-2000 , INGRESO , 95000 18 , 23-05-2000 , GAS , -21428 19 , 25-05-2000 , GAS , -16452 20 , 01-06-2000 , MENS , -98975 21 , 07-06-2000 , INGRESO , 130000 22 , 01-07-2000 , MENS , -98975 23 , 04-07-2000 , LUZ , -12403 24 , 07-07-2000 , AGUA , -5561 25 , 10-07-2000 , INGRESO , 99000 26 , 24-07-2000 , GAS , -11948 27 , 01-08-2000 , MENS , -98975 28 , 10-08-2000 , INGRESO , 122355 29 , 04-09-2000 , LUZ , -12168 30 , 10-09-2000 , INGRESO , 129000 31 , 19-09-2000 , AGUA , -10529 32 , 28-09-2000 , GAS , -2620 33 , 01-10-2000 , MENS , -98975 34 , 10-10-2000 , INGRESO , 112000

Podemos apreciar varias cosas. NR es una variable del sistema que toma el valor del número de registro que se está procesando. Podemos ver que las dos primeras líneas y la última han sido descartadas. También vemos que las primeras líneas usan un solo dígito para el número de registro y luego usan dos dígitos. Esto hace que las columnas no queden alineadas. Vamos a modificar el programa para que muestre los registros completos ($0) cuando no se cumpla la condición anterior. Para ello editaremos un fichero que llamaremos 'contabdescarte.awk'.
BEGIN { FS="\|" } ! /[0-9][0-9]\-[0-9][0-9]\-[0-9][0-9][0-9][0-9]\|/ { print NR, $0 }
Vamos a ejecutar este ejemplo y vamos a ver su salida
$ awk -f contabdescarte.awk < contabil.dat

1     fecha|concepto|importe
2 ----------+--------+-------
35 (32 rows)

4.14.7 Formato de salida con printf

Para imprimir con formato usaremos 'printf' en lugar de 'print'. printf se usa en varios lenguajes. El primer argumento de esta función debe de ser una cadena de caracteres que contenga el formato de salida deseado para la salida. Los formatos de cada dato se expresan mediante unas directivas que empiezan con el carácter '%' y debe de existir en dicha cadena tantas directivas como datos separados por coma a continuación de la cadena de formato.
Hay que tener en cuenta que en 'awk' la concatenación de cadenas se usa poniendo una cadena a continuación de otra separada por blancos.

Por ejemplo:
# cad = "(unodos)"
cad = "uno" "dos" ;
cad = "(" cad ")"
Especificación de formato de datos para 'printf'
%c Carácter ASCII
%d Entero representado en decimal
%e Coma flotante (exponente = e[+-]dd)
%E Coma flotante (exponente = E[+-]dd)
%f Coma flotante sin exponente
%g Equivale al más corto de los formatos 'e' o 'f'
%G Equivale al más corto de los formatos 'E' o 'F'
%o Entero representado en octal
%s Cadena de caracteres
%x Entero representado en hexadecimal con minúsculas
%X Entero representado en hexadecimal con mayúsculas
%% Carácter '%'

En estos formatos se puede intercalar inmediatamente a continuación del '%' primero un signo '+' o un '-' que son opcionales y significan respectivamente alineación a la derecha o a la izquierda. En segundo lugar y de forma igualmente opcional se puede intercalar un número para indicar un ancho mínimo.

(Si el dato ocupa más que el ancho especificado se muestra el dato completo haciendo caso omiso de la indicación de anchura). En el caso de coma flotante se puede indicar el ancho total del campo y la precisión (anchura) de la parte decimal.

Veamos unos pocos ejemplos.
$ echo | awk '{ print "Hola mundo" }'
Hola mundo

$ echo | awk '{ printf "Hola %s\n", "mundo" }'
Hola mundo

$ echo | awk '{ printf "#%d#%s#\n", 77, "mundo" }'
#77#mundo#

$ echo | awk '{ printf "#%10d#%10s#\n", 77, "mundo" }'
#      77#     mundo#

$ echo | awk '{ printf "#%-10d#%-10s#\n", 77, "mundo" }'
#77      #mundo    #

$ echo | awk '{ printf "#%+4d#%+4s#\n", 77, "mundo" }'
# +77#mundo#

$ echo | awk '{ printf "#%04d#%+4s#\n", 77, "mundo" }'
#0077#mundo#

$ echo | awk '{ printf "#%010.5f#%E#%g\n", 21.43527923, 21.43527923, 21.43527923 }'
#0021.43528#2.143528E+01#21.4353

$ echo | awk '{ printf "#%10.5f#%E#%g\n", 2140000, 2140000, 2140000 }'
#2140000.00000#2.140000E+06#2.14e+06
Practique un poco investigando con más detalle el funcionamiento de estos formatos.

4.14.8 Uso de variables operadores y expresiones

En 'awk' podemos usar toda clase de expresiones presentes en cualquier lenguaje. Cualquier identificador que no corresponda con una palabra reservada se asumirá que es una variable. Para asignar un valor se usa el operador '='

Vamos a editar un fichero que llamaremos 'ejemplexpr.awk' con algunas expresiones aritméticas.
{
contador = 0; # Pone a cero la variable contador
contador ++; # Incrementa en 1 la variable contador
contador +=10; # Incrementa en 10 la variable contador.
contador *=2 # Multiplica por 2 la variable contador
print contador
contador = ( 10 + 20 ) / 2 ;
print contador
contador = sqrt ( 25 ) ; # Raiz cuadrada de 25
print contador
}
Lo ejecutamos y observamos el resultado.
$ echo | awk -f ejemplexpr.awk
22
15
5

Algunas expresiones parecen inspiradas en el lenguaje C. Otras parece que han servido de inspiración para el lenguaje Perl. En realidad muchos lenguajes usan expresiones parecidas.
Vamos a resumir una serie de elementos que intervienen en las expresiones que 'awk' es capaz de manejar.

Operadores aritméticos
+ Suma
- Resta
* Multiplicación
/ División
% Módulo (resto)
^ Potenciación

Operadores de asignación.
var = expr Asignación
var ++ Incrementa la variable en una unidad
var -- Decrementa la variable en una unidad
var += expr_aritm Incrementa la variable en cierta cantidad
var -= expr_aritm Decrementa la variable en cierta cantidad
var *= expr_aritm Multiplica la variable por cierta cantidad
var /= expr_aritm Divide la variable por cierta cantidad
var %= expr_aritm Guarda en la variable el resto de su división por cierta cantidad
var ^= expr_aritm Eleva el valor de la variable en cierta cantidad

Operadores lógicos y de relación.
expr_aritm == expr_aritm Comparación de igualdad
expr_aritm != expr_aritm Comparación de desigualdad
expr_aritm < expr_aritm Comparación menor que
expr_aritm > expr_aritm Comparación mayor que
expr_aritm <= expr_aritm Comparación menor igual que
expr_aritm >= expr_aritm Comparación mayor igual que
expr_cad ~ expr_regular Se ajusta al patrón
expr_cad !~ expr_regular No se ajusta al patrón
expr_logica || expr_logica Operador lógico AND (Y)
expr_logica && expr_logica Operador lógico OR (O)
! expr_logica Operador lógico NOT (NO)

Funciones aritméticas.
atan2( y, x) Retorna el arco-tangente de y/x en radianes
cos(x) Retorna el coseno de x en radianes
exp(x) Retorna el exponencial de x (e^x)
int(x) Retorna el valor entero de x truncado la parte decimal
log(x) Retorna el logaritmo neperiano de x
rand() Retorna un valor seudo aleatorio comprendido entre 0 y 1
sin(x) Retorna el seno de x en radianes
sqrt(x) Retorna la raiz cuadrada de x
srand(x) Inicializa la semilla para generar números pseudoaleatorios

Funciones para usar con cadenas de caracteres
gsub(r, s, t) Sustituye 's' globalmente en todo 't' cada vez que se encuentre unpatrón ajustado a la expresión regular 'r'. Si no se proporciona 't'se toma $0 por defecto. Devuelve el número de sustituciones realizado.
index(cadena, subcadena) Retorna la posición de la 'subcadena' en 'cadena' (Primera posición = 1)
length(cadena) Devuelve la longitud de la 'cadena'. Tomará $0 por defecto si no seproporciona 'cadena'
split(cadena, array, sep) Parte 'cadena' en elementos de 'array' utilizando 'sep' como separador. Si no se proporciona 'sep' se usará FS. Devuelve el número de elementos del array
sub(r, s, t) Sustituye 's' en 't' la primera vez que se encuentre un patrónajustado a la expresión regular 'r'. Si no se proporciona 't' se toma $0por defecto.Devuelve 1 si tiene éxito y 0 si falla.
substr(cadena, beg, len) Devuelve una subcadena de 'cadena' que empieza en 'beg' con una longitud 'len'. Si no se proporciona longitud devuelve hasta el final de la cadenadesde 'beg'
tolower(cadena) Pasa a minúsculas
toupper(cadena) Pasa a mayúsculas

Algunas otras funciones
match(cadena, expr_reg) Indica si 'cadena' se ajusta o no a la expresión regular 'expr_reg'
system(comando)
sprintf(formato [, expr-list] ) Para obtener salida con formato.