Las expresiones regulares (REGEX) son un lenguaje para describir patrones en texto y localizar (o filtrar) coincidencias. Su potencia depende del “sabor” (flavour) de regex que use la herramienta: en el mundo Unix, grep trabaja principalmente con POSIX BRE/ERE (según uses grep o grep -E), mientras que algunas variantes (normalmente GNU grep) ofrecen PCRE mediante grep -P (no estándar, y no siempre disponible).
La diferencia más importante entre POSIX y PCRE no es solo sintáctica (“qué hay que escapar”), sino también semántica: POSIX tiende a resolver ambigüedades con la regla “leftmost-longest” (la coincidencia más a la izquierda y, entre ellas, la más larga), mientras que PCRE evalúa alternativas de izquierda a derecha y se queda con la primera que permite un match completo, lo que hace que el orden de los | importe más en PCRE.
En la práctica con grep, lo habitual es:
grep -E (ERE) para la mayoría de búsquedas y validaciones “razonables”.grep -P (PCRE) cuando necesitas lookarounds ((?=...), (?<=...)), clases abreviadas (\d, \w), cuantificadores perezosos (*?) o construcciones avanzadas (grupos con nombre, etc.).grep es fundamentalmente lineal por líneas: por defecto no puede “cruzar” saltos de línea; en GNU grep existe -z (líneas separadas por NUL) como mecanismo especial, y en casos complejos conviene pasar a awk, sed o perl.En cuanto a “validar” con regex: funciona bien para estructuras (p. ej., IPv4, CP) y como filtro previo (p. ej., DNI o email), pero hay validaciones que requieren reglas externas (ej. letra del DNI) o gramáticas demasiado ricas (ej. email completo según Internet Message Format).
Una regex es una secuencia de símbolos donde “la mayoría de caracteres se interpretan literalmente” y un conjunto de metacaracteres adquiere significado especial para describir variaciones, repeticiones y alternativas.
En sabores tipo PCRE (y la mayoría de motores modernos), fuera de corchetes suelen ser metacaracteres .^$|()[]*+?{} y la barra invertida \ como escape.
En POSIX (BRE/ERE) el conjunto “operador vs literal” depende del sabor: por ejemplo, + y ? son operadores en ERE, mientras que en BRE suelen requerir escape (y aun así puede variar por implementación). En GNU grep, BRE y ERE son esencialmente “lo mismo con distinto escapado”.
Los cuantificadores especifican repeticiones:
* → 0 o más+ → 1 o más? → 0 o 1{n}, {n,}, {n,m} → repetición acotada (si el motor lo soporta en ese modo)En PCRE existen además cuantificadores “perezosos” (*?, +?, ??, {n,m}?) y posesivos, que no existen en POSIX clásico.
[abc], [0-9], [A-Za-z] (con cuidado en locales).[^0-9] (cualquier carácter excepto dígito).[[:digit:]], [[:alpha:]], [[:space:]], etc. (portables en POSIX y muy útiles en grep).\d, \w, \s, \b (no portables a POSIX ERE/BRE salvo extensiones).Las anclas no consumen caracteres; delimitan posiciones:
^ inicio de línea$ fin de líneagrep, ^ y $ operan sobre líneas (porque grep es line-based).Los grupos capturan parte del match y se referencian después:
( ... ) captura; (?: ... ) no captura; retroreferencias \1, \2, etc.En terminal hay dos niveles:
1) El shell interpreta comillas, $, \, etc.
2) El motor regex interpreta metacaracteres.
Por eso suele recomendarse envolver patrones en comillas simples '...' para que el shell no “toque” el patrón, y usar -e cuando el patrón podría empezar por -.
POSIX define dos sintaxis históricas:
grep (-G explícito en GNU).grep -E.Además de la sintaxis, POSIX define reglas de resolución de ambigüedades: leftmost-longest (la coincidencia más a la izquierda y más larga). Esta regla aparece en la especificación base de expresiones regulares.
PCRE2 está diseñado para emular de cerca la sintaxis y semántica de Perl: más construcciones (lookarounds, cuantificadores perezosos, grupos con nombre, etc.) y una semántica típica de motores de backtracking.
Un punto práctico clave: en PCRE, la alternancia | se evalúa probando alternativas de izquierda a derecha y “la primera que funciona” se usa. Esto hace que el orden de alternativas sea parte del significado (más que en POSIX).
| Aspecto | POSIX BRE (grep / grep -G) |
POSIX ERE (grep -E) |
PCRE (grep -P / pcregrep) |
|---|---|---|---|
| Alternancia | normalmente \| (según implementación) |
| |
| |
| Grupos | \(...\) |
(...) |
(...), (?:...), (?<name>...) |
| Cuantificadores | * seguro; \{m,n\} según implementación/portabilidad |
* + ? {m,n} |
+ lo anterior y perezosos *? etc. |
| Retroreferencias | posibles (\1) dependiendo de motor/sabor |
no es el caso típico/portable | sí (\1, \k<name>, etc.) |
| Lookaround | no | no | sí ((?=...), (?<=...), etc.) |
| Clases POSIX | sí ([[:digit:]]) |
sí | sí (además \d, \w si Unicode/flags) |
| Regla de desambiguación | “leftmost-longest” | “leftmost-longest” | alternativas probadas L→R; backtracking típico |
| Portabilidad en Unix | muy alta | muy alta | media (depende de herramienta y build) |
Base normativa y documentación para esta comparación: POSIX (reglas de matching), manual de GNU grep (BRE/ERE, limitaciones y compatibilidad), y documentación de PCRE2 sobre metacaracteres y alternancia.
En sistemas tipo Linux con GNU grep, las opciones de sintaxis principales son: -G (BRE, por defecto), -E (ERE), -F (cadenas literales), -P (PCRE).
grep -P puede advertir de “features no implementadas” y su combinación con -z se considera experimental (según la página de manual).
Sobre egrep/fgrep: fueron equivalentes históricos de grep -E y grep -F, pero están obsoletos en POSIX desde hace décadas y GNU grep los considera deprecados (con avisos de obsolescencia). En scripts nuevos conviene usar grep -E / grep -F.
grep tiene un conjunto de opciones “de trabajo diario”, destacando:
-e PAT (varios patrones), -f FILE (patrones desde fichero).-i (case-insensitive), -v (invertir), -w (palabra completa), -x (línea completa).-n (número de línea), -H/-h (mostrar/ocultar nombre de fichero), -o (solo el match), --color=auto.-A/-B/-C (líneas antes/después).-r / -R (buscar en directorios).-q (salida silenciosa, usar código de salida), -m N (parar tras N coincidencias).Nota importante: los patrones suelen ir entre comillas (por ejemplo, '...') para evitar que el shell los altere.
\n real dentro de texto, y el manual lo expone explícitamente (“no hay forma de hacer match de newlines” en modo estándar).-z cambia el delimitador de “línea” a NUL, lo que permite que el patrón abarque antiguos saltos de línea, pero la salida y el enfoque cambian significativamente; si esto no basta, el propio manual recomienda transformar la entrada o usar awk, sed, perl, etc.| Caso | Objetivo | Regex recomendada | Compatibilidad | Nota clave |
|---|---|---|---|---|
| DNI (con letra) | Formato + letra de control | filtro ERE + validación con awk/perl |
ERE (filtro), algoritmo fuera de regex | la letra depende de módulo 23 |
| Email (práctico) | Emails comunes en logs/forms | ERE “pragmática” | POSIX ERE | evita cubrir el RFC completo |
| Email (RFC 5322 simplificado) | estructura local@dominio más estricta |
PCRE | grep -P o pcregrep |
addr-spec tiene variantes complejas |
| IPv4 | 4 octetos 0–255 | ERE (o PCRE equivalente) | POSIX ERE | regex puede validar rangos 0–255 |
| CP España | 5 dígitos (opcional 01–52) | ERE | POSIX ERE | norma: 5 dígitos |
| URL http/https | esquema + host + puerto + ruta + query | ERE o PCRE (más legible) | ERE (parcial) / PCRE (mejor) | RFC 3986 define sintaxis genérica |
Fuentes normativas para estructura de DNI/NIE, códigos postales y sintaxis de email/URI: organismos públicos y RFCs.
Una regex puede verificar formato (8 dígitos + letra) pero no puede calcular de forma nativa el módulo 23 para comprobar la letra (salvo enumeraciones impracticables). Por eso el enfoque recomendado en Unix es:
1) grep filtra candidatos por formato.
2) awk o perl calcula la letra y valida.
El algoritmo oficial (explicado por el Gobierno de España”) es: dividir el número entre 23, tomar el resto (0–22) y mapear a la letra según la tabla; incluye el ejemplo 12345678 → resto 14 → Z.
Regex (ERE, línea completa):
^[0-9]{8}[A-Z]$
Versión un poco más estricta (solo letras posibles del control):
^[0-9]{8}[TRWAGMYFPDXBNJZSQVHLCKE]$
Explicación paso a paso
^ inicio de línea.[0-9]{8} exactamente 8 dígitos (incluye ceros iniciales, que existen en NIF/DNI).[A-Z] una letra mayúscula (o el conjunto explícito de letras de control).$ fin de línea.Ejemplos válidos (formato + letra coherente con tabla)
12345678Z (ejemplo oficial del cálculo).00000000T (0 mod 23 = 0 → T según tabla).Ejemplos no válidos
12345678A (formato OK, pero letra no coincide con el ejemplo; debería ser Z).1234567Z (7 dígitos).12345678z (minúscula si no usas -i).1234 5678Z (espacio).Buscar líneas que “parecen DNI” (formato)
grep -nE '^[0-9]{8}[A-Z]$' datos.txt
Con nombre histórico (no recomendado en scripts nuevos)
egrep -n '^[0-9]{8}[A-Z]$' datos.txt
egrep es obsolescente/deprecado; grep -E es el equivalente moderno.
Validación real (letra correcta) con Perl (alternativa recomendada)
Esto ya no es grep, pero es el modo Unix “correcto” de completar la validación:
perl -ne 'chomp; if(/^(\d{8})([A-Za-z])$/){$n=$1; $l=uc($2); $k="TRWAGMYFPDXBNJZSQVHLCKE"; print "$_\n" if substr($k,$n%23,1) eq $l}' datos.txt
La tabla de letras y el módulo 23 están descritos en la referencia oficial.
Archivo de prueba:
cat > pruebas_dni.txt <<'EOF'
12345678Z
12345678A
00000000T
00000000R
1234567Z
12345678z
EOF
1234567Z):
grep -nE '^[0-9]{8}[A-Z]$' pruebas_dni.txt
12345678Z y 00000000T):
perl -ne 'chomp; if(/^(\d{8})([A-Za-z])$/){$n=$1; $l=uc($2); $k="TRWAGMYFPDXBNJZSQVHLCKE"; print "$_\n" if substr($k,$n%23,1) eq $l}' pruebas_dni.txt
Diagrama de flujo (validación DNI)
flowchart TD
A[Texto de entrada] --> B{¿Formato 8 dígitos + letra?}
B -- No --> X[Rechazar]
B -- Sí --> C[Extraer número y letra]
C --> D[Calcular resto = número mod 23]
D --> E[Mapear resto→letra con TRWAG...]
E --> F{¿Letra calculada = letra dada?}
F -- Sí --> G[Aceptar]
F -- No --> X
La parte “mapear resto→letra” y el ejemplo oficial están publicados por un organismo público.
El Internet Message Format define que addr-spec = local-part "@" domain y reconoce variantes (dot-atom, quoted-string, domain-literal, etc.), lo que hace que una validación completa por regex sea larga y delicada.
Por ello se suelen usar dos patrones:
Regex (ERE, línea completa):
^[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)+$
Explicación paso a paso
^ … $: validación de línea completa.[A-Za-z0-9._%+-]+: local-part “pragmático” (caracteres comunes).@: separador.[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)+: dominio con al menos un punto.Válidos
ana@example.comjohn.smith+tag@sub.example.esNo válidos (según este patrón práctico)
ana@localhost (sin punto; a veces válido en entornos internos, pero no aquí)ana@@example.comana..lopez@example.com (pasaría en este patrón; limitación típica de la versión práctica)"ana"@example.com (quoted-string: válido en RFC en ciertos casos, no contemplado aquí)grep/egrep
grep -nE '^[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)+$' emails.txt
Este patrón apunta a ser más estricto en:
local-part@domain y opciones del RFC están documentadas.Regex (PCRE, línea completa):
^(?=.{1,254}$)(?=.{1,64}@)[A-Za-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[A-Za-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+[A-Za-z]{2,63}$
Desglose
(?=.{1,254}$) límite total aproximado.(?=.{1,64}@) límite aproximado del local-part....+(?:\....)* impide .. y . final/inicial en el local-part.label. con etiqueta label que:
Compatibilidad
grep -P si tu grep lo soporta, o pcregrep.Comandos
grep -nP '^(?=.{1,254}$)(?=.{1,64}@)[A-Za-z0-9!#$%&'\''*+/=?^_`{|}~-]+(?:\.[A-Za-z0-9!#$%&'\''*+/=?^_`{|}~-]+)*@(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+[A-Za-z]{2,63}$' emails.txt
Si no tienes grep -P, alternativa:
pcregrep -n '^(?=.{1,254}$)(?=.{1,64}@)...$' emails.txt
pcregrep es un “grep” que usa PCRE para patrones compatibles con Perl.
cat > pruebas_email.txt <<'EOF'
ana@example.com
john.smith+tag@sub.example.es
ana@localhost
ana@@example.com
"ana"@example.com
EOF
grep -nE '^[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)+$' pruebas_email.txt
"ana"@... porque no cubrimos quoted-string):
grep -nP '^(?=.{1,254}$)(?=.{1,64}@)...$' pruebas_email.txt
Una dirección IPv4 en notación decimal con puntos suele representarse como cuatro números (octetos) a.b.c.d, donde cada octeto está en 0..255.
Regex (ERE, línea completa):
^((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])$
Explicación (por componentes)
25[0-5] → 250–2552[0-4][0-9] → 200–2491[0-9]{2} → 100–199[1-9]?[0-9] → 0–99 (evita 099 como 3 dígitos)(...\.){3} → tres octetos con punto, y el último octeto sin punto.Válidos
0.0.0.0192.168.1.1255.255.255.255No válidos
256.0.0.1 (octeto fuera de rango)192.168.1 (faltan octetos)192.168.001.1 (este patrón lo rechaza por “001”)Validación de líneas enteras
grep -nE '^((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])$' ips.txt
Búsqueda dentro de texto (extraer coincidencias) — GNU grep
grep -oE '((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])' logs.txt
-o imprime solo la parte coincidente.
cat > pruebas_ipv4.txt <<'EOF'
192.168.1.1
255.255.255.255
0.0.0.0
256.0.0.1
192.168.1
192.168.001.1
EOF
grep -nE '^((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])$' pruebas_ipv4.txt
Normativa: el código postal para la clasificación de correspondencia se establece como cinco dígitos (Real Decreto), y la normativa de desarrollo explica la significación: dos primeros dígitos (provincia), tercer dígito (ciudades importantes/itinerarios), cuarto y quinto (áreas de reparto/rutas).
Además, una corrección normativa indica explícitamente casos como Ceuta y Melilla (51 y 52).
Versión simple (solo 5 dígitos)
^[0-9]{5}$
Versión más estricta (01–52 + 3 dígitos)
^(0[1-9]|[1-4][0-9]|5[0-2])[0-9]{3}$
Explicación rápida
^[0-9]{5}$ valida el requisito “cinco dígitos”.(0[1-9]|[1-4][0-9]|5[0-2]) limita provincia 01–52 (incluye 51/52).[0-9]{3} resto del CP.Válidos
2801351001 (Ceuta, rango permitido)No válidos
ABCDE1234 (4 dígitos)99000 (provincia fuera de 01–52 en versión estricta)grep -nE '^[0-9]{5}$' direcciones.txt
grep -nE '^(0[1-9]|[1-4][0-9]|5[0-2])[0-9]{3}$' direcciones.txt
cat > pruebas_cp.txt <<'EOF'
28013
51001
99000
1234
ABCDE
EOF
grep -nE '^[0-9]{5}$' pruebas_cp.txt
grep -nE '^(0[1-9]|[1-4][0-9]|5[0-2])[0-9]{3}$' pruebas_cp.txt
La sintaxis genérica de un URI define el esquema y componentes como authority, path, query, fragment. En ABNF: URI = scheme ":" hier-part [ "?" query ] [ "#" fragment ] y scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ).
Aquí pediste específicamente URL web http/https con:
http o httpsNo intentaremos cubrir:
user:pass@ (permitidas por RFC, a menudo desaconsejadas),[::1]),Regex (ERE, línea completa):
^https?://([A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+[A-Za-z]{2,63}(:[0-9]{1,5})?(/[^?#]*)?(\?[^#]*)?$
Desglose
^https?://
http + s? opcional → http o https.([A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+
[A-Za-z]{2,63} TLD “simple” (limitamos a letras para evitar casos raros).(:[0-9]{1,5})? puerto opcional (1–5 dígitos; no valida <=65535).(/[^?#]*)? ruta opcional (sin ? ni #).(\?[^#]*)? query opcional (sin #).Válidos
https://example.comhttp://sub.example.es:8080/ruta/algo?x=1&y=2No válidos
ftp://example.com (esquema fuera de alcance)https://-ejemplo.com (etiqueta empieza por -)https://example.com:99999 (pasa por regex pero puerto semánticamente inválido; limitación)Si dispones de PCRE, puedes separar mejor por grupos (aunque grep no imprime grupos sin ayuda adicional). En PCRE2 la semántica de alternancia y metacaracteres está documentada.
Regex (PCRE, línea completa):
^https?://(?:(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)\.)+[A-Za-z]{2,63}(?::\d{1,5})?(?:/[^?#]*)?(?:\?[^#]*)?$
Con ERE (portable)
grep -nE '^https?://([A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+[A-Za-z]{2,63}(:[0-9]{1,5})?(/[^?#]*)?(\?[^#]*)?$' urls.txt
Con PCRE (si disponible)
grep -nP '^https?://(?:(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)\.)+[A-Za-z]{2,63}(?::\d{1,5})?(?:/[^?#]*)?(?:\?[^#]*)?$' urls.txt
Ten presente que grep -P puede avisar de funciones no implementadas en algunos builds.
cat > pruebas_url.txt <<'EOF'
https://example.com
http://sub.example.es:8080/ruta/algo?x=1&y=2
ftp://example.com
https://-ejemplo.com
https://example.com:99999
EOF
grep -nE '^https?://([A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+[A-Za-z]{2,63}(:[0-9]{1,5})?(/[^?#]*)?(\?[^#]*)?$' pruebas_url.txt
Diagrama conceptual (parseo URI simplificado)
flowchart LR
A["URL"] --> B{"scheme = http/https?"}
B -- No --> X["Rechazar"]
B -- Si --> C["host"]
C --> D{"puerto (1-5 digitos)?"}
D --> E{"ruta /... ?"}
E --> F{"query ?... ?"}
F --> G["Aceptar si estructura cumple"]
La descomposición scheme/authority/path/query/fragment está definida por la ABNF del RFC.