lunes, 15 de agosto de 2016

sed

El nombre sed es una abreviatura de stream editor (editor de flujo). Realiza edición de texto en una secuencia de texto, ya sea una serie de archivos especificados o en la entrada estándar. sed es un programa potente y algo complejo (hay libros enteros sobre él), así que no lo veremos completamente aquí.

En general, la forma en que funciona sed es que se le da un comando simple de edición (en la línea de comandos) o el nombre de un archivo de script que contenga múltiples comandos, y ejecuta estos comandos sobre cada línea de la secuencia de texto. Aquí tenemos un ejemplo muy simple de sed en acción:

[me@linuxbox ~]$ echo "front" | sed 's/front/back/'
back

En este ejemplo, producimos una secuencia de texto de una palabra usando echo y lo canalizamos a sed. sed, a su vez, ejecuta la instrucción s/front/back/ en el texto de la secuencia y produce la salida "back" como resultado. También podemos considerar este comando de forma parecida al comando "sustitución" (buscar y reemplazar) de vi.

Los comandos en sed comienzan con una sola letra. En el ejemplo anterior, el comando sustitución se representa con la letra s y es seguido por las cadenas de búsqueda y reemplazo, separadas por el carácter de la barra inclinada como separador. La elección del carácter separador es arbitraria. Por consenso, el carácter de la barra inclinada se usa a menudo, pero sed aceptará cualquier carácter que siga inmediatamente al comando como separador. Podríamos realizar el mismo comando de la siguiente forma:

[me@linuxbox ~]$ echo "front" | sed 's_front_back_'
back

Usar el carácter guion bajo inmediatamente tras el comando, lo convierte en el separador. La capacidad de establecer el separador puede usarse para realizar los comandos más legibles, como veremos.

La mayoría de los comandos en sed irán precedidos por una dirección , que especifica qué línea(s) de la secuencia de entrada será editada. Si se omite la dirección, el comando editor se ejecutará en cada línea de la secuencia de entrada. La forma más simple de dirección es un número de línea. Podemos añadir uno a nuestro ejemplo:

[me@linuxbox ~]$ echo "front" | sed '1s/front/back/'
back

Añadiendo la dirección 1 a nuestro comando hacemos que nuestra sustitución se realice en la primera línea de nuestra entrada de una línea. Si especificamos otro número:

[me@linuxbox ~]$ echo "front" | sed '2s/front/back/'
front

vemos que la edición no se ha realizado, ya que nuestra secuencia de entrada no tiene línea 2.

Las direcciones pueden ser expresadas de muchas formas. Aquí tenemos las más comunes:

Tabla 20-7: Notación de direcciones en sed
Dirección Descripción
n Un número de línea donde n es un entero positivo.
$ La última línea
/regexp/ Lineas que coinciden con una expresión regular básica POSIX. Fíjate que la expresión regular se delimita por barras inclinadas. Opcionalmente, la expresión regular puede delimitarse por un carácter alternativo, especificando la expresión con \cregexpc, donde c es el carácter alternativo.
addr1, addr2 Un rango de líneas desde addr1 a addr2, ambos incluidos. Las direcciones pueden estar en cualquiera de las formas que vimos antes.
first~step Encuentra la línea representada por el número first, y luego cada línea siguiente con el intervalo step. Por ejemplo 1~2 se refiere a cada línea impar, 5~5 se refiere a la quinta línea y cada quinta línea después de ella.
addr1, +n Encuentra addr1 y las siguientes n líneas.
addr! Encuentra todas las líneas excepto addr, que puede estar en cualquiera de las formas anteriores.

Probaremos diferentes tipos de direcciones usando el archivo distros.txt que usamos antes en este capítulo. Primero, una rango de números de línea:

[me@linuxbox ~]$ sed -n '1,5p' distros.txt
SUSE   10.2 12/07/2006
Fedora 10   11/25/2008
SUSE   11.0 06/19/2008
Ubuntu 8.04 04/24/2008
Fedora 8    11/08/2007

En este ejemplo, obtenemos un rango de líneas, comenzando con la línea 1 y continuando hasta la línea 5. Para hacerlo, usamos el comando p, que simplemente hace que una línea encontrada se muestre. Sin embargo, para que esto sea efectivo, tenemos que incluir la opción -n (la opción sin autoimpresión) para hacer que sed no muestre cada línea por defecto.

A continuación, probemos algunas expresiones regulares:

[me@linuxbox ~]$ sed -n '/SUSE/p' distros.txt
SUSE 10.2 12/07/2006
SUSE 11.0 06/19/2008
SUSE 10.3 10/04/2007
SUSE 10.1 05/11/2006

Incluyendo la expresión regular delimitada por barras inclinadas /SUSE/, podemos aislar las líneas que la contengan casi de la misma forma que con grep.

Finalmente, probaremos la negación añadiendo un signo de exclamación (!) a la dirección:

[me@linuxbox ~]$ sed -n '/SUSE/!p' distros.txt
Fedora 10   11/25/2008
Ubuntu 8.04 04/24/2008
Fedora 8    11/08/2007
Ubuntu 6.10 10/26/2006
Fedora 7    05/31/2007
Ubuntu 7.10 10/18/2007
Ubuntu 7.04 04/19/2007
Fedora 6    10/24/2006
Fedora 9    05/13/2008
Ubuntu 6.06 06/01/2006
Ubuntu 8.10 10/30/2008
Fedora 5    03/20/2006

Aquí vemos el resultado esperado: todas las líneas en el archivo excepto las que coinciden con la expresión regular.

Hasta ahora, hemos visto dos de los comandos de edición de sed, s y p. Aquí tenemos una lista completa de los comandos de edición básicos:

Tabla 20-8: comandos de edición básicos de sed
Comando Descripción
= Muestra el número de línea actual.
a Añade texto tras la línea actual.
d Borra la línea actual.
i Inserta texto antes de la línea actual.
p Muestra la línea actual. Por defecto, sed imprime cada línea y sólo edita líneas que coinciden con una dirección especificada dentro del archivo. El comportamiento por defecto puede sobrescribirse especificando la opción -n.
q Sale de sed sin procesar más líneas. Si la opción -n no se especifica, muestra la línea actual.
Q Sale de sed sin procesar más líneas.
s/regexp/replacement/ Sustituye el contenido de replacement donde se encuentre regexp, replacement puede incluir el carácter &, que es equivalente al texto correspondiente a regexp. Además, replacement puede incluir las secuencias \1 a \9, que son el contenido de las correspondientes subexpresiones de regexp. Para saber más sobre ésto, mira el tema de retroreferencias que está más adelante. Tras la barra invertida posterior a replacement, puede especificarse una marca para modificar el comportamiento del comando s.
y/set1/set2 Realiza transliteración convirtiendo caracteres de set1 a los correspondientes caracteres en set2. Fíjate que al contrario que tr, sed requiere que ambas series tengan la misma longitud.

El comando s es de lejos el comando de edición más comúnmente usado. Probaremos sólo algunas de sus capacidades realizando edición en nuestro archivo distros.txt. Vimos antes cómo el campo fecha en distros.txt no estaba en formato "compatible con ordenadores". Ya que la fecha está formateada MM/DD/YYY, y sería mejor (para ordenarla más fácilmente) si el formato fuera YYYY-MM-DD. Realizar este cambio en el archivo a mano llevaría mucho tiempo y sería propenso a errores, pero con sed, este cambio puede realizarse en un paso:

[me@linuxbox ~]$ sed 's/\([0-9]\{2\}\)\/\([0-9]\{2\}\)\/\([0-9]\{4\}\)$/\3-\1-\2/' distros.txt
SUSE   10.2 2006-12-07
Fedora 10   2008-11-25
SUSE   11.0 2008-06-19
Ubuntu 8.04 2008-04-24
Fedora 8    2007-11-08
SUSE   10.3 2007-10-04
Ubuntu 6.10 2006-10-26
Fedora 7    2007-05-31
Ubuntu 7.10 2007-10-18
Ubuntu 7.04 2007-04-19
SUSE   10.1 2006-05-11
Fedora 6    2006-10-24
Fedora 9    2008-05-13
Ubuntu 6.06 2006-06-01
Ubuntu 8.10 2008-10-30
Fedora 5    2006-03-20

¡Guau! Es un comando muy feo. Pero funciona. En sólo un paso, hemos cambiado el formato de fecha de nuestro archivo. También es un ejemplo perfecto de por qué las expresiones regulares son llamadas, a veces en broma, un medio de "sólo-escritura". Podemos escribirlas, pero a menudo no podemos leerlas. Antes de que estemos tentados a huir aterrorizados de este comando, veamos como ha sido construido. Primero, sabemos que el comando tendrá esta estructura básica:

sed 's/regexp/replacement/' distros.txt

Nuestro próximo paso es encontrar una expresión regular que aisle la fecha. Como está en formato MM/DD/YYYY y aparece al final de la línea, podemos usar una expresión como esta:

[0-9]{2}/[0-9]{2}/[0-9]{4}$

que encuentra dos dígitos, una barra, dos dígitos, una barra, cuatro dígitos y el final de la línea. Así que esto se ocupa de regexp, pero ¿qué pasa con replacement? Para manejar esto, tenemos que introducir una nueva capacidad de las expresiones regulares que aparecen en algunas aplicaciones que usan BRE. Esta capacidad se llama retroreferencias y funcionan así: Si aparece la secuencia \n en replacement donde n es un número del 1 al 9, la secuencia se referirá a la subexpresión correspondiente en la expresión regular precedente. Para crear las subexpresiones, simplemente la metemos entre paréntesis así:

([0-9]{2})/([0-9]{2})/([0-9]{4})$

Ahora tenemos tres subexpresiones. La primera contiene el mes, la segunda contiene el día del mes y la tercera contiene el año. Ahora podemos construir replacement como sigue:

\3-\1-\2

que nos da el año, un guión, el mes, un guión y el día.

Ahora nuestro comando tiene esta pinta:

sed 's/([0-9]{2})/([0-9]{2})/([0-9]{4})$/\3-\1-\2/' distros.txt

Tenemos dos problemas todavía. El primero es que las barras extra en nuestra expresión confundirán a sed cuando trate de interpretar el comando s. El segundo es que como sed, por defecto, acepta sólo expresiones regulares básicas, varios caracteres en nuestra expresión regular serán tratados como literales, en lugar de como metacaracteres. Podemos resolver ambos problemas con una generosa utilización de barras invertidas para escapar los caracteres ofensivos:

sed 's/\([0-9]\{2\}\)\/\([0-9]\{2\}\)\/\([0-9]\{4\}\)$/\3-\1-\2/' distros.txt

¡Y aquí lo tenemos!

Otra característica del comando s es el uso de marcas opcionales que pueden seguir a la cadena de reemplazo. La más importante es la marca g, que ordena a sed que aplique una búsqueda y reemplazo global a una línea, no sólo a la primera instancia, que es la opción por defecto. Aquí tenemos un ejemplo:

[me@linuxbox ~]$ echo "aaabbbccc" | sed 's/b/B/'
aaaBbbccc

Vemos que se ha realizado el reemplazo, pero sólo en la primera instancia de la letra "b", mientras que las siguientes instancias se han dejado sin cambios. Añadiendo la marca g, podemos cambiar todas las instancias:

[me@linuxbox ~]$ echo "aaabbbccc" | sed 's/b/B/g'
aaaBBBccc

Hasta ahora, sólo le hemos dado a sed comandos individuales a través de la línea de comandos. También es posible construir comandos más complejos en un archivo de script usando la opción -f. Para hacer una demostración, usaremos sed con nuestro archivo distros.txt para crear un informe. Nuestro informe mostrará un título arriba, nuestras fechas modificadas y todos los nombres de las distribuciones convertidos a mayúsculas. Para hacerlo, necesitaremos escribir un script, así que arrancaremos nuestro editor de texto y escribiremos los siguiente:

# sed script to produce Linux distributions report

1 i\
\
Linux Distributions Report\

s/\([0-9]\{2\}\)\/\([0-9]\{2\}\)\/\([0-9]\{4\}\)$/\3-\1-\2/
y/abcdefghijklmnopqrstuvwxyz/ABCDEFGHIJKLMNOPQRSTUVWXYZ/

Guardaremos nuestro script de sed como distros.sed y lo ejecutaremos así:

[me@linuxbox ~]$ sed -f distros.sed distros.txt

Linux Distributions Report

SUSE   10.2 2006-12-07
FEDORA 10   2008-11-25
SUSE   11.0 2008-06-19
UBUNTU 8.04 2008-04-24
FEDORA 8    2007-11-08
SUSE   10.3 2007-10-04
UBUNTU 6.10 2006-10-26
FEDORA 7    2007-05-31
UBUNTU 7.10 2007-10-18
UBUNTU 7.04 2007-04-19
SUSE   10.1 2006-05-11
FEDORA 6    2006-10-24
FEDORA 9    2008-05-13
UBUNTU 6.06 2006-06-01
UBUNTU 8.10 2008-10-30
FEDORA 5    2006-03-20

Como podemos ver, nuestro script produce el resultado esperado, pero ¿cómo lo ha hecho? Echemos otro vistazo a nuestro script. Usaremos cat para numerar las líneas:

[me@linuxbox ~]$ cat -n distros.sed
  1 # sed script to produce Linux distributions report
  2
  3 1 i\
  4 \
  5 Linux Distributions Report\
  6
  7 s/\([0-9]\{2\}\)\/\([0-9]\{2\}\)\/\([0-9]\{4\}\)
$/\3-\1-\2/
  8 y/abcdefghijklmnopqrstuvwxyz/ABCDEFGHIJKLMNOPQRSTUVWXYZ/

La línea uno de nuestro script es un comentario. Como muchos archivos de configuración y lenguajes de programación en sistemas Linux, los comentarios comienzan con el caracter # y siguen con texto legible por humanos. Los comentarios pueden colocarse en cualquier parte del script (aunque no dentro de los propios comandos) y son útiles para las personas que necesiten identificar y/o mantener el script.

La línea 2 es una línea en blanco. Como los comentarios, las líneas en blanco deben añadirse para mejorar la legibilidad.

Muchos comandos sed soportan direcciones de línea. Se usan para especificar en qué líneas de entrada hay que actuar. Las direcciones de línea pueden expresarse como números de línea individuales, rangos de números de líneas y el número especial de línea "$" que indica la última línea de la entrada.

Las líneas 3 a 6 contienen texto a insertar en la dirección 1, la primera línea de la entrada. Al comando i le sigue la secuencia barra invertida-retorno de carro para producir un retorno de carro escapado, o lo que se llama un carácter de continuación de línea. Esta secuencia, que puede usarse en muchas circunstancias incluyendo scripts de shell, permite insertar un retorno de carro en una secuencia de texto sin indicar al intérprete (en este caso sed) que se ha alcanzado el final de la línea. Los comandos i, y de igual forma, a (que añade texto, en lugar de insertarlo) y c (que reemplaza texto), permiten múltiples líneas de texto siempre que cada línea, excepto la última, termine con el carácter continuación de línea. La sexta línea de nuestro script es realmente el final de nuestro texto insertado y termina con un simple retorno de carro, en lugar de con un carácter de continuación de línea, señalándole el final al comando i.

Nota: Un carácter de continuación de línea lo forman una barra invertida seguida inmediatamente por un retorno de carro. No se permiten espacios intermedios.

La línea 7 es nuestro comando de búsqueda y reemplazo. Como no está precedido por una dirección, cada línea de la secuencia de entrada está sometida a su acción.

La línea 8 realiza transliteración de las letras minúsculas a letras mayúsculas. Fíjate que al contrario que tr, el comando y de sed no soporta rangos de caracteres (por ejemplo, [a-z]), ni soporta clases de caracteres POSIX. De nuevo, como el comando y no está precedido por una dirección, se aplica a cada línea de la secuencia de entrada.

No hay comentarios:

Publicar un comentario