Gentoo Logo

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 3

Contenido:

1.  ¿Funciones de cadena y... libros de cuentas?

Formateando la salida

Si bien el comando print de awk es suficiente la mayoría de las veces, en ocasiones necesitaremos algo más. Para estas ocasiones awk nos ofrece dos viejos amigos, llamados printf() y sprintf(). Si, estas funciones, como muchas otras partes de awk, son idénticas a sus análogas en C. printf() imprime una cadena con formato a stdout, mientras que sprintf() devuelve una cadena formateada que puede ser asignada a una variable. Si no estás familiarizado con printf() y sprintf() cualquier texto introductorio sobre C servirá para entender estas dos funciones esenciales. Puedes ver la página man de printf() usando "man 3 printf" en cualquier sistema Linux.

Aquí tenemos un ejemplo de uso de prinft() y sprintf() en awk, como puedes ver, todo es casi idéntico a C.

Listado de Código 1.1: Ejemplos de código awk con sprintf() y printf()

x=1
b="foo"
printf("%s got a %d on the last test\n","Jim",83)
myout=("%s-%d",b,x)
print myout

Este código imprimirá:

Listado de Código 1.2: Salida del código

Jim obtuvo 83 en el último test
foo-1

Funciones de cadena

Awk tiene una gran cantidad de funciones de cadena, y eso es algo bueno. En awk son muy necesarias ya que no podemos tratar una cadena como una matriz de caracteres, tal y como hacemos en otros lenguajes, como C, C++ y Python. Por ejemplo, si ejecutamos el código siguiente:

Listado de Código 1.3: Código de ejemplo

mystring="¿Cómo estás hoy?"
print mystring[3]

Recibiremos un error como éste:

Listado de Código 1.4: Error del código de ejemplo

awk: string.gawk:59: fatal: se intentó usar un escalar como una matriz

Bueno, quizás no tan prácticos como los tipos secuenciales de Python, pero las funciones de cadena hacen bien su trabajo. Echémosles un vistazo.

Primero, tenemos la función básica length(), que devuelve la longitud de una cadena. Se usa así:

Listado de Código 1.5: Ejemplo de uso para la función length()

print length(mystring)

Este código imprimirá la longitud de la cadena:

Listado de Código 1.6: Valor impreso

16

Bien, sigamos. La siguiente función se llama index, y devuelve la posición de la ocurrencia de una determinada cadena dentro de otra cadena, o bien 0 si la primera cadena no se encuentra dentro de la otra. Se usa de esta manera:

Listado de Código 1.7: Ejemplo de uso para la función index()

print index(mystring,"hoy")

Awk imprime:

Listado de Código 1.8: Salida de la función

13

Vamos a por dos funciones más sencillas: tolower() y toupper(). Como quizás hayas adivinado, estas funciones retornan la misma cadena, pero con todos los caracteres convertidos a minúsculas o mayúsculas respectivamente. tolower() y toupper() devuelven una nueva cadena, no modifican la original. Este código:

Listado de Código 1.9: Convertir cadenas a mayúsculas o minúsculas

print tolower(mystring)
print toupper(mystring)
print mystring

....producirá esta salida:

Listado de Código 1.10: Salida

¿cómo estás hoy?
¿CÓMO ESTÁS HOY?
¿Cómo estás hoy?

Hasta aquí todo bien. Pero ¿cómo haremos para seleccionar una subcadena o incluso un solo carácter en una cadena ya existente? Aquí es donde substr() entra en juego. Así es como se usa substr():

Listado de Código 1.11: Ejemplo de uso para la función substr()

mysub=substr(mystring,startpos,maxlen)

mystring tiene que ser una variable de cadena, o una cadena literal, de la que se pretende extraer una subcadena. startpos debe ser el número de posición del carácter desde el que se quiere extraer, y maxlen debe contener la longitud máxima de la cadena que se va a extraer. Nótese que hablo de longitud máxima, si length(mystring) es más corto que startpos+maxlen, entonces el resultado será truncado. substr() no modificará la cadena original, sino que devuelve una nueva cadena. Aquí tenemos un ejemplo:

Listado de Código 1.12: Otro ejemplo

print substr(mystring,13,3)

Awk imprimirá:

Listado de Código 1.13: Lo que awk imprimirá

hoy

Si programas regularmente en un lenguaje que use índices de matriz para acceder a partes de una cadena (¿y quién no lo hace?), toma nota menta de que substr() es tu substituto para awk. Lo necesitarás para extraer caracteres y subcadenas; como awk es un lenguaje basado en cadenas, lo usarás bastante frecuentemente.

Ahora vamos a por algo más jugoso. La primera es match(). match() se parece bastante a index(), excepto que en lugar de buscar una subcadena, como hace index(), busca una expresión regular. La función match() devolverá la posición de inicio de la coincidencia, o cero si no se encuentra ninguna. Adicionalmente, match() definirá dos variables llamadas RSTART y RLENGTH. RSTART contiene el valor de retorno (el valor de la primera coincidencia), y RLENGTH especifica su extensión en caracteres (o -1 si no hubo coincidencia). Usando RSTART, RLENGTH, substr(), y un pequeño bucle, se puede iterar fácilmente a través de cada coincidencia en una cadena. Aquí hay un ejemplo de llamada a match():

Listado de Código 1.14: Ejemplo de llamada a match()

print match(mystring,/hoy/), RSTART, RLENGTH

Awk imprimirá:

Listado de Código 1.15: Salida de la función de arriba

13 13 3

Sustitución de cadenas

Ahora veremos un par de funciones para la sustitución de cadenas: sub() y gsub(). Éstas difieren un poco de las demás funciones que hemos visto, en el sentido de que modifican la cadena original. Aquí hay un modelo que nos enseña como llamar a sub():

Listado de Código 1.16: Modelo para la función sub()

sub(regexp,replstring,mystring)

Cuándo llamas a sub(), se buscará la primera secuencia de caracteres en mystring que concuerda con regexp y se reemplazará esa secuencia con replstring. sub() y gsub() tienen idénticos argumentos; en lo único en lo que se diferencian es en que sub() reemplazará la primera ocurrencia de regexp (si hay alguna), y gsub() realizará un reemplazamiento global, cambiando todas las ocurrencias en la cadena. Aquí hay un ejemplo de a sub() y gsub():

Listado de Código 1.17: Ejemplo de llamadas a las funciones sub() y gsub()

sub(/o/,"O",mystring)
print mystring
mystring="¿Cómo estas hoy?"
gsub(/o/,"O",mystring)
print mystring

Tenemos que devolver a mystring a su valor original ya que la primera llamada a sub() modifica mystring directamente. Cuando lo ejecutamos, este código hará que awk muestre la siguiente salida:

Listado de Código 1.18: Salida de awk

¿CómO estás hoy?
¿CómO estás hOy?

Desde luego, es posible escribir expresiones regulares más complicadas . Lo dejaré para que pruebes regexps más complicadas.

Terminamos con el repaso de nuestras funciones de cadena presentándote una función llamada split(). El trabajo de split() es "trocear" una cadena y colocar las distintas partes en una matriz indexada por enteros. Aquí se muestra un ejemplo de llamada a split():

Listado de Código 1.19: Ejemplo de llamada a split()

numelements=split("Ene,Feb,Mar,Abr,May,Jun,Jul,Ago,Sep,Oct,Nov,Dic",mymonths,",")

Cuando llamamos a split(), el primer argumento contiene la cadena literal o la variable cadena a ser troceada. En el segundo argumento, debes especificar el nombre de la matriz en la que split() insertará las partes troceadas. En el tercer elemento, especifica el separador que será usado para trocear las cadenas. Cuando split() retorna, devolverá el número de elementos de la cadena que fueron troceados. split() asigna cada uno a un índice de la matriz comenzando por uno, por lo tanto el siguiente código:

Listado de Código 1.20: Código de ejemplo

print mymonths[1],mymonths[numelements]

....imprimirá:

Listado de Código 1.21: Salida ejemplo

Ene Dic

Formatos especiales de cadena

Una nota rápida -- cuando se llama a length(), sub() o gsub() puedes omitir el último argumento y awk simplemente aplicará la llamada de la función a $0 (la línea actual completa). Para imprimir la longitud de cada línea de un fichero, usa este guión awk:

Listado de Código 1.22: Código que imprime la longitud de cada línea de un fichero

{
    print length()
}

Diversión financiera

Hace unas semanas, decidí escribir mi propio programa de balance de cuentas en awk. Decidí que me gustaría tener un simple fichero de texto delimitado por tabuladores en el cual pudiera introducir mis depósitos y retiradas más recientes. La idea era manejar estos datos con un guión awk que pudiera añadir automáticamente todas las cantidades e indicarme el saldo. Aquí se muestra cómo decidí registrar todas mis transacciones en mi "libro de chequeos ASCII":

Listado de Código 1.23: Libro de chequeos ASCII para registrar transacciones


23 Aug 2000    food    -    -    Y    Jimmy's Buffet    30.25

Cada campo en este fichero esta separado por uno o más tabuladores. Después del campo fecha (campo 1, $1) hay dos campos llamados "categoría de gastos" y "categoría de ingresos". Cuando introduzco una retirada como la línea de arriba, pongo un código de cuatro letras en el campo exp y un "-" (entrada en blanco) en el campo inc. Esto significa que este elemento en particular es un "gasto en comida" :) Aquí se muestra el aspecto de un depósito:

Listado de Código 1.24: Entrada de depósito ejemplo


23 Aug 2000    -    inco    -    Y    Boss Man        2001.00

En este caso, pongo un "-" (blanco) en la categoría exp e "inco" en la categoría inc. "inco" es mi código para el ingreso genérico (estilo pago por cheque). El uso de códigos para las categorías me permite generar un desglose de categorías de ingresos y gastos. Para todos los registros los campos se describen por si solos. El campo ¿permitido? ("Y" o "N") registra si la transacción ha sido cargada en mi cuenta; le siguen una descripción de la transacción y una cantidad positiva en dólares.

El algoritmo usado para calcular el saldo actual no es muy complicado. Awk simplemente necesita leer cada línea, una por una. Si una categoría de gastos es listada pero no hay categoría de ingreso (es "-"), entonces la cantidad en dólares es un crédito. Y si se listan categorías tanto de gasto como de ingreso, entonces esta cantidad es una "categoría de transferencia"; esto es, la cantidad en dólares será restada de la categoría de gastos y añadida a la categoría de ingresos, De nuevo, todas estas categorías son virtuales pero son muy útiles para seguir los ingresos y gastos, y también para realizar presupuestos.

El código

Es el momento de echar un vistazo al código. Comenzaremos por la primera línea, el bloque BEGIN y la definición de funciones:

Listado de Código 1.25: Saldo, parte 1

#!/usr/bin/env awk -f
BEGIN {
    FS="\t+"
    months="Ene,Feb,Mar,Abr,May,Jun,Jul,Ago,Sep,Oct,Nov,Dic"
}

function monthdigit(mymonth) {
    return (index(months,mymonth)+3)/4
}

Añadiendo la primera línea "#!..." a cualquier guión awk, le permitirá ser ejecutado directamente desde la shell, suponiendo que has realizado "chmod +x miguión" previamente. El resto de las líneas, definen nuestro bloque BEGIN, el cual se ejecuta antes de que awk comience el procesamiento de nuestro fichero con el libro de apuntes. Ponemos FS (el separador de campos) a "\t+", lo que le dice a awk que los campos serán separados por uno o más tabuladores. Además, definimos una cadena llamada months que es usada por nuestra función monthdigit(), que aparece a continuación.

Las últimas tres líneas muestran cómo definir nuestra propia función awk. El formato es simple -- escribe "function" a continuación el nombre de la función, y luego los parámetros separados por comas, dentro de paréntesis. después de esto, un bloque de código "{ }" contiene el código que te gustaría que ejecutara esta función. Todas las funciones puede acceder variables globales (como nuestra variable months). Además, awk ofrece una sentencia "return" que permite a la función devolver un valor y de esta forma operar de manera similar al "return" que encontramos en C, Python y otros lenguajes. Esta función en particular convierte un nombre de mes expresado en formato de cadena de 3 letras a su equivalente numérico. Por ejemplo, esto:

Listado de Código 1.26: Ejemplo de llamada a monthdigit()

print monthdigit("Mar")

....imprimirá esto:

Listado de Código 1.27: Ejemplo de salida de monthdigit()

3

Ahora, veamos algunas funciones más.

Funciones financieras

Aquí tenemos tres funciones más que realizan la custodia del libro por nosotros. Nuestro bloque principal de código, que veremos en breve, procesará cada línea del fichero que contiene el libro de apuntes en secuencia, llamando a cada una de estas funciones de forma que las transacciones sean registradas en una matriz de awk. Hay tres tipos básicos de transacciones: crédito (doincome), débito (doexpense) y transferencia (dotransfer). Notarás que las tres funciones aceptan un único argumento llamado mybalance. El argumento mybalance es un contenedor de una matriz bidimensional que pasaremos como argumento. Hasta ahora no hemos tratado con matrices bidimensionales; sin embargo, como puedes ver abajo, la sintaxis es bastante simple. Separa cada dimensión con una coma y ya estás en ello.

Registraremos información en "mybalance" de la siguiente forma: La primera dimensión de la matriz varía entre 0 y 12 y especifica el mes, o cero para el año completo. Nuestra segunda dimensión es una categoría de cuatro letras, como "food" o "inco"; esta es la categoría con la que estamos tratando. Por lo tanto, para encontrar el saldo completo del año para la categoría food, mirarás en mybalance[0,"food"]. Para encontrar los ingresos de junio, mirarás en mybalance[6,"inco"].

Listado de Código 1.28: Encontrar la información sobre los ingresos

function doincome(mybalance) {
    mybalance[curmonth,$3] += amount
    mybalance[0,$3] += amount
}

function doexpense(mybalance) {
    mybalance[curmonth,$2] -= amount
    mybalance[0,$2] -= amount
}

function dotransfer(mybalance) {
    mybalance[0,$2] -= amount
    mybalance[curmonth,$2] -= amount
    mybalance[0,$3] += amount
    mybalance[curmonth,$3] += amount
}

Cuando invocamos a doincome() o a cualquiera de las otras funciones, almacenamos la transacción en dos lugares -- mybalance[0,category] y mybalance[curmonth,category], el saldo del año completo y el saldo de la categoría en el mes actual respectivamente. Esto nos permitirá más adelante generar fácilmente un desglose anual o mensual de ingresos/gastos.

Si le echas un vistazo a estas funciones, te darás cuenta de que la matriz referenciada por mybalance es pasada como referencia. Además, también nos referimos a algunas variables globales: currmonth, que almacena el valor numérico del mes del registro actual, $2 (la categoría de gastos), $3 (la categoría de ingresos), y amount ($7, la cantidad en dólares). Cuando se invoca doincome() y sus amigas, todas estas variables ya han sido asignadas correctamente para el registro actual (línea) procesado en ese momento.

El bloque principal

Aquí se muestra, el bloque de código principal que contiene la parte que analiza cada línea de entrada de datos. Recuerda, ya que hemos asignado FS correctamente, podemos referirnos al primer campo como $1, al segundo como $2, etc. Cuando llamamos a doincome() y a sus amigas, las funciones pueden acceder a los valores actuales de curmonth, $2, $3 y amount desde dentro de la propia función. Echa un vistazo al código y nos veremos al otro lado para la explicación.

Listado de Código 1.29: Saldo, parte 3

{
    curmonth=monthdigit(substr($1,4,3))
    amount=$7

    #registra todas las categorías que encuentra
    if ( $2 != "-" )
        globcat[$2]="yes"
    if ( $3 != "-" )
        globcat[$3]="yes"

    #tally up the transaction properly
    if ( $2 == "-" ) {
        if ( $3 == "-" ) {
            print "Error: ¡Ambos campos inc y exp están vacíos!"
            exit 1
        } else {
            #esto es un ingreso
            doincome(balance)
            if ( $5 == "Y" )
                doincome(balance2)
        }
    } else if ( $3 == "-" ) {
        #esto es un gasto
        doexpense(balance)
        if ( $5 == "Y" )
            doexpense(balance2)
    } else {
        #esto es una transferencia
        dotransfer(balance)
        if ( $5 == "Y" )
            dotransfer(balance2)
    }
}

En el bloque principal, las primeras dos líneas asignan un entero entre 1 y 12 a curmonth, asignan a amount el campo 7 (para hacer el código más fácil de comprender). A continuación tenemos cuatro líneas interesantes en las que escribimos valores en una matriz llamada globcat. globcat o la matriz de categorías globales es usado para registrar todas las categorías encontradas en el fichero -- "inco", "misc", "food", "util", etc. Por ejemplo, si $2 == "inco", actualizamos globcat["inco"] a "yes". Más adelante podemos iterar a través de nuestra lista de categorías con un simple bucle "for (x in globcat)\.

En las veinte líneas siguientes, analizamos los campos $2 y $3, y registramos la transacción de forma adecuada. Si $2=="-" y $3!="-", tenemos algún ingreso por lo que llamamos a doincome(). Si la situación es la opuesta, llamamos a doexpense(); y si tanto $2 como $3 contienen categorías, llamamos a dotransfer(). En cada ocasión pasamos la matriz de "balance" a estas funciones de forma que los datos son registrados allí de forma apropiada.

También te darás cuenta de que varias líneas que dicen "if ( $5 == "Y" ), registran la misma transacción en balance2. Recordarás que $5 contiene un "Y" o un "N", y registra si la transacción ha sido realizada en cuenta. Ya que registramos la transacción en balance2, solo si la transacción ha sido realizada en cuenta, balance2 contendrá el saldo actual de la cuenta, mientras que "balance" contendrá todas las transacciones independientemente de si han sido realizadas o no. Puedes usar balance2 para verificar tus entradas de datos (ya que debería coincidir con el saldo actual de tu cuenta reportado por tu banco, y usar "balance" para asegurarse de que no tu cuenta no tiene descubierto (ya que tomará en cuenta todos los cheques que has emitido y que no han sido cargados).

Generando el informe

Después del bloque principal se procesa de forma repetida cada registro de entrada, ahora tenemos un registro extenso de débitos y créditos clasificados por categoría y mes. Todo lo que necesitamos hacer es definir un bloque END que generará un informe, en este caso uno modesto:

Listado de Código 1.30: Generar el informe final

END {
    bal=0
    bal2=0
    for (x in globcat) {
        bal=bal+balance[0,x]
        bal2=bal2+balance2[0,x]
    }
    printf("Tus fondos actuales: %10.2f\n", bal)
    printf("El saldo de tu cuenta: %10.2f\n", bal2)
}

Este informe muestra un resumen que tiene este aspecto:

Listado de Código 1.31: Informe final

Tus fondos a la fecha:    1174.22
El saldo de tu cuenta:    2399.33

En nuestro bloque END, usamos las construcción "for (x in globcat)" para iterar a través de cada categoría, cuadrando un saldo principal basado en todas las transacciones registradas. Nosotros realmente cuadramos dos saldos, uno para los fondos disponibles, y otro para el saldo de la cuenta. Para ejecutar el programa y procesar tus bienes financieros que has introducido en un fichero llamado mycheckbook.txt, pon todo ese código en un fichero de texto llamado balance y haz un chmod +x balance, y entonces escribe "./balance mycheckbook.txt". El guión balance sumará todas tus transacciones e imprimirá un resumen de saldos de dos líneas.

Mejoras

Yo uso una versión más avanzada de este programa para gestionar mis finanzas personales y empresariales. Mi versión (que no pude incluir aquí debido a restricciones de espacio), imprime un desglose mensual de ingresos y gastos, incluyendo totales anuales, ingresos netos y un puñado de más cosas. Aún mejor, imprime los datos en formato HTML, de modo que puedo verlo en un navegador web :). Si piensas que este programa es útil, te animo a que le añadas estas características al guión. No necesitarás configurarlo para registrar ninguna información adicional; toda la información que necesitas está en balance y balance2. Simplemente mejora el bloque END, y ¡ya estás metido en ello!.

Espero que hayas disfrutado de esta serie. Para más información sobre awk, echa un vistazo a los recursos listados abajo.

2.  Recursos



Imprimir

Página actualizada 31 de octubre, 2005

Sumario: Para la conclusión de la serie sobre awk, Daniel nos presenta algunas funciones de cadena importantes, para más tarde construir un programa de balance de cuentas desde cero. De camino aprenderás como escribir tus propias funciones en awk, y también aprenderás a usar matrices multidimensionales. Para el final de este artículo tendrás más experiencia con awk, lo cual te permitirá crear guiones más potentes.

Daniel Robbins
Autor

Jesús Guerrero
Traductor

Donate to support our development efforts.

Copyright 2001-2014 Gentoo Foundation, Inc. Questions, Comments? Contact us.