Expresiones regulares: fundamentos

Una expresión regular (regex) es una cadena que define un patrón. No es un programa ni un comando: es una notación formal que describes tú y que una herramienta interpreta. grep, sed, awk, los validadores de formularios web, los motores de bases de datos —todos consumen el mismo concepto con pequeñas variaciones de dialecto. Entender la notación separada de la herramienta es lo que te permite pasar de “busco en el manual cada vez” a “compongo el patrón de cabeza”.

El motor de regex recorre el texto carácter a carácter buscando una posición donde el patrón encaje. La mayoría de caracteres son literales: la a en un patrón coincide con la a en el texto. Pero un puñado de caracteres son metacaracteres y tienen significado estructural: . * + ? ^ $ [ ] { } ( ) | \. Si quieres que un metacarácter se trate como literal, lo escapas con \.

¿Cuándo usas esto en la práctica? Siempre que necesites buscar o transformar texto con estructura variable: extraer IPs de un log, validar que una línea de configuración tiene el formato correcto, filtrar líneas que empiezan o terminan con algo concreto. Cuando lo aplicas mal, los falsos positivos o falsos negativos son silenciosos —el comando no protesta, simplemente te devuelve resultados incorrectos o no te devuelve nada.

Hay un detalle que genera confusión constante: existen dos sabores estándar en las herramientas POSIX. Las BRE (Basic Regular Expressions) son el comportamiento por defecto de grep. En BRE, los metacaracteres +, ?, |, ( y ) pierden su significado especial y hay que escaparlos con \ para activarlo —lo contrario de lo que haría la intuición. Las ERE (Extended Regular Expressions) se activan con grep -E (o egrep, que es un alias que evita usar) y hacen que esos mismos caracteres sean metacaracteres directamente. Usa siempre -E salvo que tengas una razón explícita para no hacerlo: el patrón queda más legible y el comportamiento es más predecible.

# Archivo de ejemplo: simulamos un log de acceso simplificado
cat > /tmp/accesos.log << 'EOF'
2024-03-15 08:02:11 usuario=ana      ip=192.168.1.10  estado=OK
2024-03-15 08:03:44 usuario=root     ip=10.0.0.1      estado=OK
2024-03-15 08:05:01 usuario=bob      ip=192.168.1.205 estado=FALLO
2024-03-15 08:05:02 usuario=bob      ip=192.168.1.205 estado=FALLO
2024-03-15 08:05:03 usuario=bob      ip=192.168.1.205 estado=FALLO
2024-03-15 09:00:00 usuario=ana      ip=172.16.4.8    estado=OK
2024-03-15 09:01:55 usuario=root     ip=10.0.0.255    estado=FALLO
  usuario=servicio ip=127.0.0.1      estado=OK
EOF

# 1. Ancla ^: líneas que empiezan con la fecha (excluye la línea con sangría)
grep -E '^2024' /tmp/accesos.log

# 2. Ancla $: líneas que terminan en OK (sin espacios o caracteres extra)
grep -E 'OK$' /tmp/accesos.log

# 3. Punto + cuantificador +: cualquier secuencia de uno o más caracteres
#    entre "ip=" y un espacio — forma cruda de extraer IPs (no valida formato)
grep -oE 'ip=[^ ]+' /tmp/accesos.log

# 4. Clase de caracteres con rango y cuantificador +: 
#    direcciones que empiezan por 192.168. seguidas de 1 a 3 dígitos, punto,
#    y 1 a 3 dígitos más. El \. escapa el punto para que sea literal.
grep -E '192\.168\.[0-9]{1,3}\.[0-9]{1,3}' /tmp/accesos.log

# 5. Clase negada [^]: usuario que NO contiene letras minúsculas a-z
#    (detectaría nombres con dígitos o caracteres especiales; aquí ninguno)
grep -E 'usuario=[^a-z ]' /tmp/accesos.log

# 6. Alternación |: líneas con usuario root O con estado FALLO
grep -E 'usuario=root|estado=FALLO' /tmp/accesos.log

# 7. Grupos () con alternación — misma lógica pero agrupa la alternación
#    para que no compita con el resto del patrón:
#    líneas donde el estado es OK o FALLO (cualquiera de los dos valores exactos)
grep -E 'estado=(OK|FALLO)$' /tmp/accesos.log

# 8. Cuantificador ?: hace el grupo anterior opcional
#    Coincide con "estado=OK" o "estado=" (valor vacío)
#    Útil para detectar entradas malformadas donde el valor podría faltar
grep -E 'estado=(OK)?' /tmp/accesos.log

# 9. Lo mismo en BRE para ver la diferencia de sintaxis (sin -E):
#    los paréntesis, el pipe y el + necesitan \ para ser metacaracteres
grep 'estado=\(OK\|FALLO\)$' /tmp/accesos.log

El ancla ^ en el paso 1 no busca el carácter ^ —fija la posición al inicio de la línea. Por eso la última línea, que tiene sangría, no aparece aunque contenga 2024 más adelante. $ hace lo simétrico: ancla al final, de modo que una línea con espacios tras OK no coincidiría.

El punto . es el comodín de un solo carácter. En el paso 3 no se usa el punto directamente, sino que en el paso 4 aparece escapado como \. —eso es exactamente la distinción que más errores genera. 192.168 sin escapar coincide con 192X168 o 192_168 porque el punto sin escapar acepta cualquier carácter. En un patrón de IPs eso puede producir falsos positivos.

Los cuantificadores *, + y ? siempre modifican el elemento inmediatamente anterior —un carácter, un punto, una clase o un grupo. En [0-9]{1,3} el {1,3} es la forma de cuantificador con límite explícito: entre uno y tres repeticiones. El ? del paso 8 convierte (OK) en opcional: cero o una aparición del grupo entero.

Las clases [0-9] y [a-z] describen rangos basados en el valor ASCII. [^a-z ] en el paso 5 niega la clase: cualquier carácter que no sea letra minúscula ni espacio. La posición del ^ importa: solo niega cuando es el primer carácter dentro de los corchetes; en cualquier otra posición es un literal.

La alternación | tiene la precedencia más baja de todos los operadores. Sin paréntesis, usuario=root|estado=FALLO se lee como (usuario=root)|(estado=FALLO), lo que en este caso es lo que queremos. Pero si escribieras estado=OK|FALLO esperando que matchee estado=OK o estado=FALLO, obtendrías (estado=OK)|(FALLO) —la segunda alternativa buscaría FALLO en cualquier parte de la línea. El paso 7 usa (OK|FALLO) para acotar la alternación dentro del grupo.

La diferencia BRE/ERE es visible comparando los pasos 7 y 9: exactamente el mismo patrón lógico, pero en BRE los paréntesis y el pipe llevan \ para activar su significado especial, mientras que en ERE son metacaracteres directos. Mezclar los dos dialectos en un script —usar grep sin -E para unos patrones y sed sin -E para otros— es la fuente habitual de comportamientos inconsistentes que cuestan tiempo depurar.

47

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio