<?xml version='1.0' encoding="utf-8"?>

<!-- $Header: /var/cvsroot/gentoo/xml/htdocs/doc/en/articles/l-awk3.xml,v 1.6 2005/10/31 13:47:46 neysx Exp $ -->
<!DOCTYPE guide SYSTEM "/dtd/guide.dtd">

<guide link="/doc/es/articles/l-awk3.xml" disclaimer="articles" lang="es">
<title>Awk mediante ejemplos, parte 3</title>

<author title="Autor">
  <mail link="drobbins@gentoo.org">Daniel Robbins</mail>
</author>
<author title="Traductor">
  <mail link="i92guboj@terra.es">Jesús Guerrero</mail>
</author>

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



<!-- The original version of this article was published on IBM developerWorks,

and is property of Westtech Information Services. This document is an updated

version of the original article, and contains various improvements made by the

Gentoo Linux Documentation team -->
<version>1.4</version>
<date>2005-10-31</date>

<chapter>
<title>¿Funciones de cadena y... libros de cuentas?</title>
<section>
<title>Formateando la salida</title>
<body>

<p>
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.
</p>

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

<pre caption="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
</pre>

<p>
Este código imprimirá:
</p>

<pre caption="Salida del código">
Jim obtuvo 83 en el último test
foo-1
</pre>

</body>
</section>
<section>
<title>Funciones de cadena</title>
<body>

<p>
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:
</p>

<pre caption="Código de ejemplo">
mystring="¿Cómo estás hoy?"
print mystring[3]
</pre>

<p>
Recibiremos un error como éste:
</p>

<pre caption="Error del código de ejemplo">
awk: string.gawk:59: fatal: se intentó usar un escalar como una matriz
</pre>

<p>
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.
</p>

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

<pre caption="Ejemplo de uso para la función length()">
print length(mystring)
</pre>

<p>
Este código imprimirá la longitud de la cadena:
</p>

<pre caption="Valor impreso">
16
</pre>

<p>
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:
</p>

<pre caption="Ejemplo de uso para la función index()">
print index(mystring,"hoy")
</pre>

<p>
Awk imprime:
</p>

<pre caption="Salida de la función">
13
</pre>

<p>
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:
</p>

<pre caption="Convirtiendo cadenas a mayúsculas o minúsculas">
print tolower(mystring)
print toupper(mystring)
print mystring
</pre>

<p>
....producirá esta salida:
</p>

<pre caption="Salida">
¿cómo estás hoy?
¿CÓMO ESTÁS HOY?
¿Cómo estás hoy?
</pre>

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

<pre caption="Ejemplo de uso para la función substr()">
mysub=substr(mystring,startpos,maxlen)
</pre>

<p>
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 caracter 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:
</p>

<pre caption="Otro ejemplo">
print substr(mystring,13,3)
</pre>

<p>
Awk imprimirá:
</p>

<pre caption="Lo que awk imprimirá">
hoy
</pre>

<p>
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.
</p>

<p>
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():
</p>

<pre caption="Ejemplo de llamada a match()">
print match(mystring,/hoy/), RSTART, RLENGTH
</pre>

<p>
Awk imprimirá:
</p>

<pre caption="Salida de la función de arriba">
13 13 3
</pre>

</body>
</section>
<section>
<title>Sustitución de cadenas</title>
<body>

<p>
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():
</p>

<pre caption="Modelo para la función sub()">
sub(regexp,replstring,mystring)
</pre>

<p>
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():
</p>

<pre caption="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
</pre>

<p>
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:
</p>

<pre caption="Salida de awk">
¿CómO estás hoy?
¿CómO estás hOy?

</pre>

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

<p>
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():
</p>

<pre caption="Ejemplo de llamada a split()">
numelements=split("Ene,Feb,Mar,Abr,May,Jun,Jul,Ago,Sep,Oct,Nov,Dic",mymonths,",")
</pre>

<p>
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:
</p>

<pre caption="Código de ejemplo">
print mymonths[1],mymonths[numelements]
</pre>

<p>
....imprimirá:
</p>

<pre caption="Salida ejemplo">
Ene Dic
</pre>

</body>
</section>
<section>
<title>Formatos especiales de cadena</title>
<body>

<p>
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 script awk:
</p>

<pre caption="Código que imprime la longitud de cada línea de un fichero">
{
    print length()
}
</pre>

</body>
</section>
<section>
<title>Diversión financiera</title>
<body>

<p>
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 script 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":
</p>

<pre caption="Libro de chequeos ASCII para registrar transacciones">

23 Aug 2000    food    -    -    Y    Jimmy's Buffet    30.25
</pre>

<p>
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:
</p>

<pre caption="Entrada de depósito ejemplo">

23 Aug 2000    -    inco    -    Y    Boss Man        2001.00
</pre>

<p>
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.
</p>

<p>
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.
</p>

</body>
</section>
<section>
<title>El código</title>
<body>

<p>
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:
</p>

<pre caption="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
}
</pre>

<p>
Añadiendo la primera línea "#!..." a cualquier script awk, le permitirá ser
ejecutado directamente desde la shell, suponiendo que has realizado "chmod
+x myscript" 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.
</p>

<p>
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:
</p>

<pre caption="Ejemplo de llamada a monthdigit()">
print monthdigit("Mar")
</pre>

<p>
....imprimirá esto:
</p>

<pre caption="Ejemplo de salida de monthdigit()">
3
</pre>

<p>
Ahora, veamos algunas funciones más.
</p>

</body>
</section>
<section>
<title>Funciones financieras</title>
<body>

<p>
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.
</p>

<p>
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"].
</p>

<pre caption="Encontrando 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
}
</pre>

<p>
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.
</p>

<p>
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.
</p>

</body>
</section>
<section>
<title>El bloque principal</title>
<body>

<p>
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.
</p>

<pre caption="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)
    }
}
</pre>

<p>
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)\.
</p>

<p>
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.
</p>

<p>
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, sólo 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 coindir 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).
</p>

</body>
</section>
<section>
<title>Generando el informe</title>
<body>

<p>
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:
</p>

<pre caption="Generando 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)
}
</pre>

<p>
Este informe muestra un resumen que tiene este aspecto:
</p>

<pre caption="Informe final">
Tus fondos a la fecha:    1174.22
El saldo de tu cuenta:    2399.33
</pre>

<p>
En nuestro bloque END, usamos las constucció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 <path>mycheckbook.txt</path>, pon todo ese
código en un fichero de texto llamado <path>balance</path> y haz un <c>chmod
+x balance</c>, y entonces escribe "<c>./balance mycheckbook.txt</c>". El
script balance sumará todas tus transacciones e imprimirá un resumen de
saldos de dos líneas.
</p>

</body>
</section>
<section>
<title>Mejoras</title>
<body>

<p>
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 script. 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!.
</p>

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

</body>
</section>
</chapter>

<chapter>
<title>Recursos</title>
<section>
<body>

<ul>
  <li>
    Lee otros artículos de Daniel sobre awk publicados en developerWorks: Awk
mediante ejemplos, <uri link="l-awk1.xml">Parte 1</uri> y <uri
link="l-awk2.xml">Parte 2</uri>.
  </li>
  <li>
    Si prefieres un buen libro a la vieja usanza, el <uri
link="http://www.oreilly.com/catalog/sed2/">Sed y awk, 2ª edición</uri> de
O'Reilly es una buena opción.
  </li>
  <li>
    Asegúrate también de examinar <uri
link="http://www.faqs.org/faqs/computer-lang/awk/faq/">comp.lang.awk
FAQ</uri>. También contiene varios enlaces sobre awk.
  </li>
  <li>
    El <uri link="http://sparky.rice.edu/~hartigan/awk.html">tutorial de
awk</uri> de Patrick Hartigan viene con varios scripts interesantes de awk.
  </li>
  <li>
    El <uri link="http://www.tasoft.com/tawk.html">Compilador TAWK de
Thompson</uri> compila guiones de awk transformándolos en binarios
ejecutables. Hay versiones para Windows, OS/2, DOS, y UNIX.
  </li>
  <li>
    <uri link="http://www.gnu.org/software/gawk/manual/gawk.html">La guía de
usuario de GNU Awk</uri> también está disponible para referencia en línea.
  </li>
</ul>

</body>
</section>
</chapter>

</guide>