viernes, 29 de septiembre de 2017

Procesamiento de Texto

Todos los sistemas operativos como Unix-like dependen en gran medida de los archivos de texto para varios tipos de almacenamiento de datos. Así que tiene sentido que haya muchas herramientas para manipular texto. En este capítulo, veremos programas que se usan para "cocinar" texto. En el siguiente capítulo, veremos más procesado de texto, centrándonos en programas que se utilizan para formatear el texto para imprimirlo y otros tipos de consumo humano.

Este capítulo revisitará a algunos viejos amigos y nos presentará algunos nuevos:

  • cat - Encadena archivos e imprime en la salida estándar
  • sort - Ordena líneas en archivos de texto
  • uniq - Reporta u omite líneas repetidas
  • cut - Elimina secciones de cada línea de archivos
  • paste - Une lineas de archivos
  • join - Une líneas de dos archivos en un campo común
  • comm - Compara dos archivos ordenados línea por línea
  • diff - Compara archivos línea por línea
  • patch - Aplica un archivo de diferencia a uno original
  • tr - Traduce o borra caracteres
  • sed - Editor en continuo para filtrar y transformar texto
  • aspell - Comprobador de deletreo interactivo
Aplicaciones de texto

Hasta ahora, hemos aprendido un par de editores de texto (nano y vim), hemos visto un puñado de archivos de configuración, y hemos presenciado la salida de docenas de comandos, todo en texto. Pero ¿para qué más se usa el texto? Resulta que para muchas cosas.

Documentos

Mucha gente escribe documentos usando formatos de texto plano. Mientras que es fácil ver como un archivo de texto pequeño puede ser útil para guardar notas simples, también es posible escribir grandes documentos en formato texto. Un enfoque popular es escribir un documento de texto largo en formato texto y luego usar un lenguaje de marcas para describir el formato del documento final. Muchos papers científicos están escritos usando este método, ya que los sistemas de procesado de texto basados en Unix fueron casi los primeros sistemas que soportaron diseños de tipografía avanzada necesarios para los escritores de disciplinas técnicas.

Páginas web

El tipo de documento electrónico más popular del mundo es probablemente la página web. Las páginas web son documentos de texto que usan HTML (Hypertext Markup Language - Lenguaje de marcas de hipertexto) o XML (Extensible Markup Language - Lenguaje de marcas extensible) como lenguajes de marcas para describir el formato visual de los documentos.

Email

El email es un medio intrínsecamente basado en texto. Incluso los adjuntos que no son texto son convertidos a una representación textual para transmitirlos. Podemos ver esto nosotros mismos descargando un mensaje de email y viéndolo con less. Veremos que el mensaje comienza con un encabezado que describe la fuente del mensaje y el procesado que ha recibido durante su viaje, seguido por un cuerpo del mensaje con su contenido.

Salida de impresora

En sistemas como-Unix, la salida destinada para una impresora se manda como texto plano o, si la página contiene gráficos, se convierte en un formato de texto en lenguaje de descripción de página llamado PostScript, que se envía a un programa que genera los puntos gráficos a imprimir.

Código fuente de programas

Muchos de los programas de la línea de comandos que encontramos en los sistemas como-Unix fueron creados para soportar administración de sistemas y desarrollo de software, y los programas de procesado de texto no son una excepción. Muchos de ellos están diseñados para resolver problemas de desarrollo de software. La razón por la que el procesado de texto es importante para los desarrolladores de software es que todo software nace como un texto. El código fuente, la parte del programa que el programador escribe en realidad, siempre está en formato texto.


Revisitando antiguos amigos

Volviendo al Capítulo 6 (Redirección), aprendimos algunos comandos que sirven para aceptar entrada estándar además de argumentos de línea de comandos. Sólo los vimos brevemente entonces, pero ahora le echaremos un vistazo más de cerca para ver como pueden usarse para realizar procesado de texto.

cat

El programa cat tiene un gran número de opciones interesantes. Muchas de ellas se usan para ayudarnos a visualizar mejor el contenido del texto. Un ejemplo es la opción -A, que se usa para mostrar caracteres no imprimibles en el texto. Hay veces que queremos saber si los caracteres de control están incrustados en nuestro texto visible. Los más comunes son los caracteres de tabulación (en lugar de espacios) y retornos de carro, a menudo presentes como caracteres de fin de línea en archivos de texto estilo MS-DOS. Otra situación común es un archivo que contiene líneas de texto con espacios al final.

Creemos un archivo de texto usando cat como un procesador de texto primitivo. Para hacerlo, sólo introduciremos el comando cat (sólo especificando un archivo para redirigir la salida) y escribiremos nuestro texto, seguido de Enter para terminar la línea apropiadamente, luego Ctrl-d para indicar a cat que hemos alcanzado el final del archivo. En este ejemplo, introducimos un carácter tabulador al principio y continuamos la línea con algunos espacios al final:

 [me@linuxbox ~]$ cat > foo.txt
    The quick brown fox jumped over the lazy dog.
 [me@linuxbox ~]$

A continuación, usaremos cat con la opción -A para mostrar el texto:

 [me@linuxbox ~]$ cat -A foo.txt
 ^IThe quick brown fox jumped over the lazy dog. $
 [me@linuxbox ~]$

Como podemos ver en los resultados, el carácter tabulador en nuestro texto se representa como ^I. Es una notación común que significa "Control-I" que, resulta que es lo mismo que el carácter tab. También vemos que aparece un $ al final real de la línea, indicando que nuestro texto contiene espacios al final.

cat también tiene opciones que se usan para modificar texto. Las dos más prominentes son -n, que numera las líneas, y -s, que suprime la salida de líneas en blanco múltiples. Podemos demostrarlo así:

 [me@linuxbox ~]$ cat > foo.txt
 The quick brown fox
 

 jumped over the lazy dog.
 [me@linuxbox ~]$ cat -ns foo.txt
      1 The quick brown fox
      2
      3 jumped over the lazy dog.
 [me@linuxbox ~]$

En este ejemplo, hemos creado una nueva versión de nuestro archivo de texto foo.txt, que contiene dos líneas de texto separadas por dos líneas en blanco. Tras procesarlo con cat con las opciones -ns, la línea en blanco extra se elimina y el resto de líneas son numeradas. Aunque no es un proceso muy grande a realizar sobre el texto, es un proceso.


Texto MS-DOS Vs. Texto Unix

Una de las razones por las que querrías usar cat para buscar caracteres no imprimibles en el texto es para eliminar retornos de carro ocultos. ¿De donde vienen los los retornos de carro ocultos? ¡De DOS y Windows! Unix y DOS no definen el final de una linea de la misma forma en los archivos de texto. Unix finaliza una línea con un carácter salto de línea (ASCII 10) mientras que MS-DOS y sus derivados usan la secuencia del retorno de carro (ASCII 13) y salto de línea para terminar cada línea de texto.

Hay varias formas de convertir archivos de formato DOS a UNIX. En muchos sistemas Linux, hay programas llamados dos2unix y unix2dos, que pueden convertir archivos de texto de y hacia formato DOS. Sin embargo, si no tienes dos2unix en tu sistema, no te preocupes. El proceso de convertir texto de formato DOS a Unix es muy simple; simplemente implica eliminar los retornos de carro innecesarios. Esto lo realizan fácilmente fácilmente un par de programas que veremos más tarde en éste capítulo.

sort

El programa sort ordena el contenido de la entrada estándar, o de uno o más archivos especificados en la línea de comandos, y manda el resultado a la salida estándar. Usando la misma técnica que usamos con cat, podemos demostrar el procesado de la entrada estándar directamente desde el teclado:

 [me@linuxbox ~]$ sort > foo.txt
 c
 b
 a
 [me@linuxbox ~]$ cat foo.txt
 a
 b
 c

Tras introducir el comando, escribimos las letras "c", "b" y "a", seguida de nuevo de Ctrl-d para indicar el final del archivo. Entonces vemos el archivo resultante y vemos que las líneas ahora aparecen ordenadas.

Como sort puede aceptar múltiples archivos en la línea de comandos como argumentos, es posible unir múltiples archivos en un único total. Por ejemplo, si tenemos tres archivos de texto y queremos combinarlos en un único archivo ordenado, podríamos hacer algo como ésto:

 sort file1.txt file2.txt file3.txt > final_sorted_list.txt

sort tiene varias opciones interesantes. Aquí hay una lista parcial:

Opciones comunes de ordenamiento

-b --ignore-leading-blanks
Por defecto, el ordenado se realiza en la línea completa, empezando con el primer carácter de la línea. Esta opción hace que sort ignore los espacios al principio de la línea y calcula el ordenado basándose en el primer carácter que no es un espacio en blanco en la línea.

-f --ignore-case
Hace el ordenamiento sensible a mayúsculas.

-n --numeric-sort
Realiza el ordenado basado en la evaluación numérica de una cadena. Usar esta opción permite que el ordenado se realice según valores numéricos en lugar de valores alfabéticos.

-r --reverse
Ordena inversamente. Los resultados son descendentes en lugar de ascendentes.

-k --key=campo1[,campo2]
Orden basado en un campo clave localizado entre campo1 y campo2en lugar de en la línea entera. Veremos este tema luego.

-m --merge
Trata cada argumento como el nombre de un archivo preordenado. Une múltiples archivos en uno sólo ordenado resultante sin realizar ningún ordenamiento adicional.

-o --output=archivo
Envía salida ordenada a archivo en lugar de a la salida estándar.

-t --fiel-separator=carácter
Define el caracter separador de campos. Por defecto los campos se separan por espacios o tabuladores.

Aunque la mayoría de las opciones vistas antes son muy autoexplicativas, algunas no lo son. Primero veamos la opción -n, usada para ordenamiento numérico. Con esta opción, es posible ordenar valores basándonos en valores numéricos. Podemos demostrarlo ordenado los resultados del comando du para determinar los usuarios con más espacio en disco. Normalmente, el comando du lista los resultados de un sumario por orden de la ruta:

 [me@linuxbox ~]$ du -s /usr/share/* | head
 252 /usr/share/aclocal
 96 /usr/share/acpi-support
 8 /usr/share/adduser
 196 /usr/share/alacarte
 344 /usr/share/alsa
 8 /usr/share/alsa-base
 12488 /usr/share/anthy
 8 /usr/share/apmd
 21440 /usr/share/app-install
 48 /usr/share/application-registry

En este ejemplo, entubamos el resultado en head para limitar los resultados a las primeras diez líneas. Podemos producir una lista ordenada numéricamente para mostrar los diez mayores consumidores de espacio de esta forma:

 [me@linuxbox ~]$ du -s /usr/share/* | sort -nr | head
 509940 /usr/share/locale-langpack
 242660 /usr/share/doc
 197560 /usr/share/fonts
 179144 /usr/share/gnome
 146764 /usr/share/myspell
 144304 /usr/share/gimp
 135880 /usr/share/dict
 76508 /usr/share/icons
 68072 /usr/share/apps
 62844 /usr/share/foomatic

Usando las opciones -nr, producimos un orden numérico inverso, con los mayores valores apareciendo primero en los resultados. Este orden funciona porque los valores numéricos están al principio de cada línea. Pero, ¿qué pasa si queremos ordenar una lista basándonos en algún valor localizado dentro de la línea? Por ejemplo, los resultados de un ls -l:

 [me@linuxbox ~]$ ls -l /usr/bin | head
 total 152948
 -rwxr-xr-x 1 root root 34824 2008-04-04 02:42 [
 -rwxr-xr-x 1 root root 101556 2007-11-27 06:08 a2p
 -rwxr-xr-x 1 root root 13036 2008-02-27 08:22 aconnect
 -rwxr-xr-x 1 root root 10552 2007-08-15 10:34 acpi
 -rwxr-xr-x 1 root root 3800 2008-04-14 03:51 acpi_fakekey
 -rwxr-xr-x 1 root root 7536 2008-04-19 00:19 acpi_listen
 -rwxr-xr-x 1 root root 3576 2008-04-29 07:57 addpart
 -rwxr-xr-x 1 root root 20808 2008-01-03 18:02 addr2line
 -rwxr-xr-x 1 root root 489704 2008-10-09 17:02 adept_batch

Ignorando, por el momento, que ls puede ordenar sus resultados por tamaño, podríamos usar sort par ordenar la lista por tamaño de archivo, así:

 [me@linuxbox ~]$ ls -l /usr/bin | sort -nr -k 5 | head
 -rwxr-xr-x 1 root root 8234216 2008-04-07 17:42 inkscape
 -rwxr-xr-x 1 root root 8222692 2008-04-07 17:42 inkview
 -rwxr-xr-x 1 root root 3746508 2008-03-07 23:45 gimp-2.4
 -rwxr-xr-x 1 root root 3654020 2008-08-26 16:16 quanta
 -rwxr-xr-x 1 root root 2928760 2008-09-10 14:31 gdbtui
 -rwxr-xr-x 1 root root 2928756 2008-09-10 14:31 gdb
 -rwxr-xr-x 1 root root 2602236 2008-10-10 12:56 net
 -rwxr-xr-x 1 root root 2304684 2008-10-10 12:56 rpcclient
 -rwxr-xr-x 1 root root 2241832 2008-04-04 05:56 aptitude
 -rwxr-xr-x 1 root root 2202476 2008-10-10 12:56 smbcacls

Muchos usos de sort implican el procesado de datos tabulares, como los resultados del comando ls anterior. Si aplicamos terminología de bases de datos a la tabla anterior, diríamos que cada fila es un registro consistente en múltiples campos, como los atributos de archivo, contador de enlaces, nombre de archivo, tamaño de archivo y así sucesivamente. sort puede procesar campos individuales. En términos de bases de datos, podemos especificar uno o más campos clave para usar como claves de ordenamiento. En el ejemplo anterior, especificamos las opciones n y r para realizar un ordenamiento numérico inverso y especificar -k 5 para hacer que sort use el quinto campo como la clave de ordenamiento.

La opción k es muy interesante y tiene muchas funciones, pero primero tenemos que hablar sobre como sort define los campos. Consideremos un archivo de texto muy simple consistente en una única línea que contenga el nombre del autor:

 William Shotts

Por defecto, sort ve esta línea con dos campos. El primer campo contiene los caracteres:

"William"

y el segundo campo contiene los caracteres:

"Shotts"

lo que significa que los caracteres de espacio en blanco (espacios y tabuladores) se usan como delimitadores entre los campos y que los delimitadores se incluyen en el campo cuando se realiza el ordenado.

Mirando de nuevo a la línea de la salida de nuestro ls, podemos ver que una línea contiene ocho campos y que el quinto campo es el tamaño del archivo:

 -rwxr-xr-x 1 root root 8234216 2008-04-07 17:42 inkscape

Para nuestra próxima serie de experimentos, consideraremos el siguiente archivo que contiene la historia de tres distribuciones Linux populares desarrolladas entre 2006 y 2008. Cada línea del archivo tiene tres campos: el nombre de la distribución, el número de versión y la fecha de publicación en formato MM/DD/YYYY:

 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
 SUSE 10.3 10/04/2007
 Ubuntu 6.10 10/26/2006
 Fedora 7 05/31/2007
 Ubuntu 7.10 10/18/2007
 Ubuntu 7.04 04/19/2007
 SUSE 10.1 05/11/2006
 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

Usando un editor de texto (quizá vim), introduciremos estos datos y nombraremos al archivo resultante distros.txt.

A continuación, trataremos de ordenar el archivo y observaremos el resultado:

 [me@linuxbox ~]$ sort distros.txt
 Fedora 10 11/25/2008
 Fedora 5 03/20/2006
 Fedora 6 10/24/2006
 Fedora 7 05/31/2007
 Fedora 8 11/08/2007
 Fedora 9 05/13/2008
 SUSE 10.1 05/11/2006
 SUSE 10.2 12/07/2006
 SUSE 10.3 10/04/2007
 SUSE 11.0 06/19/2008
 Ubuntu 6.06 06/01/2006
 Ubuntu 6.10 10/26/2006
 Ubuntu 7.04 04/19/2007
 Ubuntu 7.10 10/18/2007
 Ubuntu 8.04 04/24/2008
 Ubuntu 8.10 10/30/2008

Bien, casi ha funcionado. El problema aparece en el ordenamiento de los números de versión de Fedora. Como un "1" viene antes que un "5" en el listado de caracteres, la versión "10" aparece arriba mientras que la versión "9" aparece al final.

Para arreglar este problema vamos a tener que ordenar por múltiples claves. Queremos realizar un orden alfabético en el primer campo y luego un orden numérico en el segundo campo. sort permite múltiples instancias de la opción -k así que pueden especificarse múltiples claves de ordenamiento. De hecho, una clave puede incluir un rango de campos. Si no se especifica un rango (como ha sido el caso de nuestros ejemplos anteriores), sort usa una clave que comienza con el campo especificado y se extiende hasta el final de la línea. Aquí tenemos la sintaxis de nuestro ordenamiento multi-clave:

 [me@linuxbox ~]$ sort --key=1,1 --key=2n distros.txt
 Fedora 5 03/20/2006
 Fedora 6 10/24/2006
 Fedora 7 05/31/2007
 Fedora 8 11/08/2007
 Fedora 9 05/13/2008
 Fedora 10 11/25/2008
 SUSE 10.1 05/11/2006
 SUSE 10.2 12/07/2006
 SUSE 10.3 10/04/2007
 SUSE 11.0 06/19/2008
 Ubuntu 6.06 06/01/2006
 Ubuntu 6.10 10/26/2006
 Ubuntu 7.04 04/19/2007
 Ubuntu 7.10 10/18/2007
 Ubuntu 8.04 04/24/2008
 Ubuntu 8.10 10/30/2008

Aunque hemos usado la forma larga de la opción para que se vea más claramente, -k 1, 1 -k 2n sería completamente equivalente. En la primera instancia de la opción de clave, especificamos un rango de campos a incluir en la primera clave. Como queremos limitar el orden al primer campo solamente, especificamos 1, 1 que significa "comienza en el campo uno y termina en el campo uno." En la segunda instancia, hemos especificado 2n, que significa que el campo 2 es el campo de ordenado y que el orden debería ser numérico. Puede incluirse una letra de opción al final de un especificador de clave para indicar el tipo de orden a realizar. Estas letras de opción son las mismas que las opciones globales del programa sort: b (ignora los espacios en blanco delanteros), n (orden numérico), r (orden inverso), y así sucesivamente.

El tercer campo de nuestra lista contiene una fecha con un formato inapropiado para ordenarlo. En los ordenadores, las fechas están normalmente formateadas como YYYY-MM-DD para hacer más fácil el ordenamiento cronológico, pero la nuestra está en formato Americano MM/DD/YYYY. ¿Cómo podemos ordenar esta lista por orden cronológico?

Afortunadamente, sort nos da una forma. La clave de opción permite especificarcompensaciones dentro de campos, por lo que podemos definir claves dentro de campos:

 [me@linuxbox ~]$ sort -k 3.7nbr -k 3.1nbr -k 3.4nbr distros.txt
 Fedora 10 11/25/2008
 Ubuntu 8.10 10/30/2008
 SUSE 11.0 06/19/2008
 Fedora 9 05/13/2008
 Ubuntu 8.04 04/24/2008
 Fedora 8 11/08/2007
 Ubuntu 7.10 10/18/2007
 SUSE 10.3 10/04/2007
 Fedora 7 05/31/2007
 Ubuntu 7.04 04/19/2007
 SUSE 10.2 12/07/2006
 Ubuntu 6.10 10/26/2006
 Fedora 6 10/24/2006
 Ubuntu 6.06 06/01/2006
 SUSE 10.1 05/11/2006
 Fedora 5 03/20/2006

Especificando -k 3.7 le decimos a sort que use una clave de ordenación que comience en el séptimo carácter del tercer campo, que corresponde al principio del año. De igual forma, especificamos -k 3.1 y -k 3.4 para aislar las zonas del día y el mes de la fecha. También añadimos las opciones n y r para realizar un ordenado numérico inverso. La opción b se incluye para suprimir los espacios delanteros (cuyo número varía según la línea, afectando de este modo al resultado del ordenado) en el campo fecha.

Algunos archivos no usan tabuladores ni espacios como delimitadores de campos; por ejemplo, el archivo /etc/passwd:

 [me@linuxbox ~]$ head /etc/passwd
 root:x:0:0:root:/root:/bin/bash
 daemon:x:1:1:daemon:/usr/sbin:/bin/sh
 bin:x:2:2:bin:/bin:/bin/sh
 sys:x:3:3:sys:/dev:/bin/sh
 sync:x:4:65534:sync:/bin:/bin/sync
 games:x:5:60:games:/usr/games:/bin/sh
 man:x:6:12:man:/var/cache/man:/bin/sh
 lp:x:7:7:lp:/var/spool/lpd:/bin/sh
 mail:x:8:8:mail:/var/mail:/bin/sh
 news:x:9:9:news:/var/spool/news:/bin/sh

Los campos en este archivo están delimitados por dos puntos (:), así que ¿cómo ordenamos este archivo usando campos clave? sort cuenta con la opción -t para definir el carácter separador de campos. Para ordenar el archivo passwd por el séptimo campo (el shell por defecto de la cuenta), podríamos hacer ésto:

 [me@linuxbox ~]$ sort -t ':' -k 7 /etc/passwd | head
 me:x:1001:1001:Myself,,,:/home/me:/bin/bash
 root:x:0:0:root:/root:/bin/bash
 dhcp:x:101:102::/nonexistent:/bin/false
 gdm:x:106:114:Gnome Display Manager:/var/lib/gdm:/bin/false
 hplip:x:104:7:HPLIP system user,,,:/var/run/hplip:/bin/false
 klog:x:103:104::/home/klog:/bin/false
 messagebus:x:108:119::/var/run/dbus:/bin/false
 polkituser:x:110:122:PolicyKit,,,:/var/run/PolicyKit:/bin/false
 pulse:x:107:116:PulseAudio daemon,,,:/var/run/pulse:/bin/false

Especificando el carácter de los dos puntos como separador de campos, podemos ordenar por el séptimo campo.


uniq

Comparándolo con sort, el programa uniq es un peso pluma. uniq realiza una tarea que puede parecer trivial. Cuando, le damos un archivo ordenado (incluida la entrada estándar), elimina las líneas duplicadas y manda los resultados a la salida estándar. A menudo se usa junto con sort para limpiar la salida de duplicados.

Consejo: Mientras que uniq es una herramienta tradicional de Unix a menudo usada con sort, la versión GNU de sort soporta la opción -u, que elimina los duplicados de la salida ordenada.

Hagamos un archivo de texto para probarlo:

 [me@linuxbox ~]$ cat > foo.txt
 a
 b
 c
 a
 b
 c

Recuerda pulsar Ctrl-d para finalizar la salida estándar. Ahora, si ejecutamos uniqen nuestro archivo de texto:

 [me@linuxbox ~]$ uniq foo.txt
 a
 b
 c
 a
 b
 c

los resultados no son diferentes de nuestro archivo original; los duplicados no han sido eliminados. Para que uniq haga realmente su trabajo, la entrada debe ser ordenada primero:

 [me@linuxbox ~]$ sort foo.txt | uniq
 a
 b
 c

Ésto es porque uniq sólo elimina las líneas duplicadas que son adyacentes a otra.

uniq tiene varias opciones. Aquí están las más comunes:

Opciones comunes de uniq

-c   Muestra una lista de líneas duplicadas precedidas por el número de veces que aparece la línea.
-d   Sólo muestra las líneas repetidas, en lugar de las líneas únicas.
-f n Ignora n campos precedentes en cada línea. Los campos se separan por espacios en blanco como en sort; sin embargo, al contrario que en sort, uniq no tiene una opción para configurar un separador de campos alternativo.
-i   Ignora las mayúsculas durante la comparación de líneas.
-s n Se salta (ignora) los n primeros caracteres de cada línea.
-u   Sólo muestra líneas únicas. Es la configuración por defecto.

Aquí vemos uniq usado para mostrar el número de duplicados encontrados en nuestro archivo de texto, usando la opción -c:

 [me@linuxbox ~]$ sort foo.txt | uniq -c
       2 a
       2 b
       2 c


Reordenando

Los tres programas siguientes que vamos a ver se usan para extraer columnas de nuestro archivo de texto y recombinarlas de forma útil.

cut

El programa cut se usa para extraer una sección de texto de una línea y pasar la sección extraída a la salida estándar. Puede aceptar múltiples argumentos de archivo o entradas de la entrada estándar.

Especificar la sección de la línea a extraer es un tanto incómodo y se especifica usando las siguientes opciones:

Opciones de selección de cut

-c lista_de_caracteres
Extrae al porción de línea definida por lista_de_caracteres. La lista puede consistir en uno o más rangos numéricos separados por comas.


-f lista_de_campos
Extrae uno o más campos de la línea como esté definido porlista_de_campos. La lista puede contener uno o más campos o rangos de campos separados por comas.

-d carácter_delim
Cuando especificamos -f, usamos carácter_delim como el carácter delimitador de campos. Por defecto, los campos deben ser separados por un carácter de tabulador.

--complement
Extrae la línea de texto completa, excepto aquellas porciones especificadas por -c y/o -f.

Como podemos ver, la forma en que cut extrae el texto es más bien inflexible. cut se usa mejor para extraer texto de archivos que son producidos por otros programas, en lugar de texto escrito directamente por humanos. Echaremos un vistazo a nuestro archivo distros.txt para ver si está suficientemente "limpio" para ser un buen espécimen para nuestros ejemplos con cut. Si usamos cat con la opción -A, podemos ver si el archivo cumple nuestros requisitos de campos separados por tabuladores:

 [me@linuxbox ~]$ cat -A distros.txt
 SUSE^I10.2^I12/07/2006$
 Fedora^I10^I11/25/2008$
 SUSE^I11.0^I06/19/2008$
 Ubuntu^I8.04^I04/24/2008$
 Fedora^I8^I11/08/2007$
 SUSE^I10.3^I10/04/2007$
 Ubuntu^I6.10^I10/26/2006$
 Fedora^I7^I05/31/2007$
 Ubuntu^I7.10^I10/18/2007$
 Ubuntu^I7.04^I04/19/2007$
 SUSE^I10.1^I05/11/2006$
 Fedora^I6^I10/24/2006$
 Fedora^I9^I05/13/2008$
 Ubuntu^I6.06^I06/01/2006$
 Ubuntu^I8.10^I10/30/2008$
 Fedora^I5^I03/20/2006$

Tiene buena pinta. Sin espacios intermedios, sólo caracteres de tabulador entre los campos. Como el archivo usa tabuladores en lugar de espacios, usaremos la opción -fpara extraer un campo:

 [me@linuxbox ~]$ cut -f 3 distros.txt
 12/07/2006
 11/25/2008
 06/19/2008
 04/24/2008
 11/08/2007
 10/04/2007
 10/26/2006
 05/31/2007
 10/18/2007
 04/19/2007
 05/11/2006
 10/24/2006
 05/13/2008
 06/01/2006
 10/30/2008
 03/20/2006

Como nuestro archivo distros está delimitado por tabuladores, es mejor usar cutpara extraer campos en lugar de caracteres. Ésto es porque cuando un archivo está delimitado por tabuladores, es muy improbable que cada línea contenga el mismo número de caracteres, que hace que calcular posiciones de caracteres dentro de la línea sea complicado o imposible. En nuestro ejemplo anterior, sin embargo, hemos extraído un campo que afortunadamente contiene datos de igual longitud, así que podemos mostrar como funciona la extracción de caracteres extrayendo el año de cada línea:

 [me@linuxbox ~]$ cut -f 3 distros.txt | cut -c 7-10
 2006
 2008
 2008
 2008
 2007
 2007
 2006
 2007
 2007
 2007
 2006
 2006
 2008
 2006
 2008
 2006

Ejecutando cut una segunda vez en nuestra lista, podemos extraer las posiciones de caracteres de la 7 a la 10, que corresponden al año en nuestro campo fecha. La notación 7-10 es un ejemplo de rango. La man page de cut contiene una descripción completa de como pueden especificarse los rangos.

Cuando trabajamos con campos, es posible especificar un delimitador de campo diferente en lugar de el carácter tabulador. Aquí extraeremos el primer campo del archivo /etc/passwd:

 [me@linuxbox ~]$ cut -d ':' -f 1 /etc/passwd | head
 root
 daemon
 bin
 sys
 sync
 games
 man
 lp
 mail
 news

Usando la opción -d, podemos especificar el punto como carácter delimitador de campos.

Expandiendo los tabuladores

Nuestro archivo distros.txt está perfectamente formateado para extraer campos usando cut. Pero ¿qué si queremos un archivo que pueda ser completamente manipulado por cut con caracteres, en lugar de con campos? Esto requeriría que reemplazáramos los caracteres tabuladores del archivo por el correspondiente número de espacios. Afortunadamente, el paquete GNU Coreutils incluye una herramienta para ésto. Se llama expand, este programa acepta tanto uno como varios argumentos de archivos o entrada estándar, y produce un texto modificado en la salida estándar.

Si procesamos nuestro archivo distros.txt con expand, podemos usar cut -cpara extraer cualquier rango de caracteres del archivo. Por ejemplo, podríamos usar el siguiente comando para extraer el año de salida de nuestra lista, expandiendo el archivo y usando cut para extraer cada carácter de la vigésimo tercera posición al final de la línea:

 [me@linuxbox ~]$ expand distros.txt | cut -c 23-

Coreutils también aporta el programa unexpand para sustituir tabuladores por espacios.


paste

El comando paste hace lo contrario de cut. En lugar de extraer una columna de texto de un archivo, añade una o más columnas de texto a un archivo. Lo hace leyendo múltiples archivos y combinando los campos encontrados en cada archivo en una única cadena en la entrada estándar. Como cut, paste acepta múltiples argumentos de archivo y/o entrada estándar. Para demostrar como opera paste, realizaremos una cirugía a nuestro archivo distros.txt para producir una lista cronológica de lanzamientos.

De nuestro trabajo anterior con sort, primero produciremos una lista de distribuciones ordenadas por fecha y guardaremos el resultado en un archivo llamado distros-by-date.txt:

[me@linuxbox ~]$ sort -k 3.7nbr -k 3.1nbr -k 3.4nbr distros.txt > distros-by-date.txt

A continuación, usaremos cut para extraer los dos primeros campos del archivo (el nombre de distribución y la versión), y almacenar el resultado en un archivo llamado distro-versions.txt:

[me@linuxbox ~]$ cut -f 1,2 distros-by-date.txt > distros-versions.txt
[me@linuxbox ~]$ head distros-versions.txt
Fedora 10
Ubuntu 8.10
SUSE 11.0
Fedora 9
Ubuntu 8.04
Fedora 8
Ubuntu 7.10
SUSE 10.3
Fedora 7
Ubuntu 7.04

La parte final de la preparación es extraer la fecha de liberación y almacenarla en un archivo llamado distro-dates.txt:

[me@linuxbox ~]$ cut -f 3 distros-by-date.txt > distros-dates.txt

[me@linuxbox ~]$ head distros-dates.txt
11/25/2008
10/30/2008
06/19/2008
05/13/2008
04/24/2008
11/08/2007
10/18/2007
10/04/2007
05/31/2007
04/19/2007

Ahora tenemos las partes que necesitamos. Para completar el proceso, usamos pastepara colocar la columna de fechas delante de las de el nombre de la distribución y las versiones, de forma que creando así una lista cronológica. Ésto se hace simplemente usando paste y ordenando sus argumentos en el orden deseado:

 [me@linuxbox ~]$ paste distros-dates.txt distros-versions.txt
 11/25/2008 Fedora 10
 10/30/2008 Ubuntu 8.10
 06/19/2008 SUSE 11.0
 05/13/2008 Fedora 9
 04/24/2008 Ubuntu 8.04
 11/08/2007 Fedora 8
 10/18/2007 Ubuntu 7.10
 10/04/2007 SUSE 10.3
 05/31/2007 Fedora 7
 04/19/2007 Ubuntu 7.04
 12/07/2006 SUSE 10.2
 10/26/2006 Ubuntu 6.10
 10/24/2006 Fedora 6
 06/01/2006 Ubuntu 6.06
 05/11/2006 SUSE 10.1
 03/20/2006 Fedora 5


join

En algunos aspectos, join es como paste en cuanto a que añade columnas a un archivo, pero usa una forma única para hacerlo. Un join es una operación normalmente asociada con las bases de datos relacionales donde los datos de múltiples tablas con un campo clave compartido se combinan para dar el resultado deseado. El programa joinrealiza la misma operación. Une datos de múltiples archivos basándose en un campo clave compartido.

Para ver como se usa una operación join en una base de datos relacional, imaginemos una base de datos muy pequeña consistente en dos tablas, cada una con un único registro. La primera tabla, llamada CUSTOMERS, tiene tres campos: un número de cliente (CUSTNUM), el nombre del cliente (FNAME) y el apellido del cliente (LNAME):

CUSTNUM FNAME LNAME
======= ===== ======
4681934 John  Smith

La segunda tabla se llama ORDERS y contiene cuatro campos: un número de pedido (ORDER-NUM), el número de cliente (CUSTNUM), la cantidad (QUAN) y el producto pedido (ITEM).

ORDERNUM   CUSTNUM QUAN ITEM
========   ======= ==== ====
3014953305 4681934 1    Blue Widget

Fíjate que ambas tablas comparten el campo CUSTNUM. Ésto es importante, ya que permite la relación entre las tablas.

Realizar una operación join nos permitiría combinar los campos en las dos tablas para conseguir un resultado útil, como preparar un pedido. Usando los valores coincidentes en los campos CUSTNUM de ambas tablas, una operación join podría producir lo siguiente:

FNAME LNAME QUAN ITEM
===== ===== ==== ====
John  Smith 1    Blue Widget

Para probar el programa join, necesitaremos crear un par de archivos con una clave compartida. Para hacerlo, usaremos nuestro archivo distros-by-date.txt. Desde este archivo, construiremos dos archivos adicionales, uno conteniendo las fechas de lanzamiento (que será nuestra clave compartida para esta demostración) y los nombres de la versión:

[me@linuxbox ~]$ cut -f 1,1 distros-by-date.txt > distros-names.txt
[me@linuxbox ~]$ paste distros-dates.txt distros-names.txt > distroskey-names.txt
[me@linuxbox ~]$ head distros-key-names.txt
11/25/2008 Fedora
10/30/2008 Ubuntu
06/19/2008 SUSE
05/13/2008 Fedora
04/24/2008 Ubuntu
11/08/2007 Fedora
10/18/2007 Ubuntu
10/04/2007 SUSE
05/31/2007 Fedora
04/19/2007 Ubuntu

y el segundo archivo, que contiene las fechas de lanzamiento y los números de versión:

[me@linuxbox ~]$ cut -f 2,2 distros-by-date.txt > distros-vernums.txt
[me@linuxbox ~]$ paste distros-dates.txt distros-vernums.txt > distro s-key-vernums.txt
[me@linuxbox ~]$ head distros-key-vernums.txt
11/25/2008 10
10/30/2008 8.10
06/19/2008 11.0
05/13/2008 9
04/24/2008 8.04
11/08/2007 8
10/18/2007 7.10
10/04/2007 10.3
05/31/2007 7
04/19/2007 7.04

Ahora tenemos dos archivos con una clave compartida (el campo "fecha de lanzamiento"). Es importante señalar que los archivos deben estar ordenados por el campo clave para que join funcione apropiadamente.

[me@linuxbox ~]$ join distros-key-names.txt distros-key-vernums.txt | head
11/25/2008 Fedora 10
10/30/2008 Ubuntu 8.10
06/19/2008 SUSE 11.0
05/13/2008 Fedora 9
04/24/2008 Ubuntu 8.04
11/08/2007 Fedora 8
10/18/2007 Ubuntu 7.10
10/04/2007 SUSE 10.3
05/31/2007 Fedora 7
04/19/2007 Ubuntu 7.04

Fíjate también que, join usa el espacio en blanco como el delimitador de campos de entrada y un espacio individual como el el delimitador de campos de salida. Este comportamiento puede modificarse especificando opciones. Mira la man page de join para más detalles.


Comparando texto

A menudo es útil comparar versiones de archivos de texto. Para administradores de sistemas y desarrolladores de software, es particularmente importante. Un administrador de sistemas podría, por ejemplo, necesitar comparar un archivo de configuración existente con una versión anterior para diagnosticar un problema del sistema. Igualmente, un programador necesita frecuentemente ver que cambios se le han realizado a los programas a lo largo del tiempo.

comm

El programa comm compara dos archivos de texto y muestra las líneas que son únicas y las líneas que tienen iguales. Para demostrarlo, crearemos dos archivos de texto casi idénticos usando cat:

 [me@linuxbox ~]$ cat > file1.txt
 a
 b
 c
 d 

 [me@linuxbox ~]$ cat > file2.txt
 b
 c
 d
 e

A continuación, compararemos los dos archivos usando comm:

 [me@linuxbox ~]$ comm file1.txt file2.txt
 a
 b
 c
 d
 e

Como podemos ver, comm produce tres columnas de salida. La primera columna contiene líneas únicas en el primer archivo; la segunda columna, las líneas únicas en el segundo archivo; la tercera columna contiene las líneas compartidas por ambos archivos. comm soporta opciones en la forma -n donde n es 1, 2 o 3. Cuando se usan, estas opciones especifican que columna(s) suprimir. Por ejemplo, si sólo queremos obtener las líneas compartidas por ambos archivos, suprimiremos la salida de las columnas uno y dos:

 [me@linuxbox ~]$ comm -12 file1.txt file2.txt
 b
 c
 d

diff

Como el programa comm, diff se usa para detectar diferencias entre archivos. Sin embargo, diff es una herramienta mucho más compleja, soporta muchos formatos de salida y la capacidad de procesar grandes colecciones de archivos de texto a la vez. diff a menudo se usa por los desarrolladores de software para examinar cambios entre versiones diferentes de código fuente de programa, y además tiene la capacidad de examinar recursivamente directorios de código fuente, a menudo llamados árboles fuente. Un uso común para diff es crear archivos diff o parches que se usan por programas como patch (que veremos pronto) para convertir una versión de un archivo (o archivos) a otra versión.

Si usamos diff para ver nuestros archivos de ejemplo previos:

 [me@linuxbox ~]$ diff file1.txt file2.txt
 1d0
 < a
 4a4
 > e

vemos su estilo de salida por defecto: una breve descripción de las diferencias entre dos archivos. En el formato por defecto, cada grupo de cambios va precedido por un comando de cambio en la forma de un rango operación rango para describir las posiciones y tipos de cambios requeridos para convertir el primer archivo en el segundo:

Comandos de cambio de diff

r1ar2 Añade las líneas en la posición r2 del segundo archivo a la posición r1 del primer archivo.
r1cr2 Cambia (reemplaza) las lineas en la posición r1 con las líneas en la posición r2 en el segundo archivo.
r1dr2 Borra las líneas en el primer archivo en la posición r1, que habrían aparecido en el rango r2 del segundo archivo.

En este formato, un rango es una lista separada por comas de la línea inicial y la línea final. Aunque este formato es el que viene por defecto (principalmente por conformidad con POSIX y retrocompatibilidad con versiones tradicionales de Unix de diff), no está tan ampliamente usado como otros formatos opcionales. Dos de los formatos más populares son el formato contextual y el formato unificado.

Cuando lo vemos usando el formato contextual (la opción -c), veremos ésto:

 [me@linuxbox ~]$ diff -c file1.txt file2.txt
 *** file1.txt 2008-12-23 06:40:13.000000000 -0500
 --- file2.txt 2008-12-23 06:40:34.000000000 -0500
 ***************
 *** 1,4 ****
 - a
   b
   c
   d
--- 1,4 ----
   b
   c
   d
 + e

La salida comienza con los nombres de los dos archivos y sus fechas de modificación. El primer archivo está marcado con asteriscos y el segundo archivo está marcado con guiones. A lo largo del resto de la lista, estos marcadores simbolizarán sus respectivos archivos. A continuación, vemos grupos de cambios, incluyendo el número de líneas de contexto circundantes por defecto. En el primer grupo, vemos:

*** 1,4 ***

que indica lineas de la 1 a la 4 en el primer archivo. A continuación vemos:

--- 1,4 ---

que indica lineas de la 1 a la 4 en el segundo archivo. Dentro de un grupo de cambios, las líneas comienzan con uno de estos cuatro indicadores:

Indicadores de cambios de diff en formato contextual

blank Un línea mostrada de contexto. No indica ninguna diferencia entre los dos archivos.
-     Un línea borrada. Esta línea aparecerá en el primer archivo pero no en el segundo.
+     Una línea añadida. Esta línea aparecerá en el segundo archivo pero no en el primero.
!     Una línea modificada. Las dos versiones de la línea se mostrarán, cada una en su respectiva sección del grupo de cambio.

El formato unificado es similar al formato contextual pero es más conciso. Se especifica con la opción -u:

 [me@linuxbox ~]$ diff -u file1.txt file2.txt
 --- file1.txt 2008-12-23 06:40:13.000000000 -0500
 +++ file2.txt 2008-12-23 06:40:34.000000000 -0500
 @@ -1,4 +1,4 @@
 -a
  b
  c
  d
 +e

La diferencia más destacable entre los formatos contextual y unificado es la eliminación de las líneas duplicadas de contexto, haciendo que los resultados del formato unificado sean más cortos que los del formato contextual. En nuestro ejemplo anterior, vemos las marcas de tiempo de los archivos como los del formato contextual, seguidas de la cadena @@ -1,4 +1,4 @@. Ésto indica las líneas del primer archivo y las líneas del segundo archivo descritas en el grupo de cambio. Tras ésto están las líneas en sí, con las tres líneas de contexto por defecto. Cada línea comienza con uno de tres posibles caracteres:

Indicadores de cambios de diff en formato unificado

blank Esta línea la comparten ambos archivos.
-     Esta línea se eliminó del primer archivo.
+     Esta línea se añadió al primer archivo

patch

El programa patch se usa para aplicar cambios a archivos de texto. Acepta salida de diff y generalmente se usa para convertir versiones antiguas de archivos en versiones más nuevas. Consideremos un ejemplo famoso. El kernel Linux es desarrollado por un equipo grande y poco organizado de contribuidores que envían una constante cadena de pequeños cambios al código fuente. El kernel Linux consta de varios millones de líneas de código, mientras que los cambios que hace un contribuidor cada vez son muy pequeños. No tiene sentido que un contribuidor envíe en cada desarrollo un código fuente completo del kernel cada vez que realiza un pequeño cambio. En su lugar, envía un archivo diff. El archivo diff contiene el cambio desde la versión anterior del kernel a la nueva versión con los cambios del contribuidor. El que lo recibe usa el programa patch para aplicar el cambio a su propio código fuente. Usar diff/patch proporciona dos ventajas significativas:

  1. El archivo diff es muy pequeño, comparado con el tamaño completo del código fuente.
  2. El archivo diff muestra de forma concisa el cambio a realizar, permitiendo a los revisores del parche evaluarlo rápidamente.
Claro que, diff/patch funcionará con cualquier archivo de texto, no sólo con código fuente. Será igualmente aplicable a archivos de configuración o cualquier otro texto.

Para preparar un archivo diff para usarlo con patch, la documentación GNU (ver Para Saber Más a continuación) sugiere usar diff de la siguiente manera:

 diff -Naur old_file new_file > diff_file

Donde old_file y new_file son archivos individuales o directorios que contengan archivos. La opción r soporta recursividad en el árbol de directorios.

Una vez que el archivo diff ha sido creado, podemos aplicarlo para parchear el archivo antiguo y convertirlo en el nuevo archivo:

 patch < diff_file

Lo demostraremos con nuestro archivo de pruebas:

[me@linuxbox ~]$ diff -Naur file1.txt file2.txt > patchfile.txt
[me@linuxbox ~]$ patch < patchfile.txt
patching file file1.txt

[me@linuxbox ~]$ cat file1.txt
b
c
d
e

En este ejemplo, hemos creado un archivo diff llamado patchfile.txt y luego hemos usado el programa patch para aplicar el parche. Fíjate que no hemos tenido que especificar un archivo de destino para patch, ya que el archivo diff (en formato unificado) ya contiene los nombres de archivo en la cabecera. Una vez que se aplica el parche, podemos ver que file1.txt ahora coincide con file2.txt.

patch tiene una gran número de opciones, y hay programas adicionales que pueden usarse para analizar y editar parches.

Editando sobre la marcha

Nuestra experiencia con editores de texto ha sido muy interactiva, ya que movemos el cursor manualmente, y luego escribimos nuestros cambios. Sin embargo, hay formas no-interactivas de editar texto también. Es posible, por ejemplo, aplicar una serie de cambios a múltiples archivos con un único comando.


tr

El programa tr se usa para transliterar caracteres. Podemos pensar que esto es un tipo de operación de reemplazo y búsqueda basada en caracteres. La transliteración es el proceso de cambiar caracteres de un alfabeto a otro. Por ejemplo, convertir caracteres de minúscula a mayúscula es transliteración. Podemos realizar esa conversión con tr como sigue:

 [me@linuxbox ~]$ echo "lowercase letters" | tr a-z A-Z
 LOWERCASE LETTERS

Como podemos ver, tr funciona con la entrada estándar, y muestra sus resultados en la salida estándar. tr acepta dos argumentos: una serie de caracteres de donde convertir y una serie correspondiente de caracteres hacia donde convertir. Las series de caracteres pueden expresarse de tres formas diferentes:

Una lista enumerada. Por ejemplo, ABCDEFGHIJKLMNOPQRSTUVWXYZ

Un rango de caracteres. Por ejemplo, A-Z. Fíjate que este método está sujeto a menudo a los mismos problemas que otros comandos, debido al tipo de orden local, y por lo tanto hay que usarlo con precaución.

Clases de caracteres POSIX. Por ejemplo, [:upper;].


En la mayoría de los casos, ambas series de caracteres serán de igual longitud; sin embargo, es posible que la primera serie sea más grande que la segunda, particularmente y queremos convertir muchos caracteres a uno único:

 [me@linuxbox ~]$ echo "lowercase letters" | tr [:lower:] A
 AAAAAAAAA AAAAAAA

Además de la transliteración, tr permite que los caracteres sean simplemente eliminados de la cadena de entrada. Antes, en este capítulo, vimos el problema de convertir archivos de texto MS-DOS a texto estilo-Unix. Para realizar esta conversión, los caracteres de retorno de carro necesitan ser eliminados del final de cada línea. Ésto puede realizarse con tr de la forma siguiente:

tr -d '\r' < dos_file > unix_file

donde dos_file es el archivo a convertir y unix_file es el resultado. Esta forma del comando utiliza la secuencia de escape \r para representar el carácter retorno de carro. Para ver una lista completa de las secuencias y clases de caracteres que soporta tr, prueba:

 [me@linuxbox ~]$ tr --help

tr puede realizar otro truco también. Usando la opción -s, tr puede "squeeze" (borrar) las instancias repetidas de un caracter:

 [me@linuxbox ~]$ echo "aaabbbccc" | tr -s ab
 abccc

Aquí tenemos un cadena que contiene caracteres repetidos. Especificando la lista "ab" a tr, eliminamos las instancias repetidas de las letras de la lista, mientras que deja los caractres que no aparecen en la lista ("c") sin cambios. Fíjate que los caracteres repetidos debe estar contiguos. Si no lo están:

 [me@linuxbox ~]$ echo "abcabcabc" | tr -s ab
 abcabcabc


la eliminación no tendrá efecto.

sed

E
l nombre sed es una abreviatura de stream editor (editor de cadenas). Realiza edición de texto en una cadena de texto, en 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 scripts que contenga múltiples comandos, y ejecuta estos comandos línea a línea de la cadena 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 cadena de texto de una palabra usando echo y entubándolo en sed. sed, a continuación, ejecuta la instrucción s/front/back/ del texto de la cadena 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 simple letra. En el ejemplo anterior, el comando sustitución se representa con la letra s y es seguido por las cadenas a buscar y reemplazar, 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 que línea(s) de la cadena de entrada será editada. Si se omite la dirección, el comando editor se ejecutará en cada línea de la cadena 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 linea 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 cadena de entrada no tiene línea 2.

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

Notación de direcciones en sed

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. Para que esto sea efectivo sin embargo, tenemos que incluir la opción -n (la opción no auto-imprmir) 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:

comandos de edición básicos de sed

=  
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 al principio 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 si 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 encontrado por 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 delantera que sigue a replacement, puede especificarse un parámetro 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. Veremos antes como el campo fecha en distros.txt no estaba en formáto "computer-friendly". 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 consumirí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 medios "sólo-escritura". Podemos escribirlas, pero a menudo no podemos leerlas. Antes de que estemos tentados de huir de pánico de este comando, veamos como ha sido construido. Primero, sabemos que el comando tendrá está 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, una barra, el mes, una barra 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 la barra extra en nuestra expresión confundirá 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 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 etiquetas opcionales que puede seguir a la cadena de reemplazo. La más importante es la etiqueta 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 etiqueta 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 vía la línea de comandos. También es posible construir comandos más complejos en un archivo de scripts usando la opción -f. Para hacer una demostración, usaremos sed con nuestro archivo distros.txt para construir 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íneas. Aquí se usan para especificar en qué líneas de entrada hay que actuar. Las direcciones de líneas 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 cadena de texto sin señalar al interprete (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 retorno de carro plano 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 seguida inmediatamentepor un retorno de carro. No se permiten espacios intermedios.

La línea 7 es nuestro comando de buscar y reemplazar. Como no está precedido por ninguna dirección, cada línea de la cadena 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 caracters POSIX. De nuevo, como el comando y no está precedido por ninguna dirección, se aplica a cada línea de la cadena de entrada.

ROT13: El anillo decodificador no tan secreto

Un uso divertido de tr es realizar un cifrado de texto ROT13. ROT13 es un tipo de cifrado trivial basado en un cifrado de sustitución simple. Llamar a ROT13 "cifrado" es ser generosos; "ofuscación de texto" es más correcto. Se usa algunas veces en texto para esconder texto potencialmente ofensivo. El método simplemente mueve cada carácter 13 posiciones hacia delante en el alfabeto. Como esto supone la mitad de los 26 caracteres posibles, aplicando el algoritmo una segunda vez al texto lo restauramos a su forma original. Para realizar este cifrado con tr:

echo "secret text" | tr a-zA-Z n-za-mN-ZA-M
frperg grkg


echo "secret text" | tr a-zA-Z n-za-mN-ZA-M
Realizando el mismo procedimiento una segunda vez obtenemos la traducción:

echo "frperg grkg" | tr a-zA-Z n-za-mN-ZA-M
secret text

Numerosos programas de correo electrónico y lectores de noticias de Usenet soportan cifrado ROT13. Wikipedia tiene un buen artículo sobre el asuntecho "secret text" | tr a-zA-Z n-za-mN-ZA-Mo:

https://es.wikipedia.org/wiki/ROT13

A la gente que le gusta sed también le gusta...


sed es un programa muy capaz, permite realizar tareas de edición muy complejas a cadenas de texo. A menudo se usa para tareas de una línea en lugar de para scripts. Muchos usuarios prefieren otras herramientas para tareas más grandes. Los más populares son awk y perl. Éstos van más allá de meras herramientas como los programas que hemos visto aquí, y se adentran en el reino de los lenguajes de programación completos. perl, en particular, se usa a menudo en lugar de los scripts de shell para muchas tareas de administración y mantenimiento de sistemas, así como un medio muy popular en desarrollos web. awk es un poco más especializado. Su fuerza específica es la capacidad de manipular datos tabulares. Se parece a sed en que los programas awk normalmente procesan archivos de texto línea a línea, usando un esquema similar al concepto de sed de dirección seguida de una acción. Aunque tanto awk como perl que fuera del objetivo de este libro, son muy buenas experiencias para el usuario de la línea de comandos de Linux.

aspell

La última herramienta que veremos es aspell, un corrector ortográfico interactivo. El programa aspell es el sucesor de un programa anterior llamado ispell, y se puede usar, en mayor parte, como un sucesor. Aunque el programa aspell se usa mayormente por otros programas que requieren capacidad de corrección ortográfica, también puede usarse muy eficientemente como una herramienta independiente de la línea de comandos. Tiene la capacidad de comprobar inteligentemente varios tipos de archivos de texto, incluyendo documentos HTML, C/C++, programas, mensajes de correo y otros tipos de textos especializados.

Para comprobar ortográficamente un archivo de texto con un texto de prosa simple, puede usarse así:

 aspell check textfile

donde textfile es el nombre del archivo a comprobar. Como ejemplo práctico, crearemos un archivo de texto simple llamado foo.txt que contengan algunos errores ortográficos intencionados:

 [me@linuxbox ~]$ cat > foo.txt
 The quick brown fox jimped over the laxy dog.

A continuación, comprobamos el archivo usando aspell:

 [me@linuxbox ~]$ aspell check foo.txt
Como aspell es interactivo en el modo de comprobación, veremos una pantalla como esta:

The quick brown fox jimped over the laxy dog.

                                                    
1) jumped                               6) wimped
2) gimped                               7) camped
3) comped                               8) humped
4) limped                               9) impede
5) pimped                               0) umped
i) Ignore                               I) Ignore all
r) Replace                              R) Replace all
a) Add                                  l) Add Lower
b) Abort                                x) Exit

                                                    
?

En la parte de arriba de la pantalla, vemos nuestro texto con una palabra sospechosa señalada. En el centro, vemos diez sugerencias ortográficas numeradas del cero al nueve, seguida de una lista de otras opciones posibles. Finalmente, muy abajo, vemos un prompt preparado para aceptar nuestra elección.

Si pulsamos la tecla 1, aspell reemplaza la palabra incorrecta por la palabra "jumped" y se mueve a la siguiente palabra mal escrita, que es "laxy". Si seleccionamos el reemplazo "lazy", aspell la cambia y termina. Una vez que aspell ha terminado, podemos examinar nuestro archivo y ver que los errores han sido corregidos:

 [me@linuxbox ~]$ cat foo.txt
 The quick brown fox jumped over the lazy dog.

A menos que se le indique en la línea de comandos la opción --dont-backup, aspell crea una copia de seguridad del archivo con el texto original añadiéndole la extensión .bak al nombre del archivo.

Presumiendo de nuestra habilidad editando con sed, pondremos nuestro errores ortográficos de nuevo para que podamos reutilizar nuestro archivo.

 [me@linuxbox ~]$ sed -i 's/lazy/laxy/; s/jumped/jimped/' foo.txt

La opción -i de sed para editar el archivo "in-situ", significa que en lugar de enviar la salida editada a la salida estándar, reescribirá el archivo con los cambios aplicados. También vemos la capacidad de colocar más de un comando de edición en la línea separándolos con un punto y coma.

A continuación, veremos como aspell puede manejar diferentes tipos de archivos de texto. Usando un editor de texto como vim (los aventureros pueden probar sed), añadiremos algunas etiquetas HTML a nuestro archivo:

 <html>
   <head>
     <title>Mispelled HTML file</title>
   </head>
   <body>
     <p>
The quick brown fox jimped over the laxy dog.</p>
   </body>
 </html>


Ahora, si tratamos de comprobar la ortografía de nuestro archivo modificado, encontramos un problema. Si lo hacemos así:

 [me@linuxbox ~]$ aspell check foo.txt

tendremos ésto:

 <html>
   <head>
     <title>Mispelled HTML file</title>
   </head>
   <body>
     <p>The quick brown fox jimped over the laxy dog.</p>
   </body>
 </html>


                                                    
1) HTML                     4) Hamel
2) ht ml                    5) Hamil
3) ht-ml                    6) hotel

i) Ignore                   I) Ignore all
r) Replace                  R) Replace all
a) Add                      l) Add Lower
b) Abort                    x) Exit

                                                    
?

aspell verá el contenido de las etiquetas HTML como errores ortográficos. El problema puede solucionarse incluyendo la opción de modo de comprobación -H(HTML), como ésto:

 [me@linuxbox ~]$ aspell -H check foo.txt

que tendrá como resultado ésto:

 <html>
   <head>
     <title>Mispelled HTML file</title>
   </head>
   <body>
     <p>The quick brown fox jimped over the laxy dog.</p>
   </body>
 </html>

                                                    
1) Mi spelled               6) Misapplied
2) Mi-spelled               7) Miscalled
3) Misspelled               8) Respelled
4) Dispelled                9) Misspell
5) Spelled                  0) Misled
i) Ignore                   I) Ignore all
r) Replace                  R) Replace all
a) Add                      l) Add Lower
b) Abort                    x) Exit

                                                    
?

El HTML se ignora y sólo las porciones no-HTML del archivo se comprueban. En este modo, el contenido de las etiquetas HTML son ignoradas y no se les comprueba la ortografía. Sin embargo, el contenido de las etiquetas ALT, que es bueno que se comprueben, sí se chequean en este modo.

Nota: Por defecto, aspell ignorará URLs y direcciones de correo electrónico en el texto. Este comportamiento puede ser modificado con opciones de la línea de comandos. También es posible especificar qué etiquetas son comprobadas y cuales son obviadas. Mira la man page de aspell para más detalles.


Resumiendo

En este capítulo, hemos vimos algunas de las muchas herramientas de la línea de comandos que operan sobre textos. En el siguiente capítulo, veremos algunas más. Cierto es que, puede no ser inmediatamente obvio como o porqué tendrías que usar estas herramientas en el día a día, por lo que hemos tratado de mostrar algunos ejemplos semi-prácticos de su uso. Encontraremos en capítulos posteriores que estas herramientas forman la base de una colección de herramientas que se usa para resolver una serie de problemas prácticos. Ésto será particularmente cierto cuando entremos en el shell scripting, donde estas herramientas mostrarán realmente su valor.


Para saber más

La web del proyecto GNU contiene muchas guías online de las herramientas que hemos visto en éste capítulo:

Prueba también a googlear “sed one liners”, “sed cheat sheets”

Crédito Extra

Hay unos pocos comandos interesantes más de manipulación de texto que merece la pena investigar. Entre ellos: split (divide archivos en trozos), csplit (divide archivos en trozos basados en contexto) y sdiff (unión de lado a lado de diferencias entre archivos).

No hay comentarios:

Publicar un comentario

Nota: solo los miembros de este blog pueden publicar comentarios.