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.


Sed mediante ejemplos, Parte 3

1.  Subir de nivel: Manejo de datos, al estilo de sed

Sed muscular

En mi segundo artículo acerca de sed, ofrecí ejemplos que mostraban el funcionamiento de sed, pero muy pocos de estos ejemplos realizaban algo realmente útil. En este artículo final acerca de sed es el momento de cambiar esto y poner a sed en buen uso. Mostraré algunos ejemplos excelentes que no solo mostrarán el extraordinario poder de sed, sino que harán algunas cosas realmente elegantes y prácticas. Por ejemplo, en la segunda mitad del artículo, mostraré cómo he diseñado un archivo de comandos sed que convierte un archivo .QIF del programa de finanzas Quicken de Intuit en un archivo de texto con buen formato. Antes de llegar a eso, veamos algunos ejemplos menos complicados, aunque útiles, de archivos de comandos sed.

Traducción de texto

Nuestro primer ejemplo práctico, un archivo de comandos que convierte texto de tipo UNIX a texto en el formato DOS/Windows. Como muy probablemente sabemos, el texto de DOS/Windows contiene un CR (retorno de carro) y un LF (avance de línea) al final de cada línea, mientras que el texto UNIX solamente tiene un avance de línea. Pueden presentarse ocasiones en las que necesitemos mover algún texto UNIX a un sistema Windows, y este archivo de comandos realizará los pasos necesarios por nosotros.

Listado de Código 1.1: Conversión de formato entre UNIX y Windows

$ sed -e 's/$/\r/' miunix.txt > midos.txt

Con este comando, la expresión regular '$' apuntará al final de la línea, y la '\r' le indica a sed que inserte un retorno de carro justo antes de llegar al mismo. Insertamos un retorno de carro antes de un avance de línea y conseguido, un CR/LF finaliza cada línea. Por favor, hay que considerar que '\r' únicamente será reemplazado por un CR si se usa GNU sed 3.02.80 o posterior. Si aún no se ha instalado GNU sed 3.02.80, ver mi primer artículo acerca de sed para obtener las instrucciones necesarias.

No sería capaz de enumerar todas las veces que he descargado algún archivo de comandos de ejemplo o de código en C, para toparme con que estaba en el formato DOS/Windows. Mientras que a algunos programas les trae sin cuidado manejar el formato DOS/Windows CR/LF en archivos de texto, a otros les importa y mucho -- un ejemplo a destacar sería bash que lo inicia tan pronto como encuentra un retorno de carro. El siguiente comando sed convertirá texto con el formato DOS/Windows al formato de confianza UNIX:

Listado de Código 1.2: Convertir código en C desde Windows a UNIX

$ sed -e 's/.$//' midos.txt > miunix.txt

El funcionamiento de este comando es muy simple: nuestra expresión regular a sustituir coincide con el último carácter de la línea, el cual será un retorno de carro. Lo reemplazamos con nada, dando lugar a que sea eliminado de la salida completamente. Si usamos este comando y notamos que el último carácter de cada línea ha sido eliminado, entonces habríamos especificado un archivo de texto que ya estaba en formato UNIX, ¡por lo que no sería necesario!

Invertir líneas

He aquí otra pequeña macro muy práctica. Con ella invertiremos las líneas en un archivo, de forma muy similar a lo que realiza el comando "tac" que se incluye en casi todas las distribuciones Linux. El nombre "tac" puede resultar confuso, dado que "tac" no cambia el orden de los caracteres en una línea (de derecha a izquierda), sino que cambia la posición de las líneas en un archivo (de abajo a arriba). Hacerle un tac al siguiente archivo:

Listado de Código 1.3: Archivo de muestra

foo
bar
oni

....produce la siguiente salida:

Listado de Código 1.4: Archivo resultante

oni
bar
foo

Podemos hacer exactamente lo mismo con la siguiente macro en sed:

Listado de Código 1.5: Hacer lo mismo con una macro

$ sed -e '1!G;h;$!d' avance.txt > retroceso.txt

Encontraremos este comando especialmente útil si estamos en un sistema FreeBSD, que no dispone del comando "tac". Aunque es muy práctica, es una muy buena idea saber porqué hace lo que hace. Vamos a diseccionarlo.

Explicación de la inversión

En primer lugar, esta macro contiene tres comandos sed separados por puntos y comas: '1!G', 'h' y '$!d'. Ahora es un buen momento para entender las direcciones empleadas para el primer y el tercer comando. Si el primer comando fuera '1G', la 'G' se aplicaría únicamente a la primera línea. De todas formas, hay un carácter '!' adicional -- este carácter '!' niega el direccionamiento, con lo que el comando 'G' se aplicará a todas las líneas excepto a la primera. Con respecto a la '$!d' estamos en una situación muy similar. Si el comando fuera '$d', lo aplicaría únicamente a la última línea del archivo (la dirección '$' es una forma simple de designar a la última línea). De todas formas, con el '!', '$!d' aplicará el comando 'd' a todas las líneas excepto a la última. Ahora, lo único que necesitamos es entender qué es lo que hacen los comandos.

Cuando ejecutamos nuestra macro para invertir las líneas, el primer comando que se lleva a cabo es 'h'. Este comando le indica a sed que copie los contenidos del espacio de patrones (la memoria intermedia que contiene la línea en la que estamos trabajando) al espacio de mantenimiento (una memoria intermedia temporal). Entonces se ejecuta el comando 'd', que elimina "foo" del espacio de patrones, por lo que no se imprime después de que se ejecuten todos los comandos para esta línea.

Ahora, la segunda línea. Después de leer "bar" en el espacio de patrones, se lleva a cabo el comando 'G', que añade los contenidos del espacio de mantenimiento ("foo\n") al espacio de patrones ("bar\n"), dando como resultado "bar\n\foo\n" en nuestro espacio de patrones. El comando 'h' vuelve a ponerlo en el espacio de mantenimiento como medida de seguridad y 'd' borra la línea del espacio de patrones, por lo que no se imprimirá.

Para la última línea "oni", se repiten los mismos pasos, excepto que los contenidos del espacio de patrones no se eliminan (debido al '$!' anterior a 'd') y los contenidos del espacio de patrones (tres líneas) se imprimen en stdout.

Ahora, es el momento de hacer una transformación de datos mucho más poderosa con sed.

QIF mágico con sed

Durante las últimas semanas, he estado considerando comprar Quicken para hacer balances en mis cuentas bancarias. Quicken es un buen programa de finanzas y realizaría el trabajo con resultados brillantes. Pero, después de pensar acerca de ello, decidí que podía escribir sin demasiados esfuerzos algún programa que hiciera un balance de mi talonario. Después de todo, pensé, ¡soy un desarrollador de software!

Desarrollé un pequeño programa de balance de mi talonario (usando awk) que calcula el balance manejando un archivo de texto que contenía todas mis transacciones. Después de trabajar con el mismo, lo mejoré de forma que podía tener en cuenta varias categorías de crédito y débito, tal y como Quicken podía hacer. Pero había una característica más que quería añadir. He cambiado mis cuentas recientemente a un banco que tiene una interfaz Web para manejar mis cuentas en línea. Un día me dí cuenta de que el sitio web de mi banco me permitía descargar la información de mi cuenta en formato .QIF; Acto seguido, decidí que sería muy interesante convertir esa información a formato texto.

La historia de dos formatos

Antes de que veamos el formato QIF, he aquí el formato de mi archivo talonario.txt:

Listado de Código 1.6: Mi talonario.txt

28 Aug 2000     food    -       -       Y     Supermarket             30.94
25 Aug 2000     watr    -       103     Y     Check 103               52.86

En mi archivo, todos los campos están separados por uno o más tabuladores, con una transacción por línea. Después de la fecha, el siguiente campo lista el tipo de gasto (o "-" si es un ingreso). El tercer campo lista el tipo de ingreso (o "-" si es un gasto). Después hay un campo para el número de cheque (de nuevo, con un "-" si no contiene nada), un campo de transacción realizada ("Y" o "N"), un comentario y el precio en dólares. Ahora, estamos listos para echar un vistazo al formato QIF. Cuando vi el archivo descargado con un visor de textos, esto es lo que pude observar:

Listado de Código 1.7: Ejemplo de formato QIF

!Type:Bank
D08/28/2000
T-8.15
N
PCHECKCARD SUPERMARKET
^
D08/28/2000
T-8.25
N
PCHECKCARD PUNJAB RESTAURANT
^
D08/28/2000
T-17.17
N
PCHECKCARD SUPERMARKET

Después de revisar el archivo, no fue demasiado complicado figurarse el formato -- ignorando la primera línea, el formato es como sigue:

Listado de Código 1.8: Formato del archivo

D<fecha>
T<precio de la transacción>
N<número de cheque>
P<descripción>
^
(este es el separador de campos)

Comenzar el proceso

Cuando nos ponemos manos a la obra con un proyecto de sed como este, no hay que desanimarse -- sed nos permite adecuar los datos gradualmente a su formato final. A medida que progresamos en el mismo, podemos ir depurando nuestra macro sed hasta que el resultado sea justo el que esperábamos. No es necesario obtenerlo en el primer intento.

Para comenzar, he creado un archivo denominado qiftrans.sed y he comenzado a manipular los datos:

Listado de Código 1.9: qiftrans.sed

1d
/^^/d
s/[[:cntrl:]]//g

El primer comando '1d' elimina la primera línea, y el segundo elimina todos los malditos caracteres '^' de la salida. La última línea elimina todos los caracteres de control que puedan existir en el archivo. Dado que estoy trabajando con un formato de archivos extraño, quiero evitar el riesgo de encontrar cualquier carácter de control a lo largo del archivo. Ahora es el momento de añadir mayor poder de procesamiento a esta macro tan básica:

Listado de Código 1.10: Macro básica mejorada

1d
/^^/d
s/[[:cntrl:]]//g
/^D/ {
  s/^D\(.*\)/\1\tOUTY\tINNY\t/
        s/^01/Jan/
        s/^02/Feb/
        s/^03/Mar/
        s/^04/Apr/
        s/^05/May/
        s/^06/Jun/
        s/^07/Jul/
        s/^08/Aug/
        s/^09/Sep/
        s/^10/Oct/
        s/^11/Nov/
        s/^12/Dec/
        s:^\(.*\)/\(.*\)/\(.*\):\2 \1 \3:
}

Primero añado una dirección '/^D/' para que sed únicamente comience a procesar cuando encuentra el primer carácter del campo de la fecha de un archivo QIF, 'D'. Todos los comandos entre las llaves se ejecutarán en orden tan pronto como sed lea una de esas líneas en su espacio de patrones.

La primera línea entre llaves transformará una línea como esta:

Listado de Código 1.11: Primera línea antes del cambio

D08/28/2000

en otra que será como esta:

Listado de Código 1.12: Primera línea después del cambio

08/28/2000  OUTY  INNY

Por supuesto, este formato no es perfecto por ahora, pero está bien. Depuraremos gradualmente los contenidos del espacio de patrones al seguir. Las siguientes 12 líneas realizan la labor de convertir la fecha en el formato con tres letras, además, la última línea elimina las tres barras de la fecha. Con lo cual obtenemos una línea como la siguiente:

Listado de Código 1.13: Aspecto final de la línea

Aug 28 2000  OUTY  INNY

Los campos OUTY e INNY nos sirven como ubicaciones que se reemplazarán más tarde. No puedo especificarlos ahora, porque si la cantidad de dólares es negativa, querré indicar OUTY e INNY como "misc" y "-", mientras que si la cantidad de dólares es positiva, querré indicarlos como "-" e "inco" respectivamente. Dado que no se ha tenido acceso a la cantidad de dólares todavía, prefiero usar ubicadores por ahora.

Depuración

Es el momento para depurarlo aún más:

Listado de Código 1.14: Mayor depuración

1d
/^^/d
s/[[:cntrl:]]//g
/^D/ {
        s/^D\(.*\)/\1\tOUTY\tINNY\t/
        s/^01/Jan/
        s/^02/Feb/
        s/^03/Mar/
        s/^04/Apr/
        s/^05/May/
        s/^06/Jun/
        s/^07/Jul/
        s/^08/Aug/
        s/^09/Sep/
        s/^10/Oct/
        s/^11/Nov/
        s/^12/Dec/
        s:^\(.*\)/\(.*\)/\(.*\):\2 \1 \3:
        N
        N
        N
        s/\nT\(.*\)\nN\(.*\)\nP\(.*\)/NUM\2NUM\t\tY\t\t\3\tAMT\1AMT/
        s/NUMNUM/-/
        s/NUM\([0-9]*\)NUM/\1/
        s/\([0-9]\),/\1/
}

Las siguientes siete líneas son un poco complicadas, por lo que las cubriremos en detalle. Primero, encontramos tres comandos 'N' en una columna. El comando 'N' le indica a sed que lea la siguiente línea de la entrada y que la añada al espacio de patrones actual. Los tres comandos 'N' dan lugar a que las tres siguientes líneas se añadan a la memoria intermedia de nuestro espacio de patrones actual, y ahora nuestra línea será como esta:

Listado de Código 1.15: Nuevo aspecto de nuestras líneas

28 Aug 2000  OUTY  INNY  \nT-8.15\nN\nPCHECKCARD SUPERMARKET

El espacio de patrones de sed se ha deteriorado -- necesitamos eliminar las nuevas líneas adicionales y hacer cambios de formato adicionales. Para lograrlo usaremos el comando de sustitución. El patrón que deseamos encontrar es el siguiente:

Listado de Código 1.16: Eliminar líneas adicionales y aplicar un nuevo formato

'\nT.*\nN.*\nP.*'

Con ello apuntaremos a una nueva línea, seguida por una 'T', seguida de cero o más caracteres, seguida por otra nueva línea, seguida por una 'N' con cualquier número de caracteres, seguida por una 'P', seguida de cualquier número de caracteres. Esta expresión regular coincidirá con las tres líneas que acabamos de añadir al espacio de patrones. Pero queremos darle otro formato a esta región, no reemplazarla por completo. La cifra en dólares, el número de cheque (en caso de haberlo) y la descripción deben reaparecer en nuestra cadena de reemplazo. Para hacerlo, subyugamos estas "partes interesantes" con paréntesis de barra invertida, por lo que podemos hacer alusión a los mismos en nuestra cadena de reemplazo (usando '\1', '\2\', y '\3' para indicar a sed dónde insertarlos). Este es el comando final:

Listado de Código 1.17: Comando final

s/\nT\(.*\)\nN\(.*\)\nP\(.*\)/NUM\2NUM\t\tY\t\t\3\tAMT\1AMT/

Este comando transforma nuestra línea en:

Listado de Código 1.18: Resultado del comando anterior

28 Aug 2000  OUTY  INNY  NUMNUM    Y     CHECKCARD SUPERMARKET AMT-8.15AMT

A pesar de que la línea está mejorando, hay algunas cosas que parecen a primera vista... interesantes. La primera es la estúpida cadena "NUMNUM" -- ¿para qué sirve? Lo veremos en las siguientes dos líneas de la macro sed, que reemplazarán "NUMNUM" con un "-", mientras que "NUM"<number>"NUM" será reemplazado con <number>. Como puede verse, subordinar el número de cheque con una estúpida etiqueta nos permite insertar un "-" si el campo está vacío.

Retoques finales

La última línea elimina un punto decimal a continuación de un número. Con lo cual convertimos cifras en dólares como "3,231.00" en "3231.00", que es el formato que empleo. Ha llegado el momento de echar un vistazo a la macro final:

Listado de Código 1.19: La macro final

1d
/^^/d
s/[[:cntrl:]]//g
/^D/ {
  s/^D\(.*\)/\1\tOUTY\tINNY\t/
  s/^01/Jan/
  s/^02/Feb/
  s/^03/Mar/
  s/^04/Apr/
  s/^05/May/
  s/^06/Jun/
  s/^07/Jul/
  s/^08/Aug/
  s/^09/Sep/
  s/^10/Oct/
  s/^11/Nov/
  s/^12/Dec/
  s:^\(.*\)/\(.*\)/\(.*\):\2 \1 \3:
  N
  N
  N
  s/\nT\(.*\)\nN\(.*\)\nP\(.*\)/NUM\2NUM\t\tY\t\t\3\tAMT\1AMT/
  s/NUMNUM/-/
  s/NUM\([0-9]*\)NUM/\1/
  s/\([0-9]\),/\1/
  /AMT-[0-9]*.[0-9]*AMT/b fixnegs
  s/AMT\(.*\)AMT/\1/
  s/OUTY/-/
  s/INNY/inco/
  b done
:fixnegs
  s/AMT-\(.*\)AMT/\1/
  s/OUTY/misc/
  s/INNY/-/
:done
}

Las siguientes once líneas adicionales usan la sustitución y alguna funcionalidad de ramificación para perfeccionar la salida. Primero, deberíamos observar esta línea:

Listado de Código 1.20: Primera línea en la que merece la pena fijarse

        /AMT-[0-9]*.[0-9]*AMT/b fixnegs

Esta línea contiene un comando ramificado, que es de la forma "/expreg/b etiqueta". Si se encuentra en el espacio de patrones la expreg, sed ramificará la etiqueta fixnegs. Es muy sencillo encontrar esta etiqueta, que aparece en el código como ":fixnegs". Si la expreg no coincide, continúa con normalidad y sigue procesando el siguiente comando.

Ahora que entendemos cómo funciona el comando, vamos a ver las ramas. Si observamos la expresión regular ramificada, vemos que coincidirá con la cadena 'AMT', seguida de un '-', seguida de cualquier número de dígitos y 'AMT'. Como es fácil figurarse, la expreg trata de forma especial las cantidades de dólares negativas. Anteriormente subordinamos las cantidades de dólares con cadenas 'AMT' para que pudiésemos encontrarlas fácilmente después. Dado que la expreg solo coincidirá con cifras de dólares que comiencen con un '-', nuestra rama solo aparecerá cuando estemos manejando débitos. Si estamos manejando un débito OUTY debe quedar como 'misc', INNY debe quedar como '-' y debe quitar el signo negativo delante de la cantidad del débito. Si seguimos el código, veremos que eso es lo que hace exactamente. Si el ramal no se ejecuta, OUTY será reemplazado con un '-', e INNY se reemplazará por 'inco'. ¡Hemos terminado! La línea resultante es perfecta ahora:

Listado de Código 1.21: Línea resultante perfecta

28 Aug 2000  misc  -  -       Y     CHECKCARD SUPERMARKET  -8.15

No hay que confundirse

Como se ha visto, convertir datos usando sed no es tan complicado, siempre y cuando afrontemos el problema de forma incremental. No hay que tratar de hacer todo con un único comando sed, o todo a la primera. En lugar de esto, hay que trabajar gradualmente hasta que logremos nuestro objetivo, y continuaremos mejorando nuestra macro sed hasta que el resultado sea exactamente el que deseábamos. Sed contiene muchos recursos, espero que pronto nos familiaricemos con sed y con su funcionamiento interno y ¡que continuemos creciendo en nuestro dominio de sed! Como puede comprobar, la conversión de datos usando sed no es muy complicada mientras trate el problema de forma incremental. No trate de hacerlo todo a la vez o con un solo comando sed. En lugar de esto, trabaje gradualmente en su camino a la meta y continue mejorando su guión sed hasta que la salida tenga el aspecto que desee. Sed tiene un gran potencial, y ¡espero que continúe progresando en su domino de sed!

  • Leer los otros artículos acerca de sed de Daniel en developerWorks: "Sed mediante ejemplos, Parte 1 y Parte 2.
  • Comprobar la excelente FAQ de sed de Eric Pement.
  • Puede encontrar el código fuente de sed en ftp://ftp.gnu.org/pub/gnu/sed.
  • Eric Pement tiene también una lista muy práctica de sed one-liners que cualquier aspirante a gurú sed debería consultar.


Imprimir

Página actualizada 14 de febrero, 2008

Sumario: En esta parte final de la serie acerca de sed, Daniel Robbins le ofrece una verdadera muestra de la potencia de sed. Después de presentar un puñado de macros sed esenciales, le demostrará algo de guiones sed radicales convirtiendo un fichero .QIF de Quicken a un formato de texto legible. Esta macro de conversión no es solo funcional, también sirve como un excelente ejemplo de la potencia de los guiones escritos en sed.

Daniel Robbins
Autor

LinuxBlues
Traductor

José María Alonso
Traductor

Donate to support our development efforts.

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