Aquesta lliçó salda un deute que arrosseguem des del Mòdul 2. Quan vam presentar grep a 02-02 vam dir explícitament que els seus patrons són molt més potents que els comodins i que ho deixàvem per més endavant; quan a 02-05 vam comparar el globbing amb altres formes d'aparellar text, ho vam tornar a ajornar. I en tancar 05-03 va quedar clar on fa mal: el toolkit sap validar rutes, processos i codis de sortida, però quan ha de validar text —que --data 2026-08-03 sigui una data de debò, que una IP sigui una IP, que una línia d'app.log tingui l'estructura esperada— recorre a comparacions fràgils amb globs. Les expressions regulars són el llenguatge que resol això, i amb l'operador =~ de Bash validen i extreuen en un sol pas.
Contingut
- Regex enfront del globbing
- Els tres sabors: BRE, ERE i PCRE
- Literals, el punt i les classes
- Classes POSIX
- Àncores
- Quantificadors i avarícia
- Alternança, agrupació i retroreferències
- Escapaments i barres invertides dins de cometes
- L'operador
=~de Bash BASH_REMATCH: validar i extreure alhora- Validacions reals
- Extracció amb
grep -o - Quan NO fer servir regex
- Regex enfront del globbing
Les dues sintaxis s'assemblen prou per confondre, i signifiquen coses diferents:
| Globbing (02-05) | Regex | |
|---|---|---|
| Qui ho interpreta | El shell, sobre noms de fitxer | Eines, sobre text |
* |
Qualsevol seqüència de caràcters | «Zero o més de l'element anterior» |
? |
Exactament un caràcter | «Zero o una vegada de l'anterior» |
| Un caràcter qualsevol | ? |
. |
| Qualsevol seqüència | * |
.* |
| Aparellament | La cadena sencera | Una subcadena, tret d'àncores |
[abc] |
Igual en tots dos | Igual en tots dos |
Els dos paranys de la taula són els que causen gairebé tots els errors de principiant. Primer: en regex, * no significa res per si sol; modifica l'element que té al davant, així que *.log és una regex incorrecta i el que vols escriure és .*\.log. Segon: grep ERROR troba la paraula en qualsevol posició de la línia, mentre que [[ $x == ERROR ]] exigeix igualtat completa. Per exigir la línia sencera en regex cal ancorar: ^ERROR$.
- Els tres sabors: BRE, ERE i PCRE
Existeixen tres dialectes, i la diferència pràctica és en quins caràcters necessiten barra invertida:
- BRE (Basic):
+,?,{,|,(i)són literals; per fer-los servir com a metacaràcters cal escapar-los (\+,\|). - ERE (Extended): aquests caràcters són metacaràcters directament. És el dialecte còmode.
- PCRE (estil Perl): ERE més dreceres (
\d,\w,\s,\b), quantificadors mandrosos (*?) i molt més.
| Eina | Sabor per defecte | Com canviar-lo |
|---|---|---|
grep |
BRE | grep -E → ERE; grep -P → PCRE |
egrep |
ERE | Obsolet: fes servir grep -E |
sed |
BRE | sed -E (o sed -r) → ERE |
awk |
ERE | No aplica |
[[ =~ ]] de Bash |
ERE | No es pot canviar |
expr, vi |
BRE | — |
La recomanació pràctica: fes servir ERE sempre que puguis (grep -E, sed -E, awk, [[ =~ ]]). Tota aquesta lliçó està escrita en ERE tret d'on s'indiqui. grep -P no està disponible a tots els sistemes —no el porta macOS ni alguns contenidors mínims—, així que evita'l en scripts portables.
- Literals, el punt i les classes
La majoria dels caràcters es representen a si mateixos. Els especials són . [ ] ^ $ * + ? { } ( ) | \.
grep -E 'lliurat' /srv/veloz/dades/enviaments.csv # literal
grep -E 'a.opez' enviaments.csv # . = un caràcter QUALSEVOL
grep -E '[aeiou]' fitxer # un d'aquests caràcters
grep -E '[^0-9]' fitxer # ^ dins de [ ]: NEGACIÓ
grep -E '[A-Za-z0-9_]' fitxer # rangs combinatsDins dels claudàtors gairebé tot perd el seu significat especial: [.*+] aparella un punt, un asterisc o un signe més literals. Les tres excepcions són ^ (si va primer, nega), - (si va al mig, forma rang: posa'l al principi o al final perquè sigui literal) i ] (ha d'anar el primer).
El . és l'error més comú en buscar extensions o IPs: grep -E '192.168.1.1' també troba 192x168y1z1. Per a un punt literal, escapa'l: 192\.168\.1\.1.
- Classes POSIX
A grep -E '[[:digit:]]{4}-[[:digit:]]{2}' app.log, els noms van entre [: :] dins d'uns claudàtors de classe, d'aquí el doble parell:
| Classe POSIX | Equival a | Ús |
|---|---|---|
[[:digit:]] |
[0-9] |
Dígits |
[[:alpha:]] / [[:alnum:]] |
[A-Za-z] / [A-Za-z0-9] |
Lletres / lletres i dígits |
[[:space:]] |
espai, tabulador, salt | Espai en blanc |
[[:upper:]] / [[:lower:]] |
[A-Z] / [a-z] |
Majúscules / minúscules |
[[:punct:]] / [[:xdigit:]] |
.,;:!?... / [0-9A-Fa-f] |
Puntuació / hexadecimal |
Per què preferir-les a [0-9]? Per les locales. Un rang com [a-z] s'interpreta segons l'ordre de col·lació de l'idioma configurat: en algunes locales inclou caràcters accentuats i en altres no, i [A-z] (error tipogràfic freqüent) abasta a més [, \, ], ^, _ i la cometa invertida. [[:alpha:]] significa «lletra» en qualsevol locale, sense sorpreses, i en un servidor on LANG pot canviar entre desplegaments això és exactament el que vols. Per a dígits, [0-9] i [[:digit:]] són equivalents a la pràctica; per a lletres, la diferència és real.
- Àncores
grep -E '^2026-08-03' app.log # línies que COMENCEN per aquesta data
grep -E 'ERROR$' app.log # línies que ACABEN en ERROR
grep -E '^$' fitxer # línies buides
grep -E '^[[:space:]]*$' fitxer # línies buides o només amb espais
grep -E '^alopez,' enviaments.csv # primer camp exacte^ i $ no consumeixen cap caràcter: marquen posicions. I \b marca un límit de paraula, la frontera entre un caràcter de paraula i un que no ho és: grep -E 'jruiz' també troba jruizperez, mentre que grep -E '\bjruiz\b' acota al repartidor exacte.
Ancorar és també una qüestió de rendiment i de seguretat: una validació sense ^ i $ accepta brossa al voltant del que és vàlid, que és com s'esmunyen dades malformades cap a un informe.
- Quantificadors i avarícia
| Quantificador | Significat | Exemple |
|---|---|---|
* |
Zero o més vegades | [0-9]* |
+ |
Una o més vegades | [0-9]+ |
? |
Zero o una vegada (opcional) | https? |
{n} / {n,} / {n,m} |
Exactament n / n o més / entre n i m | [0-9]{4}, [0-9]{1,3} |
Els quantificadors són avariciosos (greedy): consumeixen tot el que poden i després recularan just el necessari perquè la resta encaixi. L'efecte pràctic es veu en extreure la ruta d'una línia d'acces.log:
linia='10.0.0.5 - [03/Aug/2026] "GET /envios/1234 HTTP/1.1" 200 512'
grep -oE '".*"' <<< "$linia" # "GET /envios/1234 HTTP/1.1" ← aquí encerta
grep -oE '".*" 2' <<< "$linia" # amb dues cometes separades, passaria de llarg
grep -oE '"[^"]*"' <<< "$linia" # SEMPRE correcte: fins a la cometa següent.* entre cometes dobles aparella fins a l'última cometa de la línia, no fins a la primera. L'idioma robust és [^X]* —«tot el que no sigui el delimitador»—, que no depèn de l'avarícia. ERE no té quantificadors mandrosos (.*?); això és PCRE. En ERE, [^"]* és la resposta.
- Alternança, agrupació i retroreferències
grep -E 'ERROR|WARN' app.log # alternança: una o l'altra
grep -E '^(ERROR|WARN):' app.log # agrupada i ancorada
grep -E '(19|20)[0-9]{2}' app.log # any plausible
grep -E '^(.*),\1$' fitxer # retroreferència: primer camp = últimL'alternança té la prioritat més baixa de tot el llenguatge, així que ^ERROR|WARN$ significa «comença per ERROR, o acaba en WARN», que gairebé mai no és el que es vol. Els parèntesis ho arreglen i, a més, capturen el fragment aparellat per reutilitzar-lo: \1 és el contingut del primer grup, \2 el del segon. Els grups es numeren per l'ordre dels seus parèntesis d'obertura.
Les retroreferències són cares i no totes les eines les admeten en tots els sabors (awk no en té), però el seu valor de debò apareix a la secció 10, on Bash les exposa com un array, i a sed (06-02), on permeten reescriure text conservant trossos de l'original.
- Escapaments i barres invertides dins de cometes
Aquí és on es perd més temps del que ningú confessaria. Hi ha dos nivells d'interpretació: primer el shell processa les cometes, i el que sobrevisqui arriba a la regex.
grep -E "\." fitxer # el shell converteix \. en . → la regex és "." qualsevol caràcter!
grep -E '\.' fitxer # cometes SIMPLES: la regex rep \. → punt literalRegla d'or: els patrons regex van sempre entre cometes simples. A dins no s'interpreta res, i el que escrius és exactament el que rep l'eina. L'única excepció és quan necessites interpolar una variable —grep -E "^[0-9]+,[^,]+,$ciutat," enviaments.csv—, i llavors convé construir el patró en una variable a part.
Els caràcters que necessiten escapament per ser literals són . [ ] ^ $ * + ? { } ( ) | \. I la barra invertida mateixa s'escriu \\ a la regex, cosa que dins de cometes dobles del shell es converteix en \\\\: un motiu més per fer servir cometes simples.
- L'operador
=~ de Bash
=~ de BashBash porta un motor ERE integrat, disponible només dins de [[ ]]: if [[ "$data" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then .... Dues regles de sintaxi, i totes dues són contraintuïtives:
- El patró NO va entre cometes. Si escrius
[[ "$x" =~ "^[0-9]+$" ]], les cometes converteixen el patró en una cadena literal i només aparellarà si$xconté exactament aquests caràcters. És l'error més freqüent amb=~, i no dona cap avís: simplement no coincideix mai. - La cadena de l'esquerra SÍ que va entre cometes, com sempre, per protegir-la de la divisió en paraules (03-06).
Si el patró és complex o conté espais, desa'l en una variable i fes servir la variable sense cometes —és la manera llegible i portable de tenir-ho tot sota control—:
readonly RE_DATA='^([0-9]{4})-([0-9]{2})-([0-9]{2})$'
[[ "$1" =~ $RE_DATA ]] || morir 64 "Data invàlida: $1 (s'espera AAAA-MM-DD)"A més, =~ no està ancorat per defecte: [[ abc123 =~ [0-9]+ ]] és cert. Per validar, ancora sempre amb ^ i $.
BASH_REMATCH: validar i extreure alhora
BASH_REMATCH: validar i extreure alhoraAquesta és la raó per la qual =~ mereix una secció pròpia. Després d'un aparellament amb èxit, Bash omple l'array BASH_REMATCH: la posició 0 és el text complet aparellat i les següents són els grups capturats, en ordre.
linia='2026-08-03 10:15:22 [ERROR] timeout al consultar veloz-api'
if [[ "$linia" =~ ^([0-9-]{10})\ ([0-9:]{8})\ \[([A-Z]+)\]\ (.*)$ ]]; then
data="${BASH_REMATCH[1]}" # 2026-08-03
hora="${BASH_REMATCH[2]}" # 10:15:22
nivell="${BASH_REMATCH[3]}" # ERROR
missatge="${BASH_REMATCH[4]}" # timeout al consultar veloz-api
printf '%s a les %s: %s\n' "$nivell" "$hora" "$missatge"
fiCompara-ho amb l'alternativa: un cut per a la data, un altre per a l'hora, un tr -d '[]' per al nivell i un ${linia#* } repetit quatre vegades per al missatge. Aquí, una sola comprovació valida el format i extreu els quatre camps, sense llançar ni un sol procés extern. En un bucle sobre un app.log de cent mil línies, la diferència es compta en minuts.
Els espais del patró van escapats (\ ) perquè, dins de [[ ]], el patró sense cometes està subjecte a la divisió en paraules. Desar-lo en una variable evita aquest soroll:
readonly RE_APPLOG='^([0-9-]{10}) ([0-9:]{8}) \[([A-Z]+)\] (.*)$'
[[ "$linia" =~ $RE_APPLOG ]] && nivell="${BASH_REMATCH[3]}"BASH_REMATCH se sobreescriu amb cada =~ amb èxit i no es neteja quan un falla, així que copia el que necessitis immediatament després de la comprovació, dins de l'if.
- Validacions reals
readonly RE_DATA='^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$'
readonly RE_OCTET='(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])'
readonly RE_IPV4="^$RE_OCTET\.$RE_OCTET\.$RE_OCTET\.$RE_OCTET$"
readonly RE_HTTP='^[1-5][0-9]{2}$'
readonly RE_CIUTAT='^(Valencia|Sevilla|Bilbao|Madrid)$'
validar_data() { [[ "$1" =~ $RE_DATA ]]; }
validar_ip() { [[ "$1" =~ $RE_IPV4 ]]; }Fixa't en el disseny de RE_DATA: no es conforma amb [0-9]{2} per al mes, sinó que exigeix 01-12 amb l'alternança (0[1-9]|1[0-2]). Tot i així no valida la data, només la seva forma: 2026-02-31 la supera. Per saber si la data existeix cal date -d "$f" &>/dev/null, que ja coneixes de 04-06. La regla general: la regex valida el format; la semàntica es comprova a part.
RE_OCTET mostra l'altre principi: construir per parts. Escriure la regex d'IPv4 d'una tirada és il·legible; compondre-la des d'un octet és evident. I així es depura, incrementalment: prova primer ^[0-9]{4}, després afegeix el mes, després el dia. Eines com regex101.com expliquen cada element i mostren les captures en viu, i al terminal n'hi ha prou amb grep -oE sobre un fitxer d'exemple per veure què aparella de debò.
- Extracció amb
grep -o
grep -oQuan el que vols no és la línia sinó el fragment:
grep -oE '^[0-9.]+' /var/log/veloz/acces.log | sort -u # IPs úniques
grep -oE '"GET [^ ]+' acces.log | cut -d' ' -f2 | sort | uniq -c | sort -rn | head
grep -coE 'ERROR' app.log # -c compta LÍNIES, no coincidències
grep -oE 'ERROR' app.log | wc -l # això sí que compta coincidènciesAquesta última parella amaga un matís que espatlla informes: grep -c compta línies que contenen almenys una coincidència; si una línia en té tres, segueix comptant 1. Per comptar coincidències cal fer servir -o i comptar línies de la sortida.
grep -o és l'eina de la canonada; BASH_REMATCH és la del bucle. Si ja estàs recorrent el fitxer línia a línia amb while IFS= read -r (04-01), =~ evita llançar un procés per línia.
- Quan NO fer servir regex
Les regex són un llenguatge regular, i hi ha estructures que no són regulars. Insistir-hi produeix codi que funciona amb els exemples i falla en producció:
- CSV amb comes dins de cometes.
"Sevilla, Est"trenca qualsevolcut -d,i qualsevol regex ingènua. Els CSV de Veloz Envíos són simples i per aixòIFS=,n'hi ha prou; el dia que deixin de ser-ho, l'eina ésawkamb un separador ben definit (06-01) o un analitzador de debò. - HTML i XML. Imbricació arbitrària: no és un llenguatge regular. Hi ha una resposta llegendària a Stack Overflow sobre això, i té raó.
- JSON. Mateixa raó. L'eina és
jq, i arriba a 06-05. - Rutes i noms de fitxer. Ja tens
find(05-01) i les expansions${s##*/}(04-04).
I un avís de rendiment: patrons amb quantificadors imbricats com (a+)+ poden provocar retrocés catastròfic i penjar el procés amb una entrada curta. Si la teva regex necessita imbricar quantificadors, gairebé sempre hi ha una formulació més simple.
Errors Habituals i Consells
- Posar entre cometes el patró de
=~. Es converteix en literal i no aparella mai, sense cap avís. - Fer servir cometes dobles en un patró de
grep. El shell es menja les barres invertides. Cometes simples. - Oblidar d'escapar el punt.
192.168.1.1aparella19216811i moltes coses més. *.logcom a regex.*modifica l'anterior; s'escriu.*\.log.- No ancorar una validació.
[[ $x =~ [0-9]+ ]]acceptaabc123defcom a número. ^ERROR|WARN$sense parèntesis. L'alternança té la prioritat més baixa i parteix l'expressió sencera.- Refiar-se de
grep -cper comptar coincidències. Compta línies. Fes servirgrep -o | wc -l. - Consell: desa cada regex en una variable
readonly RE_ALGUNA_COSAamb nom descriptiu, a prop del principi de l'script. Es documenta sola, es reutilitza, es prova per separat i desapareix el soroll de cometes.
Exercicis
Exercici 1. Escriu validar_data() que accepti només AAAA-MM-DD amb mes 01-12 i dia 01-31, i a més verifiqui que la data existeix de debò (rebutjant 2026-02-31). Ha de retornar 0 o 1 sense imprimir res.
Exercici 2. Recorre /var/log/veloz/app.log amb un sol bucle i produeix un recompte per nivell (INFO, WARN, ERROR), mostrant a més l'hora i el missatge del primer ERROR del dia, sense llançar processos externs dins del bucle.
Exercici 3. Extreu d'acces.log les IPs que hagin provocat almenys un codi 5xx, juntament amb quantes vegades, ordenades de major a menor.
Solucions
Solució 1.
readonly RE_DATA='^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$'
validar_data() { # Ús: validar_data AAAA-MM-DD
[[ "${1:-}" =~ $RE_DATA ]] || return 1
date -d "$1" > /dev/null 2>&1 # la semàntica, a part del format
}
validar_data 2026-08-03 && echo ok # ok
validar_data 2026-02-31 || echo malament # malament (format vàlid, data inexistent)
validar_data 2026-8-3 || echo malament # malament (format incorrecte)Dues capes: la regex descarta el que ni tan sols té forma de data —barat, sense processos— i date -d resol el que la regex no pot saber, com els anys de traspàs. L'ordre importa: si date anés primer, acceptaria entrades com next friday.
Solució 2.
readonly RE_APPLOG='^([0-9]{4}-[0-9]{2}-[0-9]{2}) ([0-9:]{8}) \[([A-Z]+)\] (.*)$'
declare -A NIVELLS=()
primer_error=''
while IFS= read -r linia; do
[[ "$linia" =~ $RE_APPLOG ]] || continue # descarta línies malformades
(( NIVELLS["${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 nivell in "${!NIVELLS[@]}"; do
printf '%-6s %5d\n' "$nivell" "${NIVELLS[$nivell]}"
done
[[ -n "$primer_error" ]] && printf 'Primer ERROR: %s\n' "$primer_error"És el patró comptador de 04-03 alimentat per BASH_REMATCH. El || continue converteix la validació en un filtre que protegeix el recompte de línies escombraries, i tot passa dins de Bash: ni un cut, ni un grep, ni un procés per línia.
Solució 3.
grep -E '" [5][0-9]{2} ' /var/log/veloz/acces.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 les línies el codi de les quals va després de la petició entre cometes —l'espai i la cometa eviten confondre'l amb un nombre de bytes que comenci per 5—, i el segon extreu només la IP inicial. El (\.[0-9]{1,3}){3} mostra que un grup també es pot quantificar. La rematada sort | uniq -c | sort -rn és l'idioma del Mòdul 2, que ara encaixa amb l'extracció precisa que només les regex donen.
Conclusió
Una regex descriu text, no noms de fitxer, i els seus metacaràcters s'assemblen als del globbing només prou per enganyar: * modifica l'element anterior, . és el comodí d'un caràcter i .* és l'equivalent de l'* del shell. Dels tres sabors, ERE és el punt dolç i el parlen grep -E, sed -E, awk i el =~ de Bash. La sintaxi es construeix amb literals, ., classes [...] amb la seva negació [^...], classes POSIX [[:digit:]] immunes a la locale, àncores ^, $ i \b, quantificadors *, +, ? i {n,m} —avariciosos, d'aquí l'idioma [^"]*—, alternança | de prioritat mínima i grups ( ) que capturen. Els patrons van sempre entre cometes simples, tret d'=~, on el patró va sense cometes i el millor és desar-lo en una variable readonly. I aquí hi ha la joia: BASH_REMATCH converteix una comprovació en una extracció de tots els camps alhora, sense llançar processos, ideal per a bucles llargs. El que no has de fer amb regex és analitzar CSV amb cometes, HTML o JSON: per a això hi ha eines específiques que arriben al Mòdul 6.
informe-diari.sh ja valida la seva --data i descompon app.log en un sol pas. El que queda per resoldre és la sortida: avui imprimeix per pantalla amb printf solts, i si vols l'informe en un fitxer has de redirigir tot l'script des de fora. No sap escriure a dos llocs alhora, ni mantenir obert un fitxer de registre mentre treballa, ni compondre una plantilla d'informe de diverses línies sense vint printf. A 05-05 entren els descriptors de fitxer, els here-documents, els here-strings i la substitució de processos: la maquinària d'entrada/sortida que converteix la sortida del toolkit en una cosa que es pot dirigir amb precisió.
Curs de Programació en Bash
Mòdul 1: Introducció a Bash
- Què és Bash?
- Configurar el teu Entorn
- Navegació Bàsica per la Línia d'Ordres
- Entendre el Shell
- Trobar Ajuda: man, help i --help
Mòdul 2: Ordres Bàsiques de Bash
- Operacions amb Fitxers i Directoris
- Ordres de Processament de Text
- Permisos i Propietat dels Fitxers
- Redirecció i Canonades
- Comodins i Expansió de Rutes
- Historial i Dreceres de Teclat
Mòdul 3: Fonaments de Scripting
- Crear i Executar un Script
- Variables i Constants
- Operadors Bàsics
- Sentències Condicionals
- Arguments i Entrada de l'Usuari
- Cometes, Expansió i Substitució
Mòdul 4: Scripting Intermedi
- Bucles en Bash
- Funcions en Bash
- Arrays i Arrays Associatius
- Manipulació de Cadenes
- La Sentència case i els Menús Interactius
- Aritmètica i Càlculs Numèrics
Mòdul 5: Tècniques Avançades de Scripting
- Operacions Avançades amb Fitxers
- Gestió de Processos
- Gestió d'Errors i Depuració
- Expressions Regulars
- Entrada/Sortida Avançada: Descriptors i Here-Documents
- Scripts Modulars i Llibreries Reutilitzables
Mòdul 6: Treballar amb Eines Externes
Mòdul 7: Automatització i Programació
- Tasques Cron
- Automatitzar Tasques
- Scripts de Còpia i Restauració
- Monitoratge i Registre
- Serveis i Temporitzadors amb systemd
- Automatització Remota amb SSH
Mòdul 8: Bones Pràctiques i Optimització
- Escriure Codi Llegible
- Optimitzar Scripts en Bash
- Consideracions de Seguretat
- Control de Versions amb Git
- Anàlisi Estàtica amb ShellCheck i shfmt
- Proves Automatitzades amb Bats
- Portabilitat: POSIX sh enfront de Bashismes
