1. Expresiones regulares y comodines
Una expresión regular es un patrón que describe un conjunto de textos posibles: «uno o más dígitos», «una fecha con forma AAAA-MM-DD», «una palabra seguida de dos puntos». Sirve para comprobar si un texto tiene una forma determinada, y para encontrar o extraer partes de él.
Ya conoces otra clase de patrones: los comodines de los nombres de archivo, de case y del == de [[ ]]. Se parecen, y eso causa confusiones, porque los mismos símbolos significan cosas distintas:
| Comodines | Expresiones regulares | |
|---|---|---|
| Dónde | Nombres de archivo, case, [[ == ]], ${var#…}. | [[ =~ ]], grep, sed, awk. |
| Cualquier texto | * | .* |
| Un carácter cualquiera | ? | . |
| Repetir lo anterior | No se puede (salvo con extglob). | *, +, ?, {n,m} |
| ¿Debe coincidir con todo el texto? | Sí, siempre. | No: basta con una parte, salvo que se anclen con ^ y $. |
La diferencia más traicionera es el asterisco: en un comodín significa «cualquier texto», y en una expresión regular, «lo anterior, repetido cero o más veces». *.txt es un comodín correcto; como expresión regular no tiene sentido.
2. El operador =~
Dentro de [[ ]], el operador =~ compara un texto con una expresión regular. Es cierto si alguna parte del texto coincide:
$ [[ "abc123" =~ [0-9]+ ]] && echo "contiene dígitos"
contiene dígitos
$ [[ "abc123" =~ ^[0-9]+$ ]] || echo "pero no son solo dígitos"
pero no son solo dígitos
La primera expresión no tiene anclas, y encuentra los dígitos en medio del texto. La segunda las tiene: ^ significa «comienzo del texto» y $, «final», de modo que todo el texto debe ser dígitos. Para validar datos, casi siempre se quieren las dos anclas: sin ellas, 4x pasaría como número porque contiene un 4.
Como toda prueba de [[ ]], =~ termina con 0 si coincide y con 1 si no. Si la expresión está mal escrita, termina con 2. Se usa directamente en if, && y ||.
3. Sin comillas, o en una variable
Aquí hay una regla que contradice la costumbre del curso: el lado derecho de =~ no va entre comillas. Las comillas hacen que el texto se tome literalmente, como en el == del tema 11, y los símbolos de la expresión pierden su significado:
$ [[ "abc" =~ a.c ]] && echo "el punto es «cualquier carácter»"
el punto es «cualquier carácter»
$ [[ "abc" =~ "a.c" ]] || echo "entre comillas, el punto es un punto"
entre comillas, el punto es un punto
Escribir la expresión sin comillas directamente en [[ ]] funciona con las simples, pero se complica en cuanto tiene espacios, <, > o paréntesis, que Bash intenta interpretar. La forma recomendada, y la que usa el curso, es guardar la expresión en una variable con comillas simples, y usar la variable sin comillas:
readonly RE_FECHA='^([0-9]{4})-([0-9]{2})-([0-9]{2})$'
if [[ "$fecha" =~ $RE_FECHA ]]; then
…
fi
Las comillas simples protegen la expresión al asignarla, sin necesidad de escapar nada; $RE_FECHA sin comillas en el =~ hace que se use como expresión. El lado izquierdo, el texto, sí va entre comillas, como siempre. Además, con un nombre como RE_FECHA, la condición se lee sola.
4. La sintaxis que entiende Bash
Bash usa las expresiones regulares extendidas de POSIX (ERE), las mismas de grep -E y awk. Estos son sus elementos:
| Elemento | Coincide con | Ejemplo |
|---|---|---|
a | Ese carácter. | abc: el texto «abc». |
. | Un carácter cualquiera. | a.c: «abc», «a-c», «a c»… |
[abc] | Uno de esos caracteres. | [sS]í: «sí» o «Sí». |
[a-z], [0-9] | Uno del rango. | [0-9]: un dígito. |
[^abc] | Cualquier carácter excepto esos. | [^,]: cualquier cosa menos una coma. |
^ y $ | El comienzo y el final del texto. | ^#: empieza con #. |
* | Lo anterior, cero o más veces. | ab*: «a», «ab», «abbb». |
+ | Lo anterior, una o más veces. | [0-9]+: un número. |
? | Lo anterior, cero o una vez. | -?[0-9]+: con signo opcional. |
{n}, {n,}, {n,m} | Lo anterior, exactamente n veces, al menos n, o entre n y m. | [0-9]{4}: cuatro dígitos. |
( ) | Agrupa, y captura lo que coincide (sección 6). | (ab)+: «ab», «abab». |
| | Una alternativa u otra. | ^(si|no)$: «si» o «no». |
\., \+… | El carácter literal, sin su significado especial. | \.txt$: termina en «.txt». |
Muchos lenguajes, y muchos tutoriales en Internet, usan \d para un dígito, \w para una letra o \s para un espacio. Son extensiones de Perl, y la sintaxis POSIX de Bash no las tiene: [[ 5 =~ \d ]] es falso. En su lugar se usan rangos, como [0-9], o las clases de la sección siguiente.
5. Las clases de caracteres
Dentro de los corchetes se pueden usar clases con nombre, que describen grupos de caracteres sin depender de rangos:
| Clase | Coincide con | Equivale a |
|---|---|---|
[[:digit:]] | Un dígito. | [0-9] |
[[:alpha:]] | Una letra, incluidas las acentuadas y la eñe. | — |
[[:alnum:]] | Una letra o un dígito. | — |
[[:upper:]], [[:lower:]] | Una mayúscula, una minúscula. | — |
[[:space:]] | Un espacio, tabulador o salto de línea. | Lo que en otros lenguajes es \s. |
[[:punct:]] | Un signo de puntuación. | — |
Los dobles corchetes no son un error: la clase [:digit:] va dentro de los corchetes de un conjunto, y puede combinarse con otros caracteres, como en [[:alnum:]_-], «letra, dígito, guion bajo o guion». Las clases tienen una ventaja sobre los rangos: respetan la configuración regional. [a-z] no incluye la «ñ» ni las vocales con tilde, y en algunas configuraciones regionales incluye mayúsculas de forma inesperada; [[:lower:]] incluye todas las minúsculas del idioma.
6. Capturar partes: BASH_REMATCH
Cuando =~ encuentra una coincidencia, Bash guarda el resultado en el array BASH_REMATCH. El elemento 0 es todo lo que coincidió, y los siguientes, lo que coincidió con cada grupo entre paréntesis, en el orden en que se abren:
$ fecha="2026-10-14"
$ re='^([0-9]{4})-([0-9]{2})-([0-9]{2})$'
$ [[ $fecha =~ $re ]] && declare -p BASH_REMATCH
declare -a BASH_REMATCH=([0]="2026-10-14" [1]="2026" [2]="10" [3]="14")
$ echo "Día ${BASH_REMATCH[3]} del mes ${BASH_REMATCH[2]}"
Día 14 del mes 10
Así, una sola comparación valida la forma del texto y, a la vez, lo descompone en sus partes, algo que con los recortes del tema 6 requeriría varias líneas. Dos precauciones:
BASH_REMATCHse reemplaza en cada=~, incluso si no coincide. Si los valores se van a usar más adelante, conviene copiarlos a variables con nombre enseguida:anio=${BASH_REMATCH[1]}.- Los números capturados pueden tener ceros a la izquierda, como el
08de un mes. Antes de usarlos en aritmética, se pasan por10#, por el problema del octal del tema 7.
7. Validar los datos más habituales
Estas expresiones cubren las validaciones que más aparecen en scripts. Todas van ancladas, porque validan el texto completo:
| Dato | Expresión | Acepta | Rechaza |
|---|---|---|---|
| Entero no negativo | ^[0-9]+$ | 42, 007 | 4x, -3, vacío |
| Entero con signo | ^-?[0-9]+$ | -3, 42 | +-3, 3.5 |
| Decimal | ^-?[0-9]+(\.[0-9]+)?$ | 3.14, -2 | 3., .5, 3,14 |
| Fecha AAAA-MM-DD | ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ | 2026-10-14 | 14/10/2026 |
| Hora HH:MM | ^([01][0-9]|2[0-3]):[0-5][0-9]$ | 09:30, 23:59 | 24:00, 9:30 |
| Nombre de usuario | ^[a-z_][a-z0-9_-]{0,31}$ | ana, web_01 | Ana, 1ana |
| Correo (forma general) | ^[^@[:space:]]+@[^@[:space:]]+\.[^@[:space:]]+$ | ana@ejemplo.com | ana@ejemplo, ana @x.com |
| Dirección IPv4 (forma) | ^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$ | 192.168.1.10 | 1.2.3 |
Hay que saber qué no comprueban. La expresión de fecha acepta 2026-02-30, porque solo mira la forma. La de IPv4 acepta 300.1.1.1, porque cada número puede tener tres dígitos cualesquiera. Para esos casos, la expresión verifica la forma y captura las partes, y una segunda comprobación verifica el significado:
# Fecha: la forma con =~, la validez con date.
[[ "$fecha" =~ $RE_FECHA ]] && date -d "$fecha" > /dev/null 2>&1
# IPv4: la forma con =~, cada número entre 0 y 255 con aritmética.
es_ipv4() {
local octeto
[[ "$1" =~ $RE_IPV4 ]] || return 1
for octeto in "${BASH_REMATCH[@]:1}"; do
(( 10#$octeto <= 255 )) || return 1
done
}
El correo merece una advertencia: una expresión que acepte exactamente todas las direcciones válidas y ninguna inválida es enorme e inútil en la práctica. La de la tabla comprueba lo razonable: algo, una arroba, algo con un punto, y sin espacios. La única forma de saber si una dirección existe es enviarle un correo.
8. Extraer campos de una línea
La otra gran aplicación es descomponer líneas con un formato conocido, como las de un registro. Con un grupo por cada campo, una sola comparación los separa a todos:
$ linea='2026-10-14 03:15:02 ERROR [db] conexión rechazada'
$ re='^([0-9-]+) ([0-9:]+) ([A-Z]+) \[([a-z]+)\] (.*)$'
$ [[ $linea =~ $re ]] && printf '<%s> ' "${BASH_REMATCH[@]:1}"; echo
<2026-10-14> <03:15:02> <ERROR> <db> <conexión rechazada>
Los corchetes literales del registro se escapan con \[ y \], y el último grupo, (.*), se lleva todo el resto de la línea, espacios incluidos. Cuando los campos están separados por un carácter fijo, separar con IFS y read, como en el tema 9, es más simple; la expresión regular conviene cuando el formato mezcla separadores, como aquí, donde hay espacios, corchetes y un texto libre al final.
Para extraer de muchas líneas de un archivo, un bucle con =~ funciona, pero es lento con archivos grandes. Ahí conviene grep -o, sed o awk, el tema siguiente.
9. Las repeticiones son codiciosas
Cuando una repetición como .* puede coincidir con más o con menos texto, toma siempre lo más que puede. Esto sorprende al extraer algo entre delimitadores:
$ s='aaa<b>bbb</b>'
$ re='<.*>'; [[ $s =~ $re ]] && echo "${BASH_REMATCH[0]}"
<b>bbb</b>
$ re='<[^>]*>'; [[ $s =~ $re ]] && echo "${BASH_REMATCH[0]}"
<b>
La primera expresión se extendió desde el primer < hasta el último >. La solución, en la sintaxis POSIX, es decir con precisión qué puede haber en medio: [^>]*, «cualquier cosa que no sea >». Otros lenguajes tienen repeticiones no codiciosas, como .*?, pero Bash no.
10. Bash, grep y las demás herramientas
Las expresiones regulares no son exactamente iguales en todas las herramientas. Hay tres dialectos principales:
| Dialecto | Dónde | Diferencia principal |
|---|---|---|
| Básico (BRE) | grep sin opciones, sed sin -E | +, ?, |, { } y ( ) necesitan una barra para tener significado especial: \+, \(. |
| Extendido (ERE) | [[ =~ ]], grep -E, sed -E, awk | Los símbolos funcionan sin barra. Es el que usa este curso. |
| Perl (PCRE) | grep -P, la mayoría de los lenguajes de programación | Agrega \d, \w, \s, repeticiones no codiciosas y más. |
Para no mezclar dialectos, conviene usar siempre la forma extendida: grep -E y sed -E. Así, una misma expresión puede probarse en la terminal con grep -E y usarse después en un =~ sin cambios.
11. Cuándo no usar expresiones regulares
Las expresiones regulares son poderosas, y por eso tentadoras para todo. Hay casos en que son la herramienta equivocada:
- Formatos anidados: HTML, XML y JSON pueden tener estructuras dentro de estructuras, que una expresión regular no puede seguir. Para JSON existe
jq; para XML,xmllintoxmlstarlet. - CSV con comillas: un campo entre comillas puede contener comas y saltos de línea. Lo veremos en el tema 30.
- Lo que ya resuelve algo más simple: «termina en
.txt» es un comodín ([[ $f == *.txt ]]); «separar por dos puntos» esIFS. Una expresión regular ilegible donde bastaba algo simple es un error de estilo. - Validaciones con significado: si una fecha existe, si un número está en rango, si un archivo existe. La expresión solo ve la forma.
Y una expresión larga se vuelve rápidamente ilegible. Si no se entiende de un vistazo, conviene partirla, comentarla, o guardarla en una variable con un nombre que diga qué valida.
12. Un script completo
Muchos proyectos guardan su número de versión en un archivo VERSION, con el formato de versionado semántico: tres números, MAYOR.MENOR.PARCHE, que se incrementan según el tipo de cambio. Este script valida la versión actual con una expresión regular, la descompone con BASH_REMATCH, incrementa la parte indicada y escribe la nueva de forma atómica.
#!/usr/bin/env bash
#
# subir-version — incrementa una versión semántica guardada en un archivo.
#
# Uso: subir-version {mayor|menor|parche} [ARCHIVO] (por defecto, VERSION)
# 1.4.2 → mayor: 2.0.0 · menor: 1.5.0 · parche: 1.4.3
#
set -euo pipefail
readonly RE_VERSION='^v?([0-9]+)\.([0-9]+)\.([0-9]+)(-[0-9A-Za-z.-]+)?$'
uso() {
echo "Uso: ${0##*/} {mayor|menor|parche} [ARCHIVO] (por defecto, VERSION)" >&2
exit 2
}
(( $# >= 1 && $# <= 2 )) || uso
parte="$1"
archivo="${2:-VERSION}"
[[ "$parte" =~ ^(mayor|menor|parche)$ ]] || uso
[[ -f "$archivo" ]] || { echo "${0##*/}: no existe $archivo" >&2; exit 1; }
IFS= read -r actual < "$archivo" || true
actual="${actual%$'\r'}" # por si el archivo viene de Windows
if [[ ! "$actual" =~ $RE_VERSION ]]; then
echo "${0##*/}: «$actual» no es una versión válida (se esperaba como 1.4.2)" >&2
exit 1
fi
mayor=$(( 10#${BASH_REMATCH[1]} ))
menor=$(( 10#${BASH_REMATCH[2]} ))
parche=$(( 10#${BASH_REMATCH[3]} ))
prefijo=""
[[ "$actual" == v* ]] && prefijo="v" # conservar la «v» inicial si la tenía
case "$parte" in
mayor) mayor=$(( mayor + 1 )); menor=0; parche=0 ;;
menor) menor=$(( menor + 1 )); parche=0 ;;
parche) parche=$(( parche + 1 )) ;;
esac
nueva="$prefijo$mayor.$menor.$parche"
tmp=$(mktemp "$archivo.XXXXXX")
trap 'rm -f "$tmp"' EXIT
printf '%s\n' "$nueva" > "$tmp"
mv -f -- "$tmp" "$archivo"
echo "$actual → $nueva"
$ cat VERSION
1.4.2
$ subir-version parche
1.4.2 → 1.4.3
$ subir-version menor
1.4.3 → 1.5.0
$ subir-version mayor
1.5.0 → 2.0.0
$ echo 1.4 > VERSION; subir-version parche
subir-version: «1.4» no es una versión válida (se esperaba como 1.4.2)
- Una expresión, dos trabajos:
RE_VERSIONvalida la forma completa, con anclas, y captura los tres números en los grupos 1, 2 y 3. La parte opcional(-[0-9A-Za-z.-]+)?acepta un sufijo de versión preliminar, como-rc.1; el script lo descarta al incrementar. - El argumento también se valida con una expresión,
^(mayor|menor|parche)$, aunque uncasehabría servido igual: es un ejemplo de alternativas con|y anclas. - Los números pasan por
10#, de modo que una versión como1.09.3se incrementa correctamente a1.10.0en lugar de fallar por el octal. - La escritura es atómica, como en el tema 24: si algo falla, el archivo
VERSIONqueda como estaba.
13. Errores frecuentes
| Escribiste | Qué pasa | Corrección |
|---|---|---|
[[ $x =~ "^[0-9]+$" ]] | Las comillas vuelven literal la expresión. | Sin comillas, o en una variable. |
[[ $x =~ [0-9]+ ]] para validar | 4x pasa, porque contiene un dígito. | Anclas: ^[0-9]+$. |
\d, \w, \s | No existen en la sintaxis POSIX. | [0-9], [[:alnum:]_], [[:space:]]. |
*.txt como expresión regular | El * no significa «cualquier texto». | \.txt$, o un comodín con ==. |
[a-z] para letras en español | No incluye «ñ» ni vocales con tilde. | [[:lower:]], [[:alpha:]]. |
Usar BASH_REMATCH varias líneas después | Otro =~ lo reemplazó. | Copiar a variables con nombre enseguida. |
<.*> para extraer una etiqueta | Se extiende hasta el último >. | <[^>]*> |
| Validar una fecha solo con la expresión | Acepta 2026-02-30. | La expresión para la forma; date -d para la validez. |
14. Resumen
- Una expresión regular describe la forma de un texto. Sus símbolos no significan lo mismo que en los comodines:
*repite lo anterior, y.es un carácter cualquiera. [[ texto =~ expresión ]]es cierto si alguna parte del texto coincide; con^y$, tiene que coincidir todo.- La expresión va sin comillas, mejor guardada en una variable con comillas simples; el texto, entre comillas.
- Bash usa la sintaxis extendida de POSIX:
[ ],^ $,* + ?,{n,m},( ),|. No existen\d,\wni\s. - Las clases como
[[:digit:]],[[:alpha:]]y[[:space:]]respetan el idioma, a diferencia de los rangos como[a-z]. BASH_REMATCH[0]es la coincidencia completa yBASH_REMATCH[n], cada grupo; se reemplaza en cada=~.- Para validar se anclan las expresiones; la forma se comprueba con la expresión, y el significado (fechas, rangos) con otra prueba.
- Con grupos, una línea de formato conocido se descompone en sus campos.
- Las repeticiones son codiciosas; se acotan con conjuntos negados como
[^>]*. grep -E,sed -Eyawkusan el mismo dialecto que Bash.- Para HTML, JSON o CSV con comillas, hay herramientas mejores; para lo simple, comodines o
IFS.
15. Preguntas de repaso
Intenta responder cada una antes de abrir la respuesta.
1. ¿Qué significa el asterisco en un comodín y en una expresión regular?
En un comodín, cualquier texto. En una expresión regular, que lo anterior se repite cero o más veces; «cualquier texto» se escribe .*.
2. ¿Por qué [[ "4x" =~ [0-9]+ ]] es cierto, y cómo se corrige para validar un número?
Porque sin anclas basta con que una parte del texto coincida, y 4 es un dígito. Con ^[0-9]+$, todo el texto debe ser dígitos.
3. ¿Por qué la expresión de =~ no debe ir entre comillas?
Porque las comillas hacen que se compare literalmente, y los símbolos como . o + pierden su significado especial.
4. ¿Qué ventaja tiene guardar la expresión en una variable?
Se escribe con comillas simples, sin preocuparse por los caracteres que Bash interpretaría, como espacios, < o paréntesis; y un nombre como RE_FECHA hace la condición más legible.
5. ¿Por qué [[ 5 =~ \d ]] es falso?
Porque \d es una extensión de Perl que no existe en la sintaxis POSIX de Bash. Se usa [0-9] o [[:digit:]].
6. ¿Qué contiene BASH_REMATCH después de [[ "ana:1000" =~ ^([a-z]+):([0-9]+)$ ]]?
[0] es ana:1000, [1] es ana y [2] es 1000.
7. ¿Por qué conviene usar [[:lower:]] en lugar de [a-z]?
Porque [a-z] no incluye la «ñ» ni las vocales con tilde, y su significado depende de la configuración regional. [[:lower:]] incluye todas las minúsculas del idioma.
8. ¿Qué no comprueba ^[0-9]{4}-[0-9]{2}-[0-9]{2}$, y cómo se completa la validación?
Que la fecha exista: acepta 2026-02-30 o 2026-13-01. Se completa comprobando con date -d "$fecha", que falla con fechas inexistentes.
9. ¿Por qué <.*> aplicado a <b>x</b> captura todo el texto?
Porque .* es codicioso: toma lo más que puede, hasta el último >. Con <[^>]*> se detiene en el primer >.
10. ¿Qué opción de grep usa el mismo dialecto que =~?
grep -E, que usa las expresiones regulares extendidas de POSIX.
Para practicar
- Escribe expresiones para validar un código postal de tu país, un número de teléfono con un formato fijo y una matrícula de vehículo. Pruébalas con casos válidos e inválidos.
- Escribe una función
es_fechaque valide la forma con=~y la existencia condate -d. - Completa la función
es_ipv4de la sección 7 y pruébala con192.168.1.1,256.1.1.1,1.2.3y01.02.03.04. - Toma una línea de
/var/log/syslogo dejournalctly escribe una expresión que separe la fecha, el equipo, el programa y el mensaje. - Comprueba con
grep -Een la terminal una expresión, y úsala después sin cambios en un=~. - Agrega a
subir-versionuna opción para fijar un sufijo preliminar, como-rc.1, y valídalo con su propia expresión.