Espressioni regolari

Una espressione regolare è un modo compatto per descrivere un pattern (uno schema) che vogliamo cercare all'interno di un testo. Invece di dire a parole "una sequenza di cifre, seguita da uno slash, seguita da altre due cifre…", lo scriviamo in modo formale e la macchina lo cerca per noi.

Questa nota separa i concetti (validi ovunque) dalla resa in ciascun linguaggio, perché le regex sembrano uguali dappertutto ma cambiano il motore e il dialetto (flavor) della sintassi. Nove linguaggi ricorrono come illustrazioni: C e Bash per il mondo POSIX, Java e JavaScript per PCRE/ECMAScript, Go per RE2, R (vedi fondamenti_r.md) come ambiente con due flavor coesistenti, Guile (vedi fondamenti_guile.md) con l'approccio SRFI-115 a s-espressioni, e OCaml/Haskell come approfondimenti idiomatici.

1. Cosa sono e a cosa servono

Le usi ogni volta che devi:

Esempio mentale: il pattern [0-9]{2}/[0-9]{2}/[0-9]{4} significa "due cifre, uno slash, due cifre, uno slash, quattro cifre" — cioè una data tipo 27/08/2026.

2. Un po' di teoria (leggera)

Non serve una laurea in informatica teorica, ma due idee chiariscono perché le regex si comportano come si comportano — e soprattutto cosa non possono fare.

2.1 Cosa descrive una regex

Formalmente, una regex descrive un linguaggio regolare: un insieme (anche infinito) di stringhe. Si costruisce da tre operazioni fondamentali, che ritrovi tali e quali nella sintassi:

Partendo dal carattere singolo e dalla stringa vuota $\varepsilon$, con queste tre operazioni costruisci qualsiasi pattern. Tutto il resto (+, ?, \d, [...]) è zucchero sintattico: scorciatoie comode per cose che potresti scrivere anche con solo queste tre. Un linguaggio regolare si può anche definire ricorsivamente:

\[L ::= \varnothing \;\mid\; \varepsilon \;\mid\; a \;\mid\; L_1 L_2 \;\mid\; L_1 \cup L_2 \;\mid\; L^{*}\]

2.2 Perché "regolari": il legame con gli automi

Ogni linguaggio regolare corrisponde a un automa a stati finiti: una macchinetta con un numero fissato di stati che legge la stringa un carattere alla volta e decide se accettarla. "Numero finito di stati" è la chiave: l'automa non ha memoria per contare quantità arbitrarie.

2.3 Cosa le regex NON possono fare

Poiché l'automa non può contare all'infinito, le regex non sanno gestire strutture annidate e bilanciate di profondità arbitraria: parentesi aperte/chiuse, tag HTML dentro altri tag, JSON. Non si può scrivere una vera regex (nel senso teorico) che riconosca "tante ( quante ), correttamente annidate", perché richiederebbe di contare e ricordare quante ne hai aperte — cosa che un numero finito di stati non permette.

Regola d'oro: per HTML, JSON, XML, codice sorgente e in generale tutto ciò che è annidato, non usare le regex: usa un vero parser. Le regex vanno bene per l'analisi riga per riga o su pezzi "piatti", non per la struttura ad albero.

Alcuni motori moderni (PCRE) aggiungono estensioni — pattern ricorsivi, gruppi bilancianti — che vanno oltre i linguaggi regolari "veri". Sono trucchi specifici e fragili: quando servono, quasi sempre conviene un parser.

3. Sintassi: i mattoni

Ogni voce ha un micro-esempio. La sintassi mostrata è quella PCRE/Perl-like, la più diffusa; le differenze tra flavor sono nel §6.

3.1 Letterali e metacaratteri

La maggior parte dei caratteri corrisponde a se stessa: casa trova esattamente casa. Alcuni caratteri hanno un significato speciale (i metacaratteri): . ^ $ * + ? ( ) [ ] { } | \. Per cercare un metacarattere letteralmente lo si fa precedere da \ (escaping): \. significa "un punto vero", non "un carattere qualsiasi". 3\.14 trova 3.14; senza \, il . significherebbe "carattere qualsiasi".

3.2 Il jolly e le ancore

Simbolo Significato Esempio
. un carattere qualsiasi (di solito tranne il newline) c.sa trova casa, cosa, c3sa
^ inizio della stringa (o della riga, con flag multiline) ^Ciao
$ fine della stringa (o della riga) fine$
\b confine di parola (tra \w e non-\w) \bgatto\b trova gatto ma non gatton
\B non confine di parola \Bgatto

^ e $ non "consumano" caratteri: sono àncore, indicano una posizione, non un carattere.

3.3 Classi di caratteri

Una classe [...] significa "uno dei caratteri elencati": [abc] è a oppure b oppure c; [a-z] è un intervallo (lettera minuscola); [A-Za-z0-9] è lettera o cifra; [^0-9] è la negazione, qualsiasi carattere tranne una cifra. Esistono classi predefinite:

Abbreviazione Equivale a Significato
\d [0-9] una cifra
\D [^0-9] non una cifra
\w [A-Za-z0-9_] carattere di "parola"
\W [^A-Za-z0-9_] non di parola
\s spazi, tab, newline… uno spazio bianco
\S   non spazio bianco

\d, \w, \s non esistono nei flavor POSIX (C, Bash base): lì si usano [0-9], [[:digit:]], [[:alpha:]], ecc. Vedi §6.

3.4 Quantificatori

Dicono quante volte ripetere l'elemento che li precede:

Quantificatore Significato Esempio
* zero o più ab* $\to$ a, ab, abbbb
+ uno o più ab+ $\to$ ab, abbbb (non a)
? zero o uno (opzionale) colou?r $\to$ color, colour
{n} esattamente n volte \d{4} $\to$ un anno a 4 cifre
{n,} almeno n volte \d{2,} $\to$ 2 o più cifre
{n,m} da n a m volte \d{2,4} $\to$ da 2 a 4 cifre

Greedy, lazy, possessive — questa distinzione crea molti bug. Di default i quantificatori sono golosi (greedy): prendono il più possibile. Il quantificatore lazy (.*?) prende il minimo possibile; il possessive (.*+) prende il massimo e non molla mai (utile per le prestazioni, vedi §4.5). Esempio su <a><b>:

Pattern Cosa trova Perché
<.*> (greedy) <a><b> (tutto!) .* divora fino all'ultimo >
<.*?> (lazy) <a> .*? si ferma al primo >

Nove volte su dieci, quando "la regex prende troppo", è perché è greedy e volevi lazy.

3.5 Alternanza e gruppi

L'alternanza ha priorità bassissima: ^gatto|cane$ significa (^gatto)|(cane$), non ^(gatto|cane)$. In caso di dubbio, usa i gruppi: ^(?:gatto|cane)$.

3.6 Backreference

Una backreference ti fa dire "di nuovo la stessa cosa che ho catturato prima". \1 = "il testo catturato dal gruppo 1". Esempio: (\w+) \1 trova una parola ripetuta, come ciao ciao.

Le backreference non esistono in RE2 (Go) né, di regola, in POSIX ERE: è una delle differenze più importanti tra i flavor.

3.7 Lookaround (asserzioni)

Sono condizioni che guardano intorno alla posizione corrente senza consumare caratteri: verificano che qualcosa ci sia (o non ci sia) prima/dopo, ma non lo includono nel match.

Sintassi Nome Significato
(?=...) look-ahead positivo seguito da
(?!...) look-ahead negativo non seguito da
(?<=...) look-behind positivo preceduto da
(?<!...) look-behind negativo non preceduto da

Esempio: \d+(?= €) trova le cifre in 50 € senza includere ` € nel match. (?<=$)\d+ trova le cifre in $50 senza includere il $`.

Il lookaround non c'è in RE2 (Go) e in POSIX. È tipico di PCRE/ECMAScript.

3.8 Flag (modificatori)

Cambiano il comportamento generale del match. La sintassi per attivarli varia (spesso un argomento a parte, oppure (?i) all'inizio del pattern):

Flag Nome Effetto
i ignore case ignora maiuscole/minuscole: casa trova anche CASA
g global trova tutte le occorrenze, non solo la prima
m multiline ^ e $ valgono a inizio/fine di ogni riga
s dotall / single line . trova anche il newline
x extended permette spazi e commenti nel pattern (più leggibile)

4. Come funziona davvero il matching (qui stanno i bug)

Conoscere la sintassi non basta: la maggior parte degli errori nasce da come il motore applica il pattern. Queste cinque cose spiegano il 90% dei "perché non funziona".

4.1 Greedy è il default

Come visto nel §3.4: se non specifichi, il motore prende il più possibile. Ricordalo ogni volta che un match "sconfina".

4.2 Match totale vs. ricerca parziale

Questa è una fonte di bug enorme perché cambia da linguaggio a linguaggio. Data la stringa "27/08/2026" e il pattern \d+: alcune funzioni chiedono che il pattern combaci con tutta la stringa (ancoraggio implicito), altre cercano una corrispondenza in un punto qualsiasi (match parziale).

Linguaggio "in un punto qualsiasi" "tutta la stringa"
Java matcher.find() matcher.matches()
JavaScript str.match(re) / re.test(str) ancora tu con ^…$
Go re.FindString re.MatchString con ^…$, oppure \A…\z
Python re.search re.fullmatch (re.match àncora solo l'inizio)

Se vuoi validare un formato "esatto", àncora sempre con ^…$ (o usa la funzione "match totale"), altrimenti \d+ trova le cifre dentro una stringa più lunga e credi di aver validato tutto.

4.3 Chi vince quando ci sono più match possibili? (POSIX vs Perl)

Se un pattern può combaciare in più modi, ci sono due filosofie: leftmost-longest (semantica POSIX), a parità di punto di partenza vince il match più lungo possibile — è un criterio globale; leftmost-first (semantica Perl/PCRE), il motore prova le alternative in ordine e si tiene la prima che funziona, guidato dal backtracking — non è detto sia la più lunga.

Esempio: pattern a|ab sul testo ab. Perl/PCRE prova a per prima, funziona $\to$ match = a. POSIX (leftmost-longest) sceglie il più lungo $\to$ match = ab.

La maggior parte dei linguaggi che userai è Perl-style; ma POSIX (C, grep, la libreria regex-tdfa di Haskell) segue l'altra regola. Se ottieni un match "più corto/lungo del previsto" tra due ambienti, quasi sempre è questa la ragione.

4.4 L'escaping doppio nelle stringhe

Trappola pratica indipendente dalla teoria: in molti linguaggi la regex si scrive dentro una stringa, e la stringa ha già il suo backslash come carattere speciale. Per ottenere la regex \d devi scrivere "\\d" nel sorgente.

Linguaggio Regex desiderata Nel sorgente scrivi
Java \d "\\d"
C \. "\\."
R \d "\\d"
JavaScript (literal) \d /\d/ (niente doppio escape nei literal /…/)
Go (raw string) \d `\d` (backtick = niente doppio escape)

Dove esistono, usa i raw string (`…` in Go, r"…" in Python/R…): eliminano il doppio escaping e rendono i pattern molto più leggibili.

4.5 Catastrophic backtracking e ReDoS

Alcuni pattern, su input costruiti ad arte, possono impiegare un tempo esponenziale. Succede con quantificatori annidati come (a+)+ che, cercando di far combaciare qualcosa che poi fallisce, provano un numero enorme di combinazioni prima di arrendersi. Ordini di grandezza sul testo "aaaa…aaa!" (nessuna corrispondenza finale):

\[\text{motore a backtracking (peggior caso): } O(2^{n}) \qquad\text{vs.}\qquad \text{automa (Thompson/RE2): } O(n \cdot m)\]

dove $n$ è la lunghezza del testo e $m$ quella del pattern. Con $n$ nell'ordine delle decine, $2^n$ diventa già impraticabile: il programma si "pianta". Quando un input dell'utente può scatenare questo comportamento si parla di ReDoS (Regular expression Denial of Service), una vulnerabilità di sicurezza reale.

Come difendersi: evita quantificatori annidati ((x+)+, (x*)*) e alternanze ambigue; àncora bene il pattern (^…$) per far fallire prima; usa quantificatori possessive o gruppi atomici (?>...) per impedire il backtracking; oppure usa un motore a tempo garantito lineare come RE2 (Go), che rinuncia a backreference e lookaround proprio per poter garantire $O(n)$.

5. Le operazioni che farai sempre

I nomi delle funzioni cambiano da linguaggio a linguaggio, il concetto no:

Operazione Cosa fa Esempio d'uso
test / match c'è una corrispondenza? (vero/falso) validare un formato
search / find trova la prima corrispondenza e dove localizzare un token
extract (gruppi) estrae le parti catturate (...) prendere giorno/mese/anno
find all / iterate scorre tutte le corrispondenze tutti i numeri in un testo
replace / substitute sostituisce (anche usando i gruppi) mascherare le email
split spezza la stringa sul pattern separare un CSV

Nel replace con i gruppi, nella stringa di sostituzione puoi riusare le parti catturate, di solito con $1, $2 (o \1, \2). Esempio: trasformare 2026-08-27 in 27/08/2026 con pattern (\d{4})-(\d{2})-(\d{2}) e sostituzione $3/$2/$1.

6. I flavor: perché la stessa regex non funziona ovunque

Le regex sembrano uguali dappertutto, ma sotto ci sono motori diversi con dialetti diversi. Conoscerli evita ore di frustrazione.

Morale: prima di copiare un pattern trovato online, chiediti in che flavor è scritto.

7. Lo stesso problema in ogni linguaggio (Rosetta)

Problema comune: dal testo "Oggi è il 27/08/2026." estrarre giorno, mese e anno. Pattern concettuale: (\d{2})/(\d{2})/(\d{4}) (con gruppi con nome dove il flavor lo permette).

C — POSIX ERE (<regex.h>)

#include <stdio.h>
#include <regex.h>

int main(void) {
    regex_t re;
    /* POSIX ERE: niente \d, si usa [0-9]. I gruppi () catturano. */
    const char *pattern = "([0-9]{2})/([0-9]{2})/([0-9]{4})";
    const char *text    = "Oggi e' il 27/08/2026.";

    /* REG_EXTENDED = sintassi ERE (senza, sarebbe BRE) */
    if (regcomp(&re, pattern, REG_EXTENDED) != 0) {
        fprintf(stderr, "Pattern non valido\n");
        return 1;
    }

    /* m[0] = match intero; m[1..3] = i tre gruppi.
       Ogni gruppo è un intervallo [rm_so, rm_eo) sul testo originale. */
    regmatch_t m[4];
    if (regexec(&re, text, 4, m, 0) == 0) {
        printf("giorno: %.*s\n", (int)(m[1].rm_eo - m[1].rm_so), text + m[1].rm_so);
        printf("mese:   %.*s\n", (int)(m[2].rm_eo - m[2].rm_so), text + m[2].rm_so);
        printf("anno:   %.*s\n", (int)(m[3].rm_eo - m[3].rm_so), text + m[3].rm_so);
    }

    regfree(&re);   /* sempre liberare la regex compilata */
    return 0;
}

Il ciclo tipico del mondo POSIX: compila (regcomp) $\to$ esegui (regexec) $\to$ libera (regfree). Si lavora con indici sul testo, non con sottostringhe pronte.

Bash — POSIX ERE ([[ =~ ]])

text="Oggi è il 27/08/2026."

# L'operatore =~ usa ERE. I gruppi finiscono nell'array BASH_REMATCH.
if [[ $text =~ ([0-9]{2})/([0-9]{2})/([0-9]{4}) ]]; then
    # BASH_REMATCH[0] = match intero; [1..3] = gruppi
    echo "giorno: ${BASH_REMATCH[1]}"
    echo "mese:   ${BASH_REMATCH[2]}"
    echo "anno:   ${BASH_REMATCH[3]}"
fi

# In alternativa, dalla riga di comando con gli strumenti classici:
#   grep -oE '[0-9]{2}/[0-9]{2}/[0-9]{4}'   -> ERE
#   grep -oP '\d{2}/\d{2}/\d{4}'            -> PCRE (GNU grep)
#   sed -E 's#.*([0-9]{2})/([0-9]{2})/([0-9]{4}).*#\1 \2 \3#'

Bash da solo mostra il caos dei flavor sulla stessa macchina: [[ =~ ]] e grep -E usano ERE, grep -P usa PCRE, sed un altro dialetto ancora.

Go — RE2 (regexp)

package main

import (
	"fmt"
	"regexp"
)

func main() {
	// (?P<nome>...) è la sintassi RE2 per i gruppi con nome.
	// Backtick = raw string: niente doppio escaping, \d resta \d.
	re := regexp.MustCompile(`(?P<day>\d{2})/(?P<month>\d{2})/(?P<year>\d{4})`)
	text := "Oggi è il 27/08/2026."

	m := re.FindStringSubmatch(text) // m[0]=intero, m[1..3]=gruppi in ordine
	if m != nil {
		fmt.Println("giorno:", m[1])
		fmt.Println("mese:  ", m[2])
		fmt.Println("anno:  ", m[3])
	}
}

RE2 garantisce tempo lineare: in cambio non offre backreference né lookaround. Ottimo quando il pattern gira su input non fidati (niente ReDoS).

Java — flavor PCRE-like (Pattern/Matcher)

import java.util.regex.*;

public class Data {
    public static void main(String[] args) {
        // Doppio escaping: nella stringa Java "\\d" corrisponde alla regex \d
        Pattern p = Pattern.compile("(?<day>\\d{2})/(?<month>\\d{2})/(?<year>\\d{4})");
        String text = "Oggi è il 27/08/2026.";

        Matcher m = p.matcher(text);
        // find() = corrispondenza in un punto qualsiasi (match parziale).
        // matches() invece pretenderebbe l'intera stringa.
        if (m.find()) {
            System.out.println("giorno: " + m.group("day"));
            System.out.println("mese:   " + m.group("month"));
            System.out.println("anno:   " + m.group("year"));
        }
    }
}

Due lezioni in uno snippet: il doppio escaping ("\\d") e la differenza find() (parziale) vs matches() (totale) del §4.2.

JavaScript — ECMAScript

// Sintassi literal /.../ : dentro NON serve il doppio escaping.
// Ma lo '/' va protetto con \/ perché delimita il literal.
const re = /(?<day>\d{2})\/(?<month>\d{2})\/(?<year>\d{4})/;
const text = "Oggi è il 27/08/2026.";

const m = text.match(re);
if (m) {
    // I gruppi con nome finiscono in m.groups
    console.log("giorno:", m.groups.day);
    console.log("mese:  ", m.groups.month);
    console.log("anno:  ", m.groups.year);
}

Unico rappresentante "puro" del flavor ECMAScript. Comodo l'oggetto m.groups per i gruppi con nome. Per tutte le occorrenze si userebbe str.matchAll(re) con il flag g.

R — base R (POSIX ERE e PCRE nello stesso ambiente)

text <- "Oggi è il 27/08/2026."

# regexec + regmatches estraggono i gruppi.
# perl = TRUE attiva PCRE (così \d funziona). Nota il doppio escaping "\\d".
m <- regmatches(text, regexec("(\\d{2})/(\\d{2})/(\\d{4})", text, perl = TRUE))[[1]]

# m[1] = match intero; m[2..4] = i tre gruppi
cat("giorno:", m[2], "\n")
cat("mese:  ", m[3], "\n")
cat("anno:  ", m[4], "\n")

# Senza perl = TRUE il default è ERE: niente \d, useresti "([0-9]{2})/...".
# Alternativa moderna con stringr (motore ICU):
#   library(stringr)
#   str_match(text, "(\\d{2})/(\\d{2})/(\\d{4})")

R mostra bene "stessa lingua, due flavor": ERE di default, PCRE con perl = TRUE, e in più l'ecosistema stringr/stringi con un terzo motore ancora (ICU).

Guile — SRFI-115 (regex come s-espressione)

(import (srfi 115))   ; SRFI-115: la regex è una struttura Scheme, non una stringa

(define rx-data
  (rx (=> day   (** 2 2 numeric))   ; gruppo "day": esattamente 2 cifre
      "/"
      (=> month (** 2 2 numeric))   ; (** n m sre) = da n a m ripetizioni
      "/"
      (=> year  (** 4 4 numeric)))) ; gruppo con nome via (=> nome sre)

(define m (regexp-search rx-data "Oggi è il 27/08/2026."))

(display (regexp-match-submatch m 'day))   (newline)   ; 27
(display (regexp-match-submatch m 'month)) (newline)   ; 08
(display (regexp-match-submatch m 'year))  (newline)   ; 2026

Approccio radicalmente diverso: niente stringa criptica da decifrare, il pattern è codice che puoi comporre, commentare e riutilizzare come qualsiasi altra struttura dati.

OCaml — libreria Re (approccio componibile)

(* La libreria Re costruisce il pattern combinando funzioni, non stringhe. *)
let re =
    let open Re in
    compile @@ seq [
        group (repn digit 2 (Some 2));   (* gruppo 1: esattamente 2 cifre *)
        char '/';
        group (repn digit 2 (Some 2));   (* gruppo 2 *)
        char '/';
        group (repn digit 4 (Some 4));   (* gruppo 3 *)
    ]

let () =
    let text = "Oggi è il 27/08/2026." in
    match Re.exec_opt re text with
    | Some g ->
        Printf.printf "giorno: %s\n" (Re.Group.get g 1);
        Printf.printf "mese:   %s\n" (Re.Group.get g 2);
        Printf.printf "anno:   %s\n" (Re.Group.get g 3)
    | None -> ()

Re (moderna, sicura, componibile) è preferibile al modulo standard Str, più basilare e non rientrante (stato globale condiviso $\to$ problemi con i thread).

Haskell — regex-tdfa, l'operatore =~ polimorfo

{-# LANGUAGE ScopedTypeVariables #-}
import Text.Regex.TDFA ((=~))

main :: IO ()
main = do
    let text    = "Oggi è il 27/08/2026." :: String
        -- TDFA usa POSIX ERE: niente \d, si usa [0-9]
        pattern = "([0-9]{2})/([0-9]{2})/([0-9]{4})" :: String

    -- Lo STESSO operatore (=~) restituisce cose diverse a seconda del TIPO che chiedi:
    let ok :: Bool
        ok = text =~ pattern                 -- vero/falso

    -- [[String]] = lista di match; ogni match = [intero, gruppo1, gruppo2, ...]
    let groups :: [[String]]
        groups = text =~ pattern

    print ok                                 -- True
    case groups of
        ((_whole : g) : _) -> do
            putStrLn ("giorno: " ++ g !! 0)
            putStrLn ("mese:   " ++ g !! 1)
            putStrLn ("anno:   " ++ g !! 2)
        _ -> putStrLn "nessun match"

Due unicità di Haskell: (1) =~ è polimorfo nel risultato — è il tipo che chiedi (Bool, Int, String, [[String]]…) a decidere cosa ottieni; (2) regex-tdfa implementa la semantica POSIX leftmost-longest del §4.3. Nota culturale: in Haskell idiomatico, per input strutturati si preferiscono spesso i parser combinator (es. megaparsec) alle regex.

8. Tabella comparativa dei flavor

Caratteristica POSIX BRE POSIX ERE PCRE / Perl ECMAScript (JS) RE2 (Go)
\d \w \s no (usa [[:digit:]]) no (usa [[:digit:]])
+ ? { } \| ( ) senza escape no (servono \+ ecc.)
Quantificatori lazy *? no no
Quantificatori possessive *+ no no no no
Gruppi con nome no no (?<n>) (?<n>) (?P<n>)
Backreference \1 no (standard) no
Lookahead (?=) (?!) no no no
Lookbehind (?<=) (?<!) no no sì (ES2018+) no
Semantica del match leftmost-longest leftmost-longest leftmost-first leftmost-first leftmost-first
Tempo garantito lineare dipende dipende no (backtracking) no (backtracking) sì, $O(n)$

Riepilogo per linguaggio:

Linguaggio Motore / flavor Nota chiave
C POSIX ERE (<regex.h>) compila $\to$ esegui $\to$ libera; lavori con indici
Bash POSIX ERE ([[ =~ ]]) ma grep -P/sed/awk cambiano flavor
Go RE2 tempo lineare, niente backref/lookaround
Java PCRE-like doppio escaping "\\d"; find() vs matches()
JavaScript ECMAScript literal /…/, m.groups, matchAll
R ERE (default) / PCRE (perl=TRUE) + stringr/stringi (ICU)
Guile SRFI-115 (SRE) pattern come s-espressione
OCaml libreria Re pattern componibile via funzioni
Haskell regex-tdfa (POSIX) =~ polimorfo; leftmost-longest

9. Checklist e trappole comuni

Prima di scrivere il pattern:

Mentre lo scrivo:

Prima di mandarlo in produzione:

Errori più frequenti in assoluto:

  1. Greedy dove serviva lazy $\to$ il match "sconfina".
  2. Dimenticare ^…$ $\to$ validi solo un pezzo della stringa.
  3. Usare \d/\w in POSIX (C, Bash) dove non esistono.
  4. Doppio escaping sbagliato $\to$ il pattern non è quello che credi.
  5. Provare a parsare HTML/JSON con le regex.

Esercizio risolto (in Bash): prendi il pattern data del §7 e riscrivilo per estrarre un'altra cosa, ad esempio una riga di log IP - - [data] "GET /path" 200 — costringe a usare gruppi, quantificatori e ancore tutti insieme. Bash è il linguaggio più snello per risolverlo: niente import, niente funzione, match ed estrazione dei gruppi in un solo if.

line='93.184.216.34 - - [27/Aug/2026:14:32:05 +0200] "GET /index.html HTTP/1.1" 200'

# ancore (§3.2): ^...$ pretende che il pattern copra l'intera riga
# quantificatori (§3.4): + per IP e path, {3} per lo status
# gruppi catturanti (§3.5): un gruppo per ciascun campo da estrarre
pattern='^([0-9.]+) - - \[([^]]+)\] "([A-Z]+) ([^ ]+)[^"]*" ([0-9]{3})$'

if [[ $line =~ $pattern ]]; then
    echo "ip:     ${BASH_REMATCH[1]}"
    echo "data:   ${BASH_REMATCH[2]}"
    echo "metodo: ${BASH_REMATCH[3]}"
    echo "path:   ${BASH_REMATCH[4]}"
    echo "status: ${BASH_REMATCH[5]}"
fi

Il pattern mette in pratica tutti e tre gli ingredienti richiesti: le ancore ^…$ (§3.2) obbligano il match a coprire l'intera riga, non un pezzo qualsiasi (senza, basterebbe un IP a caso in mezzo al testo — è lo stesso problema del §4.2); [0-9.]+ e [^ ]+ sono quantificatori (§3.4) su classi di caratteri; i cinque (...) sono gruppi catturanti (§3.5) che finiscono, in ordine, in BASH_REMATCH[1..5] — la stessa operazione di extract del §5.