Esta lección salda una deuda que arrastramos desde el Módulo 2. Cuando presentamos grep en 02-02 dijimos explícitamente que sus patrones son mucho más potentes que los comodines y que lo dejábamos para más adelante; cuando en 02-05 comparamos el globbing con otras formas de emparejar texto, volvimos a aplazarlo. Y al cerrar 05-03 quedó claro dónde duele: el toolkit sabe validar rutas, procesos y códigos de salida, pero cuando tiene que validar texto —que --fecha 2026-08-03 sea una fecha de verdad, que una IP sea una IP, que una línea de app.log tenga la estructura esperada— recurre a comparaciones frágiles con globs. Las expresiones regulares son el lenguaje que resuelve eso, y con el operador =~ de Bash validan y extraen en un solo paso.
Contenido
- Regex frente a globbing
- Los tres sabores: BRE, ERE y PCRE
- Literales, el punto y las clases
- Clases POSIX
- Anclas
- Cuantificadores y avaricia
- Alternancia, agrupación y retrorreferencias
- Escapes y barras invertidas dentro de comillas
- El operador
=~de Bash BASH_REMATCH: validar y extraer a la vez- Validaciones reales
- Extracción con
grep -o - Cuándo NO usar regex
- Regex frente a globbing
Las dos sintaxis se parecen lo justo para confundir, y significan cosas distintas:
| Globbing (02-05) | Regex | |
|---|---|---|
| Quién lo interpreta | El shell, sobre nombres de fichero | Herramientas, sobre texto |
* |
Cualquier secuencia de caracteres | "Cero o más del elemento anterior" |
? |
Exactamente un carácter | "Cero o una vez del anterior" |
| Un carácter cualquiera | ? |
. |
| Cualquier secuencia | * |
.* |
| Emparejamiento | La cadena entera | Una subcadena, salvo anclas |
[abc] |
Igual en ambos | Igual en ambos |
Las dos trampas de la tabla son las que causan casi todos los errores de principiante. Primera: en regex, * no significa nada por sí solo; modifica al elemento que tiene delante, así que *.log es una regex incorrecta y lo que quieres escribir es .*\.log. Segunda: grep ERROR encuentra la palabra en cualquier posición de la línea, mientras que [[ $x == ERROR ]] exige igualdad completa. Para exigir la línea entera en regex hay que anclar: ^ERROR$.
- Los tres sabores: BRE, ERE y PCRE
Existen tres dialectos, y la diferencia práctica está en qué caracteres necesitan barra invertida:
- BRE (Basic):
+,?,{,|,(y)son literales; para usarlos como metacaracteres hay que escaparlos (\+,\|). - ERE (Extended): esos caracteres son metacaracteres directamente. Es el dialecto cómodo.
- PCRE (estilo Perl): ERE más atajos (
\d,\w,\s,\b), cuantificadores perezosos (*?) y mucho más.
| Herramienta | Sabor por defecto | Cómo cambiarlo |
|---|---|---|
grep |
BRE | grep -E → ERE; grep -P → PCRE |
egrep |
ERE | Obsoleto: usa grep -E |
sed |
BRE | sed -E (o sed -r) → ERE |
awk |
ERE | No aplica |
[[ =~ ]] de Bash |
ERE | No se puede cambiar |
expr, vi |
BRE | — |
La recomendación práctica: usa ERE siempre que puedas (grep -E, sed -E, awk, [[ =~ ]]). Toda esta lección está escrita en ERE salvo donde se indique. grep -P no está disponible en todos los sistemas —no lo lleva macOS ni algunos contenedores mínimos—, así que evítalo en scripts portables.
- Literales, el punto y las clases
La mayoría de los caracteres se representan a sí mismos. Los especiales son . [ ] ^ $ * + ? { } ( ) | \.
grep -E 'entregado' /srv/veloz/datos/envios.csv # literal
grep -E 'a.opez' envios.csv # . = un carácter CUALQUIERA
grep -E '[aeiou]' fichero # uno de esos caracteres
grep -E '[^0-9]' fichero # ^ dentro de [ ]: NEGACIÓN
grep -E '[A-Za-z0-9_]' fichero # rangos combinadosDentro de los corchetes casi todo pierde su significado especial: [.*+] empareja un punto, un asterisco o un signo más literales. Las tres excepciones son ^ (si va primero, niega), - (si va en medio, forma rango: ponlo al principio o al final para que sea literal) y ] (debe ir el primero).
El . es el error más común al buscar extensiones o IPs: grep -E '192.168.1.1' también encuentra 192x168y1z1. Para un punto literal, escápalo: 192\.168\.1\.1.
- Clases POSIX
En grep -E '[[:digit:]]{4}-[[:digit:]]{2}' app.log, los nombres van entre [: :] dentro de unos corchetes de clase, de ahí el doble par:
| Clase POSIX | Equivale a | Uso |
|---|---|---|
[[:digit:]] |
[0-9] |
Dígitos |
[[:alpha:]] / [[:alnum:]] |
[A-Za-z] / [A-Za-z0-9] |
Letras / letras y dígitos |
[[:space:]] |
espacio, tabulador, salto | Espacio en blanco |
[[:upper:]] / [[:lower:]] |
[A-Z] / [a-z] |
Mayúsculas / minúsculas |
[[:punct:]] / [[:xdigit:]] |
.,;:!?... / [0-9A-Fa-f] |
Puntuación / hexadecimal |
¿Por qué preferirlas a [0-9]? Por las locales. Un rango como [a-z] se interpreta según el orden de colación del idioma configurado: en algunas locales incluye caracteres acentuados y en otras no, y [A-z] (error tipográfico frecuente) abarca además [, \, ], ^, _ y la comilla invertida. [[:alpha:]] significa "letra" en cualquier locale, sin sorpresas, y en un servidor donde LANG puede cambiar entre despliegues eso es exactamente lo que quieres. Para dígitos, [0-9] y [[:digit:]] son equivalentes en la práctica; para letras, la diferencia es real.
- Anclas
grep -E '^2026-08-03' app.log # líneas que EMPIEZAN por esa fecha
grep -E 'ERROR$' app.log # líneas que TERMINAN en ERROR
grep -E '^$' fichero # líneas vacías
grep -E '^[[:space:]]*$' fichero # líneas vacías o solo con espacios
grep -E '^alopez,' envios.csv # primer campo exacto^ y $ no consumen ningún carácter: marcan posiciones. Y \b marca un límite de palabra, la frontera entre un carácter de palabra y uno que no lo es: grep -E 'jruiz' también encuentra jruizperez, mientras que grep -E '\bjruiz\b' acota al repartidor exacto.
Anclar es también una cuestión de rendimiento y de seguridad: una validación sin ^ y $ acepta basura alrededor de lo válido, que es como se cuelan datos malformados en un informe.
- Cuantificadores y avaricia
| Cuantificador | Significado | Ejemplo |
|---|---|---|
* |
Cero o más veces | [0-9]* |
+ |
Una o más veces | [0-9]+ |
? |
Cero o una vez (opcional) | https? |
{n} / {n,} / {n,m} |
Exactamente n / n o más / entre n y m | [0-9]{4}, [0-9]{1,3} |
Los cuantificadores son avariciosos (greedy): consumen todo lo que pueden y luego retroceden lo justo para que el resto encaje. El efecto práctico se ve al extraer la ruta de una línea de acceso.log:
linea='10.0.0.5 - [03/Aug/2026] "GET /envios/1234 HTTP/1.1" 200 512'
grep -oE '".*"' <<< "$linea" # "GET /envios/1234 HTTP/1.1" ← aquí acierta
grep -oE '".*" 2' <<< "$linea" # con dos comillas separadas, se pasaría de largo
grep -oE '"[^"]*"' <<< "$linea" # SIEMPRE correcto: hasta la siguiente comilla.* entre comillas dobles empareja hasta la última comilla de la línea, no hasta la primera. El idioma robusto es [^X]* —"todo lo que no sea el delimitador"—, que no depende de la avaricia. ERE no tiene cuantificadores perezosos (.*?); eso es PCRE. En ERE, [^"]* es la respuesta.
- Alternancia, agrupación y retrorreferencias
grep -E 'ERROR|WARN' app.log # alternancia: una u otra
grep -E '^(ERROR|WARN):' app.log # agrupada y anclada
grep -E '(19|20)[0-9]{2}' app.log # año plausible
grep -E '^(.*),\1$' fichero # retrorreferencia: primer campo = últimoLa alternancia tiene la prioridad más baja de todo el lenguaje, así que ^ERROR|WARN$ significa "empieza por ERROR, o termina en WARN", que casi nunca es lo que se quiere. Los paréntesis lo arreglan y, además, capturan el fragmento emparejado para reutilizarlo: \1 es el contenido del primer grupo, \2 el del segundo. Los grupos se numeran por el orden de sus paréntesis de apertura.
Las retrorreferencias son caras y no todas las herramientas las soportan en todos los sabores (awk no las tiene), pero su verdadero valor aparece en la sección 10, donde Bash las expone como un array, y en sed (06-02), donde permiten reescribir texto conservando trozos del original.
- Escapes y barras invertidas dentro de comillas
Aquí es donde se pierde más tiempo del que nadie confesaría. Hay dos niveles de interpretación: primero el shell procesa las comillas, y lo que sobreviva llega a la regex.
grep -E "\." fichero # el shell convierte \. en . → la regex es "." ¡cualquier carácter!
grep -E '\.' fichero # comillas SIMPLES: la regex recibe \. → punto literalRegla de oro: los patrones regex van siempre entre comillas simples. Dentro de ellas nada se interpreta, y lo que escribes es exactamente lo que recibe la herramienta. La única excepción es cuando necesitas interpolar una variable —grep -E "^[0-9]+,[^,]+,$ciudad," envios.csv—, y entonces conviene construir el patrón en una variable aparte.
Los caracteres que necesitan escape para ser literales son . [ ] ^ $ * + ? { } ( ) | \. Y la barra invertida misma se escribe \\ en la regex, lo que dentro de comillas dobles del shell se convierte en \\\\: otro motivo más para usar comillas simples.
- El operador
=~ de Bash
=~ de BashBash trae un motor ERE integrado, disponible solo dentro de [[ ]]: if [[ "$fecha" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then .... Dos reglas de sintaxis, y ambas son contraintuitivas:
- El patrón NO va entrecomillado. Si escribes
[[ "$x" =~ "^[0-9]+$" ]], las comillas convierten el patrón en una cadena literal y solo emparejará si$xcontiene exactamente esos caracteres. Es el error más frecuente con=~, y no da ningún aviso: simplemente nunca coincide. - La cadena de la izquierda SÍ va entrecomillada, como siempre, para protegerla de la división de palabras (03-06).
Si el patrón es complejo o contiene espacios, guárdalo en una variable y usa la variable sin comillas —es la forma legible y portable de tenerlo todo bajo control—:
readonly RE_FECHA='^([0-9]{4})-([0-9]{2})-([0-9]{2})$'
[[ "$1" =~ $RE_FECHA ]] || morir 64 "Fecha inválida: $1 (se espera AAAA-MM-DD)"Además, =~ no está anclado por defecto: [[ abc123 =~ [0-9]+ ]] es cierto. Para validar, ancla siempre con ^ y $.
BASH_REMATCH: validar y extraer a la vez
BASH_REMATCH: validar y extraer a la vezEsta es la razón por la que =~ merece una sección propia. Tras un emparejamiento con éxito, Bash rellena el array BASH_REMATCH: la posición 0 es el texto completo emparejado y las siguientes son los grupos capturados, en orden.
linea='2026-08-03 10:15:22 [ERROR] timeout al consultar veloz-api'
if [[ "$linea" =~ ^([0-9-]{10})\ ([0-9:]{8})\ \[([A-Z]+)\]\ (.*)$ ]]; then
fecha="${BASH_REMATCH[1]}" # 2026-08-03
hora="${BASH_REMATCH[2]}" # 10:15:22
nivel="${BASH_REMATCH[3]}" # ERROR
mensaje="${BASH_REMATCH[4]}" # timeout al consultar veloz-api
printf '%s a las %s: %s\n' "$nivel" "$hora" "$mensaje"
fiCompáralo con la alternativa: un cut para la fecha, otro para la hora, un tr -d '[]' para el nivel y un ${linea#* } repetido cuatro veces para el mensaje. Aquí, una sola comprobación valida el formato y extrae los cuatro campos, sin lanzar un solo proceso externo. En un bucle sobre un app.log de cien mil líneas, la diferencia se cuenta en minutos.
Los espacios del patrón van escapados (\ ) porque, dentro de [[ ]], el patrón sin comillas está sujeto a la división de palabras. Guardarlo en una variable evita ese ruido:
readonly RE_APPLOG='^([0-9-]{10}) ([0-9:]{8}) \[([A-Z]+)\] (.*)$'
[[ "$linea" =~ $RE_APPLOG ]] && nivel="${BASH_REMATCH[3]}"BASH_REMATCH se sobrescribe con cada =~ con éxito y no se limpia cuando uno falla, así que copia lo que necesites inmediatamente después de la comprobación, dentro del if.
- Validaciones reales
readonly RE_FECHA='^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$'
readonly RE_OCTETO='(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])'
readonly RE_IPV4="^$RE_OCTETO\.$RE_OCTETO\.$RE_OCTETO\.$RE_OCTETO$"
readonly RE_HTTP='^[1-5][0-9]{2}$'
readonly RE_CIUDAD='^(Valencia|Sevilla|Bilbao|Madrid)$'
validar_fecha() { [[ "$1" =~ $RE_FECHA ]]; }
validar_ip() { [[ "$1" =~ $RE_IPV4 ]]; }Fíjate en el diseño de RE_FECHA: no se conforma con [0-9]{2} para el mes, sino que exige 01-12 con la alternancia (0[1-9]|1[0-2]). Aun así no valida la fecha, solo su forma: 2026-02-31 la supera. Para saber si la fecha existe hace falta date -d "$f" &>/dev/null, que ya conoces de 04-06. La regla general: la regex valida el formato; la semántica se comprueba aparte.
RE_OCTETO muestra el otro principio: construir por partes. Escribir la regex de IPv4 de una tacada es ilegible; componerla desde un octeto es evidente. Y así se depura, incrementalmente: prueba primero ^[0-9]{4}, luego añade el mes, luego el día. Herramientas como regex101.com explican cada elemento y muestran las capturas en vivo, y en la terminal basta con grep -oE sobre un fichero de ejemplo para ver qué empareja de verdad.
- Extracción con
grep -o
grep -oCuando lo que quieres no es la línea sino el fragmento:
grep -oE '^[0-9.]+' /var/log/veloz/acceso.log | sort -u # IPs únicas
grep -oE '"GET [^ ]+' acceso.log | cut -d' ' -f2 | sort | uniq -c | sort -rn | head
grep -coE 'ERROR' app.log # -c cuenta LÍNEAS, no coincidencias
grep -oE 'ERROR' app.log | wc -l # esto sí cuenta coincidenciasEsa última pareja esconde un matiz que estropea informes: grep -c cuenta líneas que contienen al menos una coincidencia; si una línea tiene tres, sigue contando 1. Para contar coincidencias hay que usar -o y contar líneas de la salida.
grep -o es la herramienta de la tubería; BASH_REMATCH es la del bucle. Si ya estás recorriendo el fichero línea a línea con while IFS= read -r (04-01), =~ evita lanzar un proceso por línea.
- Cuándo NO usar regex
Las regex son un lenguaje regular, y hay estructuras que no son regulares. Insistir en ellas produce código que funciona con los ejemplos y falla en producción:
- CSV con comas dentro de comillas.
"Sevilla, Este"rompe cualquiercut -d,y cualquier regex ingenua. Los CSV de Veloz Envíos son simples y por esoIFS=,basta; el día que dejen de serlo, la herramienta esawkcon un separador bien definido (06-01) o un parser de verdad. - HTML y XML. Anidamiento arbitrario: no es un lenguaje regular. Existe una respuesta legendaria en Stack Overflow sobre esto, y tiene razón.
- JSON. Misma razón. La herramienta es
jq, y llega en 06-05. - Rutas y nombres de fichero. Ya tienes
find(05-01) y las expansiones${s##*/}(04-04).
Y un aviso de rendimiento: patrones con cuantificadores anidados como (a+)+ pueden provocar retroceso catastrófico y colgar el proceso con una entrada corta. Si tu regex necesita anidar cuantificadores, casi siempre hay una formulación más simple.
Errores Comunes y Consejos
- Entrecomillar el patrón de
=~. Se convierte en literal y no empareja nunca, sin aviso alguno. - Usar comillas dobles en un patrón de
grep. El shell se come las barras invertidas. Comillas simples. - Olvidar escapar el punto.
192.168.1.1empareja19216811y muchas cosas más. *.logcomo regex.*modifica al anterior; se escribe.*\.log.- No anclar una validación.
[[ $x =~ [0-9]+ ]]aceptaabc123defcomo número. ^ERROR|WARN$sin paréntesis. La alternancia tiene la prioridad más baja y parte la expresión entera.- Confiar en
grep -cpara contar coincidencias. Cuenta líneas. Usagrep -o | wc -l. - Consejo: guarda cada regex en una variable
readonly RE_ALGOcon nombre descriptivo, cerca del principio del script. Se documenta sola, se reutiliza, se prueba por separado y desaparece el ruido de comillas.
Ejercicios
Ejercicio 1. Escribe validar_fecha() que acepte solo AAAA-MM-DD con mes 01-12 y día 01-31, y además verifique que la fecha existe de verdad (rechazando 2026-02-31). Debe devolver 0 o 1 sin imprimir nada.
Ejercicio 2. Recorre /var/log/veloz/app.log con un solo bucle y produce un recuento por nivel (INFO, WARN, ERROR), mostrando además la hora y el mensaje del primer ERROR del día, sin lanzar procesos externos dentro del bucle.
Ejercicio 3. Extrae de acceso.log las IPs que hayan provocado al menos un código 5xx, junto con cuántas veces, ordenadas de mayor a menor.
Soluciones
Solución 1.
readonly RE_FECHA='^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$'
validar_fecha() { # Uso: validar_fecha AAAA-MM-DD
[[ "${1:-}" =~ $RE_FECHA ]] || return 1
date -d "$1" > /dev/null 2>&1 # la semántica, aparte del formato
}
validar_fecha 2026-08-03 && echo ok # ok
validar_fecha 2026-02-31 || echo malo # malo (formato válido, fecha inexistente)
validar_fecha 2026-8-3 || echo malo # malo (formato incorrecto)Dos capas: la regex descarta lo que ni siquiera tiene forma de fecha —barato, sin procesos— y date -d resuelve lo que la regex no puede saber, como los años bisiestos. El orden importa: si date fuera primero, aceptaría entradas como next friday.
Solución 2.
readonly RE_APPLOG='^([0-9]{4}-[0-9]{2}-[0-9]{2}) ([0-9:]{8}) \[([A-Z]+)\] (.*)$'
declare -A NIVELES=()
primer_error=''
while IFS= read -r linea; do
[[ "$linea" =~ $RE_APPLOG ]] || continue # descarta líneas malformadas
(( NIVELES["${BASH_REMATCH[3]}"]++ ))
if [[ "${BASH_REMATCH[3]}" == ERROR && -z "$primer_error" ]]; then
primer_error="${BASH_REMATCH[2]} — ${BASH_REMATCH[4]}"
fi
done < /var/log/veloz/app.log
for nivel in "${!NIVELES[@]}"; do
printf '%-6s %5d\n' "$nivel" "${NIVELES[$nivel]}"
done
[[ -n "$primer_error" ]] && printf 'Primer ERROR: %s\n' "$primer_error"Es el patrón contador de 04-03 alimentado por BASH_REMATCH. El || continue convierte la validación en un filtro que protege el recuento de líneas basura, y todo ocurre dentro de Bash: ni un cut, ni un grep, ni un proceso por línea.
Solución 3.
grep -E '" [5][0-9]{2} ' /var/log/veloz/acceso.log \
| grep -oE '^[0-9]{1,3}(\.[0-9]{1,3}){3}' \
| sort | uniq -c | sort -rn
# 41 10.0.0.87
# 9 10.0.0.5El primer grep selecciona las líneas cuyo código va tras la petición entrecomillada —el espacio y la comilla evitan confundirlo con un número de bytes que empiece por 5—, y el segundo extrae solo la IP inicial. El (\.[0-9]{1,3}){3} muestra que un grupo también se puede cuantificar. El remate sort | uniq -c | sort -rn es el idioma del Módulo 2, que ahora encaja con la extracción precisa que solo las regex dan.
Conclusión
Una regex describe texto, no nombres de fichero, y sus metacaracteres se parecen a los del globbing solo lo suficiente para engañar: * modifica al elemento anterior, . es el comodín de un carácter y .* es el equivalente del * del shell. De los tres sabores, ERE es el punto dulce y lo hablan grep -E, sed -E, awk y el =~ de Bash. La sintaxis se construye con literales, ., clases [...] con su negación [^...], clases POSIX [[:digit:]] inmunes a la locale, anclas ^, $ y \b, cuantificadores *, +, ? y {n,m} —avariciosos, de ahí el idioma [^"]*—, alternancia | de prioridad mínima y grupos ( ) que capturan. Los patrones van siempre entre comillas simples, salvo en =~, donde el patrón va sin comillas y lo mejor es guardarlo en una variable readonly. Y ahí está la joya: BASH_REMATCH convierte una comprobación en una extracción de todos los campos a la vez, sin lanzar procesos, ideal para bucles largos. Lo que no debes hacer con regex es parsear CSV con comillas, HTML o JSON: para eso hay herramientas específicas que llegan en el Módulo 6.
informe-diario.sh ya valida su --fecha y descompone app.log en un solo paso. Lo que queda por resolver es la salida: hoy imprime a pantalla con printf sueltos, y si quieres el informe en un fichero tienes que redirigir todo el script desde fuera. No sabe escribir a dos sitios a la vez, ni mantener abierto un fichero de registro mientras trabaja, ni componer una plantilla de informe de varias líneas sin veinte printf. En 05-05 entran los descriptores de fichero, los here-documents, los here-strings y la sustitución de procesos: la maquinaria de entrada/salida que convierte la salida del toolkit en algo que se puede dirigir con precisión.
Curso de Programación en Bash
Módulo 1: Introducción a Bash
- ¿Qué es Bash?
- Configurando tu Entorno
- Navegación Básica en la Línea de Comandos
- Entendiendo el Shell
- Encontrar Ayuda: man, help y --help
Módulo 2: Comandos Básicos de Bash
- Operaciones con Archivos y Directorios
- Comandos de Procesamiento de Texto
- Permisos y Propiedad de Archivos
- Redirección y Tuberías
- Comodines y Expansión de Rutas
- Historial y Atajos de Teclado
Módulo 3: Fundamentos de Scripting
- Creando y Ejecutando un Script
- Variables y Constantes
- Operadores Básicos
- Sentencias Condicionales
- Argumentos y Entrada del Usuario
- Comillas, Expansión y Sustitución
Módulo 4: Scripting Intermedio
- Bucles en Bash
- Funciones en Bash
- Arrays y Arrays Asociativos
- Manipulación de Cadenas
- La Sentencia case y los Menús Interactivos
- Aritmética y Cálculos Numéricos
Módulo 5: Técnicas Avanzadas de Scripting
- Operaciones Avanzadas con Archivos
- Gestión de Procesos
- Manejo de Errores y Depuración
- Expresiones Regulares
- Entrada/Salida Avanzada: Descriptores y Here-Documents
- Scripts Modulares y Librerías Reutilizables
Módulo 6: Trabajando con Herramientas Externas
Módulo 7: Automatización y Programación
- Trabajos Cron
- Automatizando Tareas
- Scripts de Respaldo y Restauración
- Monitoreo y Registro
- Servicios y Temporizadores con systemd
- Automatización Remota con SSH
Módulo 8: Mejores Prácticas y Optimización
- Escribiendo Código Legible
- Optimizando Scripts en Bash
- Consideraciones de Seguridad
- Control de Versiones con Git
- Análisis Estático con ShellCheck y shfmt
- Pruebas Automatizadas con Bats
- Portabilidad: POSIX sh frente a Bashismos
