<?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>