jueves, 28 de septiembre de 2017

Expresiones Regulares

En los próximos capítulos, vamos a ver herramientas que se usan para manejar texto. Como hemos visto, los datos de texto juegan un papel importante en todos los sistemas como-Unix, por ejemplo Linux. Pero antes de poder apreciar al completo todas las funciones ofrecidas por estas herramientas, tenemos que examinar primero una tecnología que está frecuentemente asociada a los usos más sofisticados de éstas herramientas - las expresiones regulares.

Según hemos ido navegando por las muchas funciones y soluciones ofrecidas por la línea de comandos, hemos encontrado algunas funciones y comandos verdaderamente antiguos, como la expansión del shell y el entrecomillado, los atajos de teclado y el historial de comandos, no hace falta mencionar al editor vi. Las expresiones regulares continúan esta "tradición" y pueden ser (podría decirse) la función más arcaica de todas ellas. Esto no quiere decir que el tiempo que nos lleve aprender sobre ellas no merezcan el esfuerzo. Justo lo contrario. Un buen entendimiento nos permitirá usarlas para realizar cosas impresionantes, aunque su verdadero valor no sea aparente inicialmente.


¿Qué son las expresiones regulares?

Simplemente digamos que, las expresiones regulares son notaciones simbólicas usadas para identificar patrones en el texto. En muchos aspectos, se parecen al método de los comodines del shell para encontrar archivos y rutas, pero en una escala mucho más grande. Las expresiones regulares están soportadas por muchas herramientas de la línea de comandos y por la mayoría de los lenguajes de programación para facilitar la solución de problemas de manipulación de texto. Sin embargo, para confundir más las cosas, no todas las expresiones regulares con iguales; varían un poco de herramienta a herramienta y de lenguaje de programación a lenguaje de programación. Para nuestro tema, nos limitaremos a expresiones regulares como se describen en el estándar POSIX (que cubrirá la mayoría de las herramientas de la línea de comandos), al contrario que muchos lenguajes de programación (principalmente Perl), que usa un catálogo algo más grande y rico de notaciones.

grep

El principal programa que usaremos para trabajar con expresiones regulares es nuestro antiguo colega, grep. El nombre "grep" deriva realmente de la frase "global regular expresión print - impresión global de expresiones regulares", luego podemos ver que grep tiene algo que ver con las expresiones regulares. En esencia, grep busca en archivos de texto la coincidencia con una expresión regular especificada y produce líneas que contengan una coincidencia a la salida estándar.

Hasta ahora, hemos usado grep con cadenas concretas, así:

 [me@linuxbox ~]$ ls /usr/bin | grep zip

Esto listará todos los archivos en el directorio /usr/bin cuyos nombres contengan la cadena "zip".

El programa grep acepta opciones y argumentos de esta forma:

 grep [opciones] regex [archivo...]

donde regex es una expresión regular.

Aquí tenemos una lista de las opciones más comunes usadas en grep:

Opciones de grep

-i Ignora las mayúsculas. No distingue entre caracteres mayúscula y minúscula. También puede indicarse como --ignore-case.


-v Coincidencia inversa. Normalmente, grep muestra líneas que contienen un patrón. Esta opción hace que grep muestra todas las líneas que no contengan un patrón. También puede indicarse como --invert-match.

-c Muestra el número de coincidencias (o no-coincidencias si también se especifica la opción -v) en lugar de las propias líneas. También puede indicarse como --count.

-l Muestra el nombre de cada archivo que contenga un patrón en lugar de las propias líneas. También puede indicarse como --files-witth-matches.

-L Como la opción -l, pero muestra sólo los nombres de los archivos que no contengan el patrón. Puede indicarse también como --files-without-match.

-n Precede cada línea coincidente con el número de la línea dentro del archivo. También puede indicarse como --line-number.

-h Para búsquedas multi-archivo, suprime los nombres de archivo de la salida. También puede indicarse como --no-filename.

Para explorar más a fondo grep, creemos algunos archivos de texto para buscar:

 [me@linuxbox ~]$ ls /bin > dirlist-bin.txt
 [me@linuxbox ~]$ ls /usr/bin > dirlist-usr-bin.txt
 [me@linuxbox ~]$ ls /sbin > dirlist-sbin.txt
 [me@linuxbox ~]$ ls /usr/sbin > dirlist-usr-sbin.txt
 [me@linuxbox ~]$ ls dirlist*.txt
 dirlist-bin.txt dirlist-sbin.txt dirlist-usr-sbin.txt
 dirlist-usr-bin.txt

Podemos realizar una búsqueda simple de nuestra lista de archivos así:

 [me@linuxbox ~]$ grep bzip dirlist*.txt
 dirlist-bin.txt:bzip2
 dirlist-bin.txt:bzip2recover

En este ejemplo, grep busca todos los archivos listados por la cadena bzip y encuentra dos coincidencias, ambos en el archivo dirlist-bin.txt. Si sólo estuviéramos interesados en la lista de archivos que contienen las coincidencias, podríamos especificar la opción -l:

 [me@linuxbox ~]$ grep -l bzip dirlist*.txt
 dirlist-bin.txt

Inversamente, si quisiéramos sólo ver una lista de los archivos que no contienen un patrón, podríamos hacer ésto:

 [me@linuxbox ~]$ grep -L bzip dirlist*.txt
 dirlist-sbin.txt
 dirlist-usr-bin.txt
 dirlist-usr-sbin.txt

Metacaracteres y literales

Aunque no parezca aparente, nuestras búsquedas con grep han usado expresiones regulares todo el tiempo, aunque muy simples. La expresión regular "bzip" significa que una coincidencia ocurrirá sólo si la línea del archivo contiene al menos cuatro caracteres y que en algún lugar de la línea los caracteres "b", "z", "i" y "p" se encuentran en ese orden, sin otros caracteres en medio. Los caracteres en la cadena "bzip" son caracteres literales que coinciden consigo mismos. Además de los literales, las expresiones regulares pueden también incluir metacaracteres que se usan para especificar coincidencias más complejas. Los metacaracteres para las expresiones regulares son los siguientes:

^ $ . [ ] { } - ? * + ( ) | \

Todos los demás caracteres se consideran literales, aunque la barra invertida se utiliza en algunos casos para crear meta secuencias, así como para que los metacaracteres puedan ser "escapados" y tratados como literales en lugar de ser interpretados como metacaracteres.

Nota: Como podemos ver, muchos metacaracteres de expresiones regulares son también caracteres que tienen significado para el shell cuando se realiza la expansión. Cuando pasamos expresiones regulares que contienen metacaracteres en la línea de comandos, es vital que estén entrecomillados para prevenir que el shell trate de expandirlos.


El carácter cualquiera

El primer metacaracter que veremos es el punto, que se usa para buscar cualquier carácter. Si lo incluimos en una expresión regular, encontrará cualquier carácter en esa posición. Aquí tenemos un ejemplo:

 [me@linuxbox ~]$ grep -h '.zip' dirlist*.txt
 bunzip2
 bzip2
 bzip2recover
 gunzip
 gzip
 funzip
 gpg-zip
 preunzip
 prezip
 prezip-bin
 unzip
 unzipsfx

Hemos buscado cualquier linea en nuestro archivos que coincidan con la expresión regular ".zip". Hay un par de cosas interesantes a tener en cuenta en los resultados. Fíjate que el programa zip no ha sido encontrado. Es porque la inclusión del metacaracter punto en nuestra expresión regular incrementa la longitud de la coincidencia requerida a cuatro caracteres, y como el nombre "zip" sólo contiene tres, no coincide. También, si algún archivo en nuestra lista contiene la extensión .zip, debería haber coincidido también, porque el punto en la extensión del archivo es tratado como "cualquier carácter" también.


Anclas

El símbolo de intercalación (^) y el signo del dolar ($) se tratan como anclas en la expresiones regulares. Esto significa que hacen que la coincidencia ocurra sólo si la expresión regular se encuentra al principio de la línea (^) o al final de la línea ($):

 [me@linuxbox ~]$ grep -h '^zip' dirlist*.txt
 zip
 zipcloak
 zipgrep
 zipinfo
 zipnote
 zipsplit
 [me@linuxbox ~]$ grep -h 'zip$' dirlist*.txt
 gunzip
 gzip
 funzip
 gpg-zip
 preunzip
 prezip
 unzip
 zip
 [me@linuxbox ~]$ grep -h '^zip$' dirlist*.txt
  zip

Aquí hemos buscado en la lista de archivos si la cadena "zip" se encuentra al principio de línea, al final de la línea o en una línea donde está tanto al principio como al final de la línea (p.ej., que él mismo sea la línea). Fíjate que la expresión ‘^$’ (un principio y un final con nada en medio) encontrará líneas en blanco.


Un ayudante de crucigramas

Incluso con nuestro limitado conocimiento de las expresiones regulares en este momento, podemos hacer algo útil.

A mi esposa le encantan los crucigramas y a veces me pide ayuda con alguna pregunta concreta. A veces como, "¿Que palabra de cinco letras cuya tercera letra es "j" y la ultima letra es "r" significa...?" Este tipo de preguntas me hacen pensar. ¿Sabes que tu sistema Linux tiene un diccionario? Lo tiene. Echa un vistazo al directorio /usr/share/dict y encontrarás uno, o varios. Los archivos del diccionario localizados allí son sólo largas listas de palabras, una por línea, ordenadas alfabéticamente. En mi sistema, el archivo words contiene alrededor de 98.500 palabras.
Para encontrar posibles respuestas a la pregunta del crucigrama anterior, podríamos hacer ésto:

 [me@linuxbox ~]$ grep -i '^..j.r$' /usr/share/dict/words
 Major
 major

Usando la expresión regular, podemos encontrar todas las palabras del archivo del diccionario que tienen cinco letras y tienen una "j" en la tercera posición y una "r" en la última posición.


Expresiones entre corchetes y clases de caracteres

Además de encontrar un carácter en una determinada posición en nuestra expresión regular, podemos también encontrar un único carácter de una colección específica de caracteres usando expresiones entre corchetes. Con las expresiones entre corchetes, podemos especificar una colección de caracteres (incluyendo caracteres que en otro casos serían interpretados como metacaracteres) para que sean encontrados. En este ejemplo, usamos una colección de dos caracteres:

 [me@linuxbox ~]$ grep -h '[bg]zip' dirlist*.txt
 bzip2
 bzip2recover
 gzip

encontramos cualquier línea que contenga la cadena "bzip" o "gzip".

Una colección puede contener cualquier número de caracteres, y los metacaracteres perder su significado especial cuando se coloquen entre corchetes. Sin embargo, hay dos casos en que los metacaracteres se usan dentro de las expresiones entre corchetes, y tienen significados diferentes. El primero es el símbolo de intercalación (^), que se usa para indicar negación; el segundo es el guión (-), que se usa para indicar un rango de caracteres.


Negación

Si el primer carácter en una expresión entre corchetes es el símbolo de intercalación (^), los caracteres restantes se toman como una colección de caracteres que no deben estar presentes en la posición dada. Podemos hacer esto modificando nuestro ejemplo anterior:

 [me@linuxbox ~]$ grep -h '[^bg]zip' dirlist*.txt
 bunzip2
 gunzip
 funzip
 gpg-zip
 preunzip
 prezip
 prezip-bin
 unzip
 unzipsfx

Con la negación activada, obtenemos una lista de archivos que contienen la cadena "zip" precedida de cualquier carácter excepto "b" o "g". Fíjate que el archivo zip no ha sido encontrado. Una configuración de negación de carácter requiere todavía un carácter en la posición dada, pero el carácter no debe ser un miembro de la lista de negaciones.

El símbolo de intercalación sólo implica negación si es el primer carácter dentro de una expresión entre corchetes; de otra forma, pierde su significado especial y pasa a ser un carácter ordinario en la lista.


Rangos de caracteres tradicionales

Si queremos construir una expresión regular que encuentre cualquier archivo en nuestra lista que empiece con una letra mayúscula, podríamos hacer ésto:

[me@linuxbox ~]$ grep -h '^[ABCDEFGHIJKLMNOPQRSTUVWXZY]' dirlist*.txt

Es simplemente una forma de poner todas las letras mayúsculas en una expresión entre corchetes. Pero la idea de escribir todo eso es muy preocupante, así que aquí tenemos otra forma:

 [me@linuxbox ~]$ grep -h '^[A-Z]' dirlist*.txt
 MAKEDEV
 ControlPanel
 GET
 HEAD
 POST
 X
 X11
 Xorg
 MAKEFLOPPIES
 NetworkManager
 NetworkManagerDispatcher

Usando un rango de tres caracteres, podemos abreviar las 26 letras. Cualquier rango de caracteres puede expresarse de esta forma incluyendo rangos múltiples, como esta expresión que encuentra todos los archivos cuyo nombre empieza con letras y números:

 [me@linuxbox ~]$ grep -h '^[A-Za-z0-9]' dirlist*.txt

En los rangos de caracteres, vemos que el carácter guión se trata especialmente, entonces ¿cómo incluimos el carácter guión en una expresión entre corchetes? Haciéndolo el primer carácter en la expresión. Considera estos dos ejemplos:

 [me@linuxbox ~]$ grep -h '[A-Z]' dirlist*.txt

Ésto encontrará todos los archivos cuyo nombre contiene una letra mayúscula. Mientras que:

 [me@linuxbox ~]$ grep -h '[-AZ]' dirlist*.txt

encontrará todos los archivos cuyo nombre contiene un guión, o una letra "A" mayúscula o una letra "Z" mayúscula.


Clases de caracteres POSIX

Los rangos tradicionales de caracteres son una forma fácilmente compresible y efectiva de manejar el problema de especificar colecciones de caracteres rápidamente. Desafortunadamente, no siempre funcionan. Aunque no hemos encontrado problemas usando grep hasta ahora, podríamos tener problemas con otros programas.

Volviendo al capítulo 4, vimos como los comodines se usan para realizar expansiones en los nombres de archivos. En dicho tema, dijimos que los rangos de caracteres podían usarse de forma casi idéntica a la forma en que se usan en expresiones regulares, pero aquí está el problema:

 [me@linuxbox ~]$ ls /usr/sbin/[ABCDEFGHIJKLMNOPQRSTUVWXYZ]*
 /usr/sbin/MAKEFLOPPIES
 /usr/sbin/NetworkManagerDispatcher
 /usr/sbin/NetworkManager

(Dependiendo de la distribución Linux, obtendremos una lista diferente de archivos, posiblemente una lista vacía. Este ejemplo es de Ubuntu). Este comando produce el resultado esperado - una lista de los archivos cuyos nombre comienzan con una letra mayúscula, pero:

 [me@linuxbox ~]$ ls /usr/sbin/[A-Z]*
 /usr/sbin/biosdecode
 /usr/sbin/chat
 /usr/sbin/chgpasswd
 /usr/sbin/chpasswd
 /usr/sbin/chroot
 /usr/sbin/cleanup-info
 /usr/sbin/complain
 /usr/sbin/console-kit-daemon

con este comando obtenemos un resultado completamente diferente (sólo se muestra una lista parcial de resultados). ¿Por qué? Es una larga historia, pero aquí tienes un resumen:

En la época en que Unix fue desarrollado por primera vez, sólo entendía caracteres ASCII, y esta característica refleja este hecho. En ASCII, los primeros 32 caracteres (los números 0 a 31) son códigos de control (cosas como tabuladores, espacios y retornos de carro). Los siguientes 32 (32 a 63) contienen caracteres imprimibles, incluyendo la mayoría de los signos de puntuación y los números del cero al nueve. Los siguientes 32 (64 a 95) contienen las letras mayúsculas y algunos signos de puntuación más. Los últimos 31 (números del 96 al 127) contienen las letras mayúsculas y todavía más signos de puntuación. Basándose en este ordenamiento, los sistemas que usan ASCII utilizan un orden de colación que tiene esta pinta:

ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz

Esto difiere del orden correcto del diccionario, que es así:

aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTuUvVwWxXyYzZ

Cuando la popularidad de Unix se expandió fuera de los Estados Unidos, creció la necesidad de soportar caracteres que no existen en Inglés U.S. La tabla ASCII se incrementó para usar ochos bits completos, añadiendo los números de caracteres 128-255, que albergan muchos más idiomas. Para soportar esta capacidad, los estándares POSIX introdujeron un concepto llamado un local, que puede ajustarse para seleccionar la configuración de caracteres necesarias para una localización particular. Podemos ver la configuración de idioma de nuestro sistema usando este comando:

 [me@linuxbox ~]$ echo $LANG
 en_US.UTF-8

Con esta configuración, las aplicaciones que cumplen con POSIX usarán el orden del diccionarios en lugar del orden ASCII. Ésto explica el comportamiento de los comandos anteriores. Un rango de caracteres de [A-Z] cuando es interpretado en orden del diccionario incluye todos los caracteres alfabéticos excepto la "a" minúscula, de ahí nuestros resultados.

Para evitar este problema, el estándar POSIX incluye un número de clases de caracteres que proporcionan rango útiles de caracteres. Está descrito en la siguiente tabla:

Clases de caracteres POSIX

[:alnum:]
Los caracteres alfanuméricos. En ASCII, equivalente a: [A-Za-z0-9]


[:word:]
Los mismo que [:alnum:], con el añadido del carácter subrayado (_).

[:alpha:]
Los caracteres alfabéticos. En ASCII, equivalente a: [a-Za-z]

[:blank:]
Incluye los caracteres del espacio y tabulador.

[:cntrl:]
Los caracteres de control ASCII. Incluyen los caracteres ASCII del 0 al 31 y 127.

[:digit:]
Los números del cero al nueve.

[:graph:]
Los caracteres visibles. En ASCII, incluye los caracteres del 33 al 126.

[:lower:]
Las letras minúsculas.

[:punct:]
Los símbolos de puntuación. En ASCII, equivalente a: [-!"#$%&'()*+,./:;<=>?@[\\\]_`{|}~]

[:print:]
Los caracteres imprimibles. Los caracteres de [:graph:]más el carácter espacio.

[:space:]
Los caracteres de espacio en blanco, incluyendo el espacio, el tabulador, el salto de carro, nueva linea, tabulador vertical, y salto de página. En ASCII equivalente :
[ \t\r\n\v\f]

[:upper:]
Los caracteres mayúsculas.

[:xdigit:]
Los caracteres usados para expresar números hexadecimales. En ASCII, equivalente a:
[0-9A-Fa-f]

Incluso con las clases de caracteres, sigue sin haber una forma conveniente de expresar rangos parciales, como [A-M].

Usando las clases de caracteres, podemos repetir nuestro listado de directorio y ver un resultado mejorado:

 [me@linuxbox ~]$ ls /usr/sbin/[[:upper:]]*
 /usr/sbin/MAKEFLOPPIES
 /usr/sbin/NetworkManagerDispatcher
 /usr/sbin/NetworkManager

Recuerda, sin embargo, que esto no es un ejemplo de expresión regular, es por el contrario el resultado de una expansión de ruta de shell. Lo vemos porque las clases de caracteres POSIX pueden usarse en ambos casos.

Volviendo al orden tradicional

Puedes optar porque tu sistema use el orden tradicional (ASCII) cambiando el valor de la variable de entorno LANG. Como vimos antes, la variable LANG contiene el nombre del idioma y catálogo de caracteres usados en configuración local. Este valor fue determinado originalmente cuando seleccionaste un idioma de instalación cuando tu Linux fue instalado.

Para ver la configuración local, usa el comando locale:

 [me@linuxbox ~]$ locale
 LANG=en_US.UTF-8
 LC_CTYPE="en_US.UTF-8"
 LC_NUMERIC="en_US.UTF-8"
 LC_TIME="en_US.UTF-8"
 LC_COLLATE="en_US.UTF-8"
 LC_MONETARY="en_US.UTF-8"
 LC_MESSAGES="en_US.UTF-8"
 LC_PAPER="en_US.UTF-8"
 LC_NAME="en_US.UTF-8"
 LC_ADDRESS="en_US.UTF-8"
 LC_TELEPHONE="en_US.UTF-8"
 LC_MEASUREMENT="en_US.UTF-8"
 LC_IDENTIFICATION="en_US.UTF-8"
 LC_ALL=

Para cambiar el local para usar el comportamiento tradicional de Unix, cambia la variable LANG a POSIX:

 [me@linuxbox ~]$ export LANG=POSIX

Fíjate que este cambio convierte tu sistema a Inglés U.S. (más específicamente, ASCII) en su catálogo de caracteres, así que asegúrate que es realmente lo que quieres.

Puedes hacer que este cambio sea permanente añadiendo esta línea a tu archivo .bashrc:

 export LANG=POSIX


POSIX básico vs. Expresiones regulares extendidas

Justo cuando pensábamos que no podía ser más confuso, descubrimos que POSIX también divide las expresiones regulares en dos tipos: expresiones regulares básicas (BRE - Basic regular expressions) y expresiones regulares extendidas (ERE - extended regular expressions). Las funciones que hemos visto hasta ahora son soportadas por cualquier aplicación que sea compatible con POSIX e implemente BRE. Nuestro programa grep es uno de esos programas.

¿Cuál es la diferencia entre BRE y ERE? Es un asunto de metacaracteres. Con BRE, se reconocen los siguientes metacaracteres:

^ $ . [ ] *

Todos los demás caracteres se consideran literales. Con ERE, se añaden los siguientes metacaracteres (y sus funciones asociadas):

( ) { } ? + |

Sin embargo (y esta es la parte divertida), los caracteres "(", ")", "{" y "}" se tratan como metacaracters en BRE si son escapados con una barra invertida, mientras que con ERE, preceder cualquier metacaracter con una barra invertida hace que sea tratado como un literal. Cualquier rareza que se presente será tratada en los temas siguientes.

Como las funciones que vamos a ver a continuación son parte de ERE, vamos a necesitar un grep diferente. Tradicionalmente, se ha utilizado el programa egrep, pero la versión GNU de grep también soporta expresiones regulares cuando se usa la opción -E.


POSIX

Durante los años ochenta, Unix se volvió un sistema operativo comercial muy popular, pero alrededor de 1988, el mundo Unix se volvió muy confuso. Muchos fabricantes de ordenadores licenciaron el código fuente Unix de sus creadores, AT&T, y vendieron varias versiones del sistema operativo con sus equipos. Sin embargo, en su esfuerzo de crear diferenciación de producto, cada fabricante añadió cambios y extensiones propietarias. Ésto empezó a limitar la compatibilidad del software. Como siempre pasa con los vendedores propietarios, cada uno intentaba un juego ganador de "atrapar" a sus clientes. La época oscura de la historia de Unix es conocida hoy en día como "la Balcanización".

Aparece el IEEE (Institute of Electrical and Electronics Engineers - Instituto de Ingenieros Eléctricos y Electrónicos). A medidados de los 80, el IEEE comenzó a desarrollar una serie de estándares que definieran como se comportan los sistemas Unix (y como-Unix). Estos estándares, formalmente conocidos como IEEE 1003, definen lasapplication programing interfaces - Interfaces de programación de aplicaciones (APIs), shell y utilidades que deben encontrarse en un sistema como-Unix estándar. El nombre "POSIX", que viene de Portable Operating System Interface - Interfaz de Sistema Operativo Portable (con la "X" al final para que tenga más chispa), fue sugerido por Richard Stallman (si, ese Richard Stallman), y fué adoptado por el IEEE.


Alternancia

La primera característica de las expresiones regulares que veremos se llama alternancia, que es la función que nos permite que una coincidencia ocurra de entre una serie de expresiones. Justo como una expresión entre llaves que permite que un carácter único se encuentre dentro de una serie de caracteres especificados, la alternancia permite coincidencias dentro de una serie de cadenas y otras expresiones regulares.

Para demostrarlo, usaremos grep junto con echo. Primero, probemos una antigua coincidencia sencilla:

 [me@linuxbox ~]$ echo "AAA" | grep AAA
 
AAA
 [me@linuxbox ~]$ echo "BBB" | grep AAA
 [me@linuxbox ~]$

Un ejemplo muy sencillo, en el que entubamos la salida de echo dentro de grep y vemos el resultado. Cuando ocurre una coincidencia, vemos que se imprime; cuando no hay coincidencias, no vemos resultados.

Ahora añadiremos alternancia, indicada por el metacaracter de la barra vertical:

 [me@linuxbox ~]$ echo "AAA" | grep -E 'AAA|BBB'
 AAA
 [me@linuxbox ~]$ echo "BBB" | grep -E 'AAA|BBB'
 BBB
 [me@linuxbox ~]$ echo "CCC" | grep -E 'AAA|BBB'
 [me@linuxbox ~]$

Aquí vemos la expresión regular 'AAA|BBB', que significa "encuentra la cadena AAAo la cadena BBB." Fíjate que como es una característica extendida, añadimos la opción -E a grep (aunque podríamos simplemente usar el programa egrep en su lugar), e incluiremos la expresión regular entre comillas para prevenir que el shell interprete el metacaracter de la barra vertical como el operador de entubar:

 [me@linuxbox ~]$ echo "AAA" | grep -E 'AAA|BBB|CCC'
 AAA

Para combinar la alternancia con otros elementos de expresiones regulares, podemos usar () para separar la alternancia:

 [me@linuxbox ~]$ grep -Eh '^(bz|gz|zip)' dirlist*.txt

Esta expresión encontrará los nombres de archivo en nuestras listas que empiecen con "bz", "gz" o "zip". Si eliminamos los paréntesis, el significado de esta expresión regular:

 [me@linuxbox ~]$ grep -Eh '^bz|gz|zip' dirlist*.txt

cambia y encuentra cualquier nombre de archivo que comience por "bz" o contenga "gz" o "zip".


Cuantificadores

Las expresiones regulares extendidas soportan varias formas de especificar el número de veces que se encuentra un elemento.

? - Encuentra un elemento cero u una vez

Este cuantificador significa, en la práctica, "Haz el elemento precedente opcional." Digamos que queremos comprobar la validez de un número de teléfono y consideramos que un número de teléfono es válido si encaja en alguna de estas dos formas:

(nnn) nnn-nnnn
nnn nnn-nnnn

donde "n" es un numeral. Podríamos construir una expresión regular como esta:

^\(?[0-9][0-9][0-9]\)? [0-9][0-9][0-9]-[0-9][0-9][0-9][0-9]$

En esta expresión, seguimos a los caracteres de los paréntesis con signos de interrogación para indicar que tienen que ser comprobados cero o una vez. De nuevo, como los paréntesis son normalmente metacaracteres (en ERE), los precedemos con barras invertidas para hacer que sean tratadas como literales.

Probémoslo:

[me@linuxbox ~]$ echo "(555) 123-4567" | grep -E '^\(?[0-9][0-9][0-9]\)? [0-9][0-9][0-9]-[0-9][0-9][0-9][0-9]$'
(555) 123-4567
[me@linuxbox ~]$ echo "555 123-4567" | grep -E '^\(?[0-9][0-9][0-9]\)? [0-9][0-9][0-9]-[0-9][0-9][0-9][0-9]$'
555 123-4567
[me@linuxbox ~]$ echo "AAA 123-4567" | grep -E '^\(?[0-9][0-9][0-9]\)? [0-9][0-9][0-9]-[0-9][0-9][0-9][0-9]$'
[me@linuxbox ~]$

Aquí vemos que la expresión encuentra ambos formatos del número de teléfono, pero no encuentra ninguno que contenga caracteres no numéricos.

* - Encuentra un elemento cero o más veces

Como el metacaracter ?, el * se usa para señalar un elemento opcional; sin embargo, al contrario que ?, el elemento puede ocurrir cualquier número de veces, no sólo una. Digamos que queremos ver si una cadena era una frase; o sea, que comienza con una mayúscula, luego contiene cualquier número de letras mayúsculas o minúsculas y espacios, y termina con un punto. Para encontrar esta definición (muy básica) de una frase, podríamos usar una expresión regular como ésta:

[[:upper:]][[:upper:][:lower:] ]*\.

La expresión consta de tres elementos: un expresión entre corchetes conteniendo la clase de caracteres [:upper:], una expresión entre corchetes conteniendo tanto la clase de caracteres [:upper:] como [:lower:] y un espacio, y un punto escapado por una barra invertida. El segundo elemento está precedido de un metacaracter *, de forma que tras la letra mayúscula del principio de nuestra frase, cualquier cantidad de letras mayúculas o minúsculas y espacios que le sigan serán encontrados:

[me@linuxbox ~]$ echo "This works." | grep -E '[[:upper:]][[:upper:][:lower:] ]*\.'
This works.
[me@linuxbox ~]$ echo "This Works." | grep -E '[[:upper:]][[:upper:][:lower:] ]*\.'
This Works.
[me@linuxbox ~]$ echo "this does not" | grep -E '[[:upper:]][[:upper:][:lower:] ]*\.'
[me@linuxbox ~]$

La expresión coincide en los dos primeros test, pero no en el tercero, ya que carece de la mayúscula al principio y del punto al final.

+ - Encuentra un elemento una o más veces

El metacaracter + funciona de forma muy parecida a *, excepto que requiere al menos una instancia del elemento precedente para que ocurra una coincidencia. Aquí tenemos una expresión regular que sólo encontrará líneas consistentes en grupos de uno o más caracteres alfabéticos separados por un espacio:

^([[:alpha:]]+ ?)+$


[me@linuxbox ~]$ echo "This that" | grep -E '^([[:alpha:]]+ ?)+$'
This that
[me@linuxbox ~]$ echo "a b c" | grep -E '^([[:alpha:]]+ ?)+$'
a b c
[me@linuxbox ~]$ echo "a b 9" | grep -E '^([[:alpha:]]+ ?)+$'
[me@linuxbox ~]$ echo "abc d" | grep -E '^([[:alpha:]]+ ?)+$'
[me@linuxbox ~]$

Vemos que esta expresión no encuentra la línea "a b 9", porque contiene un carácter no alfabético; ni encuentra "abc d", por que los caracteres "c" y "d" están separados por más de un espacio en blanco.


{} - Encuentra un elemento un número específico de veces

Los metacaracteres { y } se usan para expresar el número mínimo y máximo de coincidencias requeridas. Pueden especificarse de cuatro formas diferentes:

Especificando el número de coincidencias

{n}   Encuentra el elemento precedente y ocurre exactamente n veces.


{n,m} Encuentra el elemento precedente y ocurre al menos n veces, pero no más de m veces.

{n,}  Encuentra el elemento precedente y ocurre n o más veces.

{,m}  Encuentra el elemento precedente y ocurre no más de m veces.

Volviendo a nuestro ejemplo anterior con los números de teléfono, podemos usar este método de especificar repeticiones para simplificar nuestra expresión regular de:

^\(?[0-9][0-9][0-9]\)? [0-9][0-9][0-9]-[0-9][0-9][0-9][0-9]$

a:

^\(?[0-9]{3}\)? [0-9]{3}-[0-9]{4}$

Probémoslo:

[me@linuxbox ~]$ echo "(555) 123-4567" | grep -E '^\(?[0-9]{3}\)? [0-9]{3}-[0-9]{4}$'
(555) 123-4567
[me@linuxbox ~]$ echo "555 123-4567" | grep -E '^\(?[0-9]{3}\)? [0-9]{3}-[0-9]{4}$'
555 123-4567
[me@linuxbox ~]$ echo "5555 123-4567" | grep -E '^\(?[0-9]{3}\)? [0-9]{3}-[0-9]{4}$'
[me@linuxbox ~]$

Como podemos ver, nuestra expresión revisada puede validar correctamente tanto los números con paréntesis como sin paréntesis, mientras que rechaza aquellos números que no están formateados correctamente.

Poniendo las expresiones regulares a trabajar

Veamos algunos comandos que ya sabemos para ver como pueden usarse con expresiones regulares.

Validando una lista de teléfonos con grep

En nuestro ejemplo anterior, vimos como comprobábamos si un número telefónico estaba correctamente formateado. Un escenario más realista sería chequear una lista de números en su lugar, así que hagamos una lista. Haremos ésto recitando un encantamiento mágico a la línea de comandos. Será mágico porque no hemos visto la mayoría de los comandos involucrados, pero no te preocupes. Los veremos en próximos capítulos. Aquí está el encantamiento:

[me@linuxbox ~]$ for i in {1..10}; do echo "(${RANDOM:0:3}) ${RANDOM:0:3}-${RANDOM:0:4}" >> phonelist.txt; done

Este comando producirá un archivo llamado phonelist.txt que contiene diez números de teléfono. Cada vez que repetimos el comando, otros diez números se añaden a la lista. También podemos cambiar el valor 10 que está cerca del principio del comando para producir más o menos números de teléfono. Si examinamos el contenido del archivo, sin embargo, vemos que tenemos un problema:

 [me@linuxbox ~]$ cat phonelist.txt
 (232) 298-2265
 (624) 381-1078
 (540) 126-1980
 (874) 163-2885
 (286) 254-2860
 (292) 108-518
 (129) 44-1379
 (458) 273-1642
 (686) 299-8268
 (198) 307-2440

Algunos números están mal formateados, lo que es perfecto para nuestro propósito, ya que usaremos grep para validarlos.

Un método de validación útil sería escanear un archivo para encontrar números no válidos y mostrar la lista resultante en pantalla:

 [me@linuxbox ~]$ grep -Ev '^\([0-9]{3}\) [0-9]{3}-[0-9]{4}$'
 phonelist.txt
 (292) 108-518
 (129) 44-1379
 [me@linuxbox ~]$

Aquí usamos la opción -v para producir la coincidencia inversa ya que sólo mostraremos líneas de la lista que no coincidan con la expresión especificada. La propia expresión incluye metacaracteres ancla en los extremos para asegurarnos que el número no tiene caracteres extra al final. Esta expresión también requiere que estén presentes los paréntesis en un número válido, al contrario que nuestro número de teléfono del ejemplo anterior.


Encontrando nombres de archivos feos con find

El comando find soporta un test basado en un expresión regular. Hay una consideración importante a tener en cuenta cuando usamos expresiones regulares confind en lugar de grep. Mientras que grep imprimirá una linea cuando la línea contiene una cadena que coincide con una expresión, find requiere que la ruta coincida exactamente con la expresión regular. En el siguiente ejemplo, usaremos find con una expresión regular para encontrar cada ruta que contenga un carácter que no sea miembro de la siguiente lista:

[-_./0-9a-zA-Z]

Un escaneo como este revelará rutas que contenga espacios y otros potenciales caracteres ofensivos:

 [me@linuxbox ~]$ find . -regex '.*[^-_./0-9a-zA-Z].*'

Debido al requerimiento de que coincida exactamente la ruta completa, usamos .* en ambos extremos de la expresión para buscar cero o más instancias de cada carácter. En el centro de la expresión, usamos una expresión entre corchetes negada conteniendo nuestra colección de caracteres de ruta aceptables.


Buscando archivos con locate

El programa locate soporta expresiones regulares tanto básicas (la opción --regexp) como extendidas (la opción --regex). Con él, podemos realizar muchas de las mismas operaciones que hicimos antes con nuestros archivos dirlist:

 [me@linuxbox ~]$ locate --regex 'bin/(bz|gz|zip)'
 /bin/bzcat
 /bin/bzcmp
 /bin/bzdiff
 /bin/bzegrep
 /bin/bzexe
 /bin/bzfgrep
 /bin/bzgrep
 /bin/bzip2
 /bin/bzip2recover
 /bin/bzless
 /bin/bzmore
 /bin/gzexe
 /bin/gzip
 /usr/bin/zip
 /usr/bin/zipcloak
 /usr/bin/zipgrep
 /usr/bin/zipinfo
 /usr/bin/zipnote
 /usr/bin/zipsplit

Usando alternancia, realizamos un búsqueda de rutas que contengan bin/bz, bin/gzo /bin/zip.


Buscando texto con less y vim

less y vim comparten el mismo método de búsqueda de texto. Pulsando la tecla / seguida de una expresión regular realizaremos una búsqueda. Si usamos less para ver nuestro archivo phonelist.txt:

 [me@linuxbox ~]$ less phonelist.txt

y luego buscamos por nuestra expresión de validación:

 (232) 298-2265
 (624) 381-1078
 (540) 126-1980
 (874) 163-2885
 (286) 254-2860
 (292) 108-518
 (129) 44-1379
 (458) 273-1642
 (686) 299-8268
 (198) 307-2440
 ~
 ~
 ~
 /^\([0-9]{3}\) [0-9]{3}-[0-9]{4}$

less resaltará las cadenas que coinciden, dejando las que no valen fáciles de eliminar:

(232) 298-2265
(624) 381-1078
(540) 126-1980
(874) 163-2885
(286) 254-2860

(292) 108-518
(129) 44-1379
(458) 273-1642
(686) 299-8268
(198) 307-2440

~
~
~
(END)

vim, por otra parte, soporta expresiones regulares básicas, de forma que nuestra expresión aparecería así:

/([0-9]\{3\}) [0-9]\{3\}-[0-9]\{4\}

Podemos ver que la expresión es prácticamente igual; sin embargo, muchos de los caracteres que se consideran metacaracteres en expresiones extendidas se consideran literales en expresiones básicas. Sólo se tratan como metacaracteres cuando los escapamos con una barra invertida. Dependiendo de la configuración particular de vim en nuestro sistema, la coincidencia será resaltada. Si no, prueba este comando de modos:

 :hlsearch

para activar la búsqueda resaltada.

Nota: Dependiendo de tu distribución, vim soportará o no la búsqueda resaltada. Ubuntu, en particular, contiene una versión muy simplificada de vim por defecto. En ese tipo de sistemas, puedes usar tu administrador de paquetes para instalar la versión completa de vim.


Resumiendo

En este capítulo, hemos visto algunos de los muchos usos de las expresiones regulares. Podemos encontrar incluso más si usamos expresiones regulares para buscar aplicaciones opcionales que las usen. Podemos hacerlo buscando las man pages:

 [me@linuxbox ~]$ cd /usr/share/man/man1
 [me@linuxbox man1]$ zgrep -El 'regex|regular expression' *.gz

El programa zgrep proporciona una interfaz para grep, permitiéndonos leer archivos comprimidos. En nuestro ejemplo, buscamos la sección uno de la man page comprimida en su localización usual. El resultado de este comando es una lista de archivos que contienen tanto la cadena "regex" como "regular expression". Como podemos ver, las expresiones regulares muestran un montón de programas.

Hay una función en las expresiones regulares básicas que no hemos visto. Se llaman retroreferencias, esta función será tratada en el siguiente capítulo.


Para saber más

Hay muchos recursos online para aprender expresiones regulares, incluyendo varios tutoriales y chuletas.

Además, la Wikipedia tiene buenos artículos en los siguientes enlaces:

No hay comentarios:

Publicar un comentario

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