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 sólo 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>
^
|
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 sólo coincidirá con cifras de dólares que comiencen con un '-',
nuestra rama sólo 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!
As you can see, converting data using sed isn't all that hard, as long as you
approach the problem incrementally. Don't try to do everything with a single sed
command, or all at once. Instead, gradually work your way toward the goal, and
continue to enhance your sed script until your output looks just the way you
want it to. Sed packs a lot of punch, and I hope that you've become very
familiar with its inner workings and that you'll continue to grow in your sed
mastery!
-
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.
|