$ grep — corso
Lezione 08 di 10

PCRE, il superpotere di -P

Con -P grep parla la lingua delle regex di Perl: lookahead, lookbehind, non-greedy, classi abbreviate. È il livello che permette di estrarre testo con precisione chirurgica — quando ti serve, non c'è sostituto.

Teoria

Cosa aggiunge PCRE

PCRE sta per Perl Compatible Regular Expressions: la sintassi di regex usata in Perl, in PHP, in Python (con piccole differenze) e in molti altri linguaggi. Rispetto a ERE aggiunge cose che non puoi fare altrimenti:

simbolosignificato
\d \w \scifra, carattere "word", spazio (e le maiuscole sono i complementari)
\bconfine di parola (come \< e \> insieme)
*? +? ??versione non-greedy: matcha il meno possibile
(?=...)lookahead positivo: "seguito da..."
(?!...)lookahead negativo: "non seguito da..."
(?<=...)lookbehind positivo: "preceduto da..."
(?<!...)lookbehind negativo: "non preceduto da..."
Importante — macOS: il grep di sistema su macOS (BSD) non supporta -P. Se ci provi, ti risponde "grep: -P not supported". La soluzione è quella vista nella lezione 1: installa GNU grep con brew install grep e usa ggrep -P. Su Linux è già disponibile.
Pratica

Estrarre con lookahead e lookbehind

La cosa più potente di PCRE è la coppia lookahead/lookbehind. Permettono di richiedere che attorno al match ci sia qualcosa senza includerlo nel match stesso. Esempio canonico: estrarre il valore di una variabile ignorando il prefisso chiave=.

estrarre valori da un .env
$ cat > .env <<'EOF'
DB_HOST=localhost
DB_PORT=5432
DB_USER=admin
API_KEY=sk-1234-abcd-5678
EOF

# solo il valore di API_KEY, senza il prefisso
$ grep -oP '(?<=^API_KEY=).*' .env
sk-1234-abcd-5678

# con ERE avresti dovuto fare due passaggi (grep + cut o sed):
$ grep '^API_KEY=' .env | cut -d= -f2

La sintassi (?<=^API_KEY=).* dice: "matcha .*, ma solo se preceduto da ^API_KEY=". Il prefisso resta fuori dal match. Combinato con -o, abbiamo direttamente il valore puro.

Le classi abbreviate \d, \w, \s rendono i pattern molto più leggibili rispetto alle equivalenti POSIX:

classi abbreviate
# trovare numeri di telefono italiani (10 cifre, eventuali spazi)
$ grep -oP '\b\d{3}[\s-]?\d{3}[\s-]?\d{4}\b' rubrica.txt
340 123 4567
02-1234567
3331234567

# non-greedy: contenuto tra parentesi quadre, ma solo il più corto
$ echo "[INFO] [main] avvio" | grep -oP '\[.*?\]'
[INFO]
[main]

# greedy avrebbe restituito tutta [INFO] [main]

Il lookahead (?=...) è altrettanto utile per filtri condizionali:

righe che contengono ERROR ma non timeout
$ grep -P '^(?=.*ERROR)(?!.*timeout).*$' app.log
10:00:09 [ERROR] utente=bob    db locked
10:01:12 [ERROR] utente=alice  permission denied

I due lookahead nello stesso pattern fanno un "AND" tra condizioni indipendenti sulla stessa riga: contiene ERROR AND NON contiene timeout. Senza PCRE servirebbero due grep concatenati: grep ERROR | grep -v timeout.

Trucco

Estrarre stringhe tra delimitatori senza i delimitatori

Il pattern più riusabile di tutto PCRE in grep è probabilmente questo: prendere ciò che sta tra due delimitatori, ma senza i delimitatori stessi. Vale per virgolette, parentesi, tag HTML, marker custom… ed è una richiesta che torna di continuo.

estrarre stringhe tra virgolette
$ cat > codice.py <<'EOF'
nome = "Alice"
saluto = "Ciao mondo"
print(f"Benvenuta {nome}")
EOF

# tutto ciò che sta tra "..." (senza le virgolette)
$ grep -oP '(?<=")[^"]+(?=")' codice.py
Alice
Ciao mondo
Benvenuta 

Letto a voce: "qualcosa che non sia una virgoletta, ripetuto una o più volte, preceduto e seguito da una virgoletta". Lo stesso schema funziona per qualunque coppia di delimitatori: cambia solo cosa ci metti dentro.

altri delimitatori, stesso schema
# dentro [parentesi quadre]
$ echo "[INFO] avvio [bob] sessione [123]" | grep -oP '(?<=\[)[^\]]+(?=\])'
INFO
bob
123

# dentro un tag <title>
$ echo '<title>La mia pagina</title>' | grep -oP '(?<=<title>).*?(?=</title>)'
La mia pagina

# dentro backtick di markdown
$ grep -oP '(?<=`)[^`]+(?=`)' README.md
Promemoria operativo: ogni volta che ti viene voglia di "estrarre solo X, senza la cornice che lo identifica", il pattern di partenza è (?<=APERTURA)CONTENUTO(?=CHIUSURA) con grep -oP. Salvalo da qualche parte: lo userai più volte di quanto credi.

Quando i delimitatori sono multi-carattere o le strutture annidate, PCRE inizia a mostrare i suoi limiti — lì il giusto strumento diventa sed, oppure un parser vero. Ma per il 90% delle estrazioni quotidiane il lookaround di grep è più che sufficiente.