2.8.1. Expresiones regulares

Una expresión regular (comúnmente denominadas regex) es una secuencia de caracteres que conforman un patrón de búsqueda. Por lo general, estas búsquedas se realizan sobre cadenas de caracteres, esto es, sobre texto plano.

Existen muchas herremientas de selección o sustitución de texto que las usan, por lo que antes de pasar a presentar éstas, es necesario conocer cómo se construyen.

2.8.1.1. Tipos de expresiones regulares

Hay muchas implementaciones distintas de regex[1], pero todas las podemos agrupar dentro de una de estas tres familias:

BRE (Basic Regular Expression):

Las expresiones regulares básicas, estándar POSIX, nacieron a partir de la implementación de las regex por el antiguo editor ed, antecesor de vim[2]

ERE (Extended Regular Expression):

Las expresiones regulares extendidas, también estándar POSIX, nacieron a partir de una extensión del comando grep, llamada egrep[3]. Son similares, pero no compatibles con las anteriores[4], y son la base del resto de tipos de expresiones regulares nacidas después. De hecho, lo común es que cualquier implementación de expresiones regulares que podamos encontrar, sea compatible con el estándar ERE, aunque disponga de extensiones adicionales.

PCRE (Perl Compatible Regular Expression):

Las expresiones regulares compatibles con Perl son la implementación de las regex para el lenguaje de programación perl. Añaden a las expresiones ERE algunas extensiones útiles y, aunque no son estándar POSIX, son de facto un estándar, puesto que muchas implementaciones posteriores las han tomado de base.

En la línea de órdenes los herramientas básicas que permiten el uso de expresiones básicas (y que estudiaremos más adelante) son grep, sed, awk y el propio bash desde su versión 3. Las dos primeras usan por defecto expresiones BRE, pero con la adición de una opción también expresiones ERE; mientras que las dos últimas usan únicamente expresiones ERE. Por otro lado, la mayoría de implementaciones de los lenguajes de programación más conocidos entienden las expresiones PCRE, que son, a su vez, practicamente compatibles con las ERE (salvo por alguna excepción). Por tanto, lo más sensato es aprender las expresiones PCRE, sabiendo cuáles no existen en ERE para no usarlas con herramientas que no llegan a soportarlas[5][6].

Nota

Para probar las expresiones regulares que introduzcamos a continuación, se utilizará grep con la opción -E para que interprete expresiones ERE, o con la opción -P para que interprete expresiones PCRE. Tal comando se introducirá formalmente más adelante junto a los demás.

2.8.1.2. Patrones básicos

Patrones básicos

Operación

Operador

Descripción

Ejemplo (ERE)

BRE

ERE

PCRE

Cuantificación

\?

?

Una vez o ninguna.

a?

*

Las veces que sea (incluso ninguna)

a*

\+

+

Al menos una vez

a+

\{n,m\}

{n,m}

Entre n y m veces. Puede omitirse uno de los límites.

a{5,9}

Agrupación

Ver grupos.

(?regex)

(?:regex)

Para modificar el alcance de un operador

(?123)+

Alternativa

\|

|

O lo uno o lo otro

(?Blas|Luis)

Principio

^

El patrón comienza

^a

Fin

$

El patrón acaba

a$

Repr. universal

.

Cualquier carácter

.{2,3}

Escape

\

No interpretar un carácter especial

\.

Advertencia

La versión de awk que trae instalada debian por defecto (mawk) no soporta el cuantificador de llaves {n,m}.

Ilustremos estos patrones con algunos ejemplos:

  1. ¿Contiene el texto una o ninguna «e» [7]?

    $ grep -E 'e?' <<<"abracadabra"
    abracadabra
    
  2. ¿El texto contiene al menos una «e»[8]?

    $ grep -E 'e+' <<<"abracadabra"
    
  3. ¿El texto contiene al menos dos «a» seguidas?

    $ grep -E 'a{2,}' <<<"abracadabra"
    
  4. ¿El texto comienza por «a»?

    $ grep -E '^a' <<<"abracadabra"
    abracadabra
    
  5. ¿El texto acaba en «a»?

    $ grep -E 'a$' <<<"abracadabra"
    abracadabra
    
  6. ¿El texto empieza y acaba en «a»?

    $ grep -E '^a(?.*a)?$' <<<"abracadabra"
    abracadabra
    
  7. ¿El texto empieza por «a» y está constituida por a y otro carácter cualquiera alternativamente?

    $ grep -E '^(?a.)+?$' <<<"abracadabra"
    

    El patrón no concuerda porque el texto contiene dos grupos -br-.

    Nota

    En realidad, el punto representa cualquier carácter lo cual incluye a la propia «a». Por ese motivo, si la línea fuera «abaaac» concordaría con el patrón y esa quizás no sea nuestro objetivo. Puede volverse a este ejercicio, después de haber visto los patrones extendidos, y sustituir cualquier carácter (o sea, el punto) por cualquier carácter que no sea una «a».

  8. Como en el caso anterior, pero entre una «a» y la siguiente puede haber hasta dos caracteres:

    $ grep -E '^(?a..?)+a?$' <<<"abracadabra"
    abracadabra
    
  9. ¿El texto contiene al menos un carácter cualquier?

    $ grep -E '.' <<<"abracadabra"
    abracadabra
    
  10. ¿El texto contiene un punto?

    $ grep -E '\.' <<<"abracadabra"
    

Advertencia

Es muy importante tener presente que el patrón siempre concuerda con el mayor número de caracteres posibles. Por ejemplo, la regex “.*:” expresa una cantidad indeterminada de caracteres cualesquiera seguidas de dos puntos. Si probamos a buscar dicho patrón en este ejemplo:

$ grep -E '.*:' <<< "usuario:x:1000"

La concordancia será con la subcadena usuario:x: y no con usuario: puesto que la segunda incluye a la primera[9].

2.8.1.3. Patrones avanzados

Patrones avanzados

Operación

Operador

Descripción

Ejemplo

BRE

ERE/PCRE

Alternativa

[...]

Uno de los caracteres incluidos. Puede indicarse un rango.

[A-Za-z]

[^...]

Un caracter que no sea ninguno de los incluidos

[^A-Z]

Clases

Sin equivalencia

\w

Un carácter de palabra (letra, dígito o «_»).

\w+

Sin equivalencia

\W

Un carácter que no sea de palabra.

^\W

Sin equivalencia

\d

Un dígito, o sea, [0-9]

\d{4}

Sin equivalencia

\D

Un carácter que no sea un dígito.

\D+

Sin equivalencia

\s

Un carácter de espaciado, o sea, [ \t\r\b\f]

\w+\s\w+

Sin equivalencia

\S

Un carácter que no sea de espaciado.

\S+\s

Sin equivalencia

[:nombre:]

Un carácter de la clase nombre.

[[:alpha:],;.]+

Sin equivalencia

[=x=]

Cualquier variante del caracter «x»

[[=a=]]

Límite de palabra

Sin equivalencia

\b[10]

Principio o fin de palabra.

\bdado\b

Grupos

\(regex\)

(regex)

Captura un grupo

(\w+)\s+\1

\1, \2, ... \9

Refiere un grupo previamente capturado

Advertencia

Ni grep ni sed entienden las clases \d y \D. bash, por su parte, sólo entiende las clases con nombre.

Advertencia

Las clases que se expresan con el escapado y un carácter (como \d o \s) y la mayoría de los caracteres especiales (., *, etc.), pierden su significado cuando se incluyen entre corchetes. Por ejemplo, [d.] significa un carácter que es la barra invertida, la letra d o el punto.

Algunas de las clases posibles que se expresan con la fórmula [:nombre:] son:

[:any:]

Cualquier carácter. Equivale por tanto al punto (.).

[:alpha:]

Carácter alfabético (letra). Téngase en cuenta que esta expresión no es equivalente a [A-Za-z], puesto que en lenguas como el castellano á* o ü son también letras.

[:digit:]

Carácter numérito (un dígito).

[:alnum:]

Carácter alfanumérico (una letra o un dígito).

[:lower:]

Letra minúscula.

[:upper:]

Letra mayúscula.

[:punct:]

Signo de puntuación.

[:space:]

Carácter de espaciado.

[:ascii:]

Carácter ASCII (códigos 0-127).

Por su parte, que la clase [=x=] represente cualquier carácter que sea variación del indicado, significa que [=a=] concordará con a, A, á, à, etc.

Advertencia

Estas clases sólo pueden usarse dentro de una alternativa. Por tanto, esto no es válido:

[:alpha:]

pero esto sí:

[[:alpha:]]

Ejemplo de uso:

  1. ¿Comienza el texto por una letra mayúscula?

    $ grep -E '^[A-Z]' <<<"Pepe Gotera y Otilio"
    Pepe Gotera y Otilio
    

    No obstante, esto no hubiera funcionado si la mayúscula hubiera sido acentuada. Es mejor esto:

    $ grep -E '^[[:upper:]]' <<<"Pepe Gotera y Otilio"
    Pepe Gotera y Otilio
    

    Existe otra solución más, pero requiere los patrones extendidos de PCRE.

  2. ¿Acaba la frase en un texto que no es de palabra?

    $ grep -E '\W$' <<<"Pepe Gotera y Otilio"
    
  3. La frase no contiene ningún digito:

    $ grep -E '^\D+$' <<<"Pepe Gotera y Otilio"
    Pepe Gotera y Otilio
    
  4. ¿Hay alguna palabra que empieze por «o»?

    $ grep -E '\b[[=o=]]' <<<"Pepe Gotera y Otilio"
    Pepe Gotera y Otilio
    
  5. ¿El texto empieza y acaba con los mismos caracteres?

    $ grep -E '^(?.+).*\1$' <<<"abracadabra"
    abracadabra
    
  6. La frase sólo contiene palabras que empiezan por o:

    $ grep -E '^\W*(?\b[[=o=]]\w*\W*)+$' <<<"¡Oh, Ovidio, obnubilido os oteo!"
    ¡Oh, Ovidio, obnubilido os oteo!
    

2.8.1.4. Patrones extendidos (PCRE)

Advertencia

El conocimiento de estos patrones no es estrictamente necesario y rara vez tendremos que recurrir a ellos en las labores de administración.

Patrones extendidos

Operación

Operador

Descripción

Ejemplo

Clases

\p{CLASS}

Un carácter de la clase unicode[11] CLASS

\p{L}+

\P{CLASS}

Un carácter que no sea de clase unicode CLASS.

\P{L}+

Grupos

(?P<nombre>regex)

Captura un grupo, asignándole nombre

(?P<palabra>\w+)\s+(?P=palabra)

(?P=nombre)

Refiere un grupo con nombre previamente capturado

Lookahead

(?=regex)

Comprueba que esté la regex hacia adelante

w(?=a)

(?!regex)

Comprueba que no esté la regex hacia adelante

w(?!a)

Lookabehind

(?<=regex)

Comprueba que esté la regex hacia atrás

(?<=a)s

(?<!regex)

Comprueba que no esté la regex hacia atrás

(?<!.)$

Los patrones extendidos que proporciona PCRE en algunos casos amplían las posibilidades de los que ya hay en ERE. Tal es el caso de las nuevas clases que pueden difinirse (se han incluido en las tablas las categorías unicode) o de los grupos con nombre. Sin embargo, introducen también la definición de lookarounds (lookahead y lookbehind) que son un concepto nuevo. Si tuviéramos que traducir el concepto quizás lo haríamos como vistazos, unos hacia adelante y otros hacia atrás.

Los lookarounds consisten en seleccionar texto en función de lo que pudiera haber antes o después. Tomemos como ejemplo el último de los ejemplos del apartado anterior:

$ grep -E '^(.+).*\1$' <<<"abracadabra"

en que determinamos si el texto empieza y acaba por los mismos caracteres. Conseguimos nuestro objetivo, pero la comprobación implica que seleccionemos todo el texto. De hecho, si usamos la opción --color comprobaremos que grep remarca todo en rojo. Ahora bien, quizás lo único que queríamos seleccionar era el conjunto de caracteres que abren el texto y que sirven luego para cerrarlo. Para esto precisamente sirven los lookarounds. En este caso:

$ grep -P '^(.+)(?=.*\1$)' <<<"abracadabra"

En este caso, sólo veremos señalado con color -abra-, porque es eso mismo lo que hemos seleccionado. Lo que hay a continuación es simplemente para echar un vistazo, sin llegar a seleccionarlo. Esto, además, es una de las características de los lookarounds que no consumen caracteres. De hecho, esto queda meridianamente claro, si añadimos una c, que es el siguiente carácter a abra:

$  grep -P '^(.+)(?=.*\1$)c' <<<"abracadabra"

Como se ve el lookaround comprobó los caracteres siguientes, pero no consumió ninguno. De ahí que el siguiente caracter a (.+) sea la letra c.

Nota

En teoría mirar hacia detrás y mirar hacia adelante funcionan del mismo modo, pero en muchas implementaciones lookbehind tiene limitaciones que no tiene lookahead. En algunos casos, la expresión regular que se usa debe tener una longitud fija (como es el caso del propio grep) mientras que en otros se permite la longitud variable, lo que excluiría cuantificadores como * o +.

Otra de las posibilidades que abren los lookarounds, es la de comprobar si un texto no contiene determinada cadena, aunque la solución no es nada trivial. Esto es imposible de hacer con los patrones ERE. Véanse los ejemplos que se incluyen a continuación.

Advertencia

Algunas herramientas (grep, por ejemplo) permiten mediente opciones invertir la selección para que se muestren las líneas que no contienen el patrón suministrado. De este modo, no tendríamos que recurrir a los lookarounds de las expresiones PCRE.

Ejemplos:

  1. ¿Comienza el texto por una letra mayúscula?

    $ grep -P '^\p{Lu}' <<<"Pepe Gotera y Otilio"
    Pepe Gotera y Otilio
    
  2. ¿El texto empieza y acaba con los mismos caracteres? (Úsense grupos con nombre)

    $ grep -P '^(?<princ>.+).*(?P=princ)$' <<<"abracadabra"
    abracadabra
    
  3. Seleccionar una frase si no contiene la palabra Pepe:

    $ grep -P '^((?!Pepe).)*$' <<<"abracadabra"
    

    La explicación de que esto funcione es que se busca que la frase este constituida repetidamente por algo y ese algo es (?!Pepe)., es decir, una nada, pero que no esté seguida por la palabra Pepe y un carácter. Si la frase no contiene la palabra Pepe en algún lugar, esto es cierto. Sin embargo, si la contiene, esto será falso cuando grep analice la primera P de Pepe, porque ese carácter es una nada seguida de Pepe y luego un carácter: la propia P.

    Podrámos pensar que ^(.(?!Pepe))*$ también es solución, pero no lo es, proque falla en un caso:

    $ grep -P '^(.(?!Pepe))*$' <<<"Pepe Gotera y Otilio"
    

    ya que la expresión significa, cualquier carácter seguido de Pepe. Pero en el ejemplo escogido, no hay ningún carácter anterior a Pepe, puesto que Pepe abre la cadena.

    Otra solución posible es '^(.(?<!Pepe))*$', o sea, comprobar si de principio a fin hay un carácter seguido de una nada a la que antecede la palabra Pepe. Esto también funciona en todos los caso. El problema es que al usar una lookbehind estamos limitados a que la cadena que no queramos encontrar tenga una longitud fija.

2.8.1.5. Enlaces de interés

2.8.1.6. Ejercicios sobre regex

Nota

Para comprobar la validez de su solución puede utilizar la orden grep:

  • Sin opción específica podrá ensayar BRE:

    # grep '\<\(p\|P\)alabra\>' <<<"Esto es una palabra aislada."
    
  • Con la opción -E (o egrep) podrá ensayar ERE:

    # grep -E '\b(?p|P)alabra\b' <<<"Esto es una palabra aislada."
    
  • Con la opción -P podrá ensayar PCRE:

    # grep -P '\b(:?p|P)alabra\b' <<<"Esto es una palabra aislada."
    
  1. Usando únicamente los patrones básicos de los apuntes, indique las expresiones regulares que:

    1. Concuerdan con dos «e» separadas por un caracter cualquiera.

    2. Concuerdan con dos «e» separadas por tres o cuatro caracteres cualquiera.

    3. Concuerdan con frases que tiene al menos tres «e».

    4. Concuerdan con frases que contiene la palabra «bola» ó «bolo».

    5. Concuerdan con frases que empiezan por «a», tienen al menos una «J» y acaban en «e».

    6. Concuerdan con frases que contiene al menos dos veces la palabra «bola».

    7. Concuerdan con frases que empiezan por «11» y acaban por «99».

    8. Concuerdan con frases de tres palabras (considérese que no hay comas ni puntos).

    9. Concuerdan con frases de menos de 100 caracteres.

    10. Concuerdan con frases que sólo tienen vocales.

    Nota

    Algunas soluciones, si nos restringimos al uso de los patrones básicos, serán algo definicientes aún.

  2. Repase las soluciones anteriores para mejorarlas usando también los patrones avanzados.

  3. Indique una expresión regular que:

    1. contenga tres vocales seguidas.

    2. contega tres vocales.

    3. concuerde con una sucesión de números y caracteres asimilables al espacio.

    4. concuerde con dos palabras.

    5. concuerde con una frase de entre 20 y 30 caracteres.

    6. contenga tres signos de interrogación.

    7. contenga dos «a» seguidas y dos «e» seguidas en cualquier orden.

  4. Para la configuración de un servidor nginx necesitamos utilizar un patrón que concuerde con rutas que no empiezan por /uploads y que sí acaban por .php. Por tanto, concordará con rutas tales como:

    /scripts/01/index.php
    /plugins/users.php
    

    pero no con rutas como:

    /plugins/readme.txt
    /uploads/2009/01/script.php
    

Notas al pie