Pipe e composizione
Grep dà il meglio di sé quando non legge un file: quando sta in mezzo a una pipe, a filtrare l'output di un altro comando. È in questa posizione che passa da utility a tassello del modo di pensare Unix.
La filosofia: piccoli comandi, in serie
Il principio dietro alla shell Unix è che ogni comando faccia una cosa,
e che il modo di combinarli sia mettere l'output dell'uno in input all'altro
attraverso il simbolo |. In questa visione, grep è il filtro per
eccellenza: legge righe da standard input, ne lascia passare alcune, scarta le
altre.
Quando grep non riceve un file come argomento, si mette automaticamente in ascolto dello standard input. Da lì si crea il pattern che si ripete in mille varianti:
COMANDO | grep PATTERN | ALTRO_COMANDO
Da questa idea derivano usi che vedremo qui sotto: filtrare processi in esecuzione, monitorare log in tempo reale, cercare nei commit di git, contare occorrenze, eccetera. Sono casi diversi in superficie, ma sotto è sempre lo stesso schema.
Sette pipe che userai ovunque
1. Trovare un processo per nome:
$ ps aux | grep nginx root 1234 0.0 0.1 nginx: master process www 1235 0.0 0.1 nginx: worker process tu 9999 0.0 0.0 grep --color=auto nginx
Nota la riga finale: grep ha trovato anche se stesso. Il fix è il trucco di questa lezione.
2. Filtrare l'output di ls:
$ ls -la | grep '^d' # solo directory $ ls -la /etc | grep '\.conf$' # file .conf in /etc
3. Cercare in git log:
$ git log --oneline | grep -i 'fix' a1b2c3d fix: corretto bug login e4f5g6h Fix: typo nel readme # commit di un certo autore che riguardano una zona $ git log --author=Anna --pretty=%s | grep -i checkout
4. Monitorare un log in tempo reale:
$ tail -f /var/log/syslog | grep --line-buffered -i error
--line-buffered è importante: senza, grep accumula l'output in un
buffer e tu non vedi le righe in tempo reale. Con, ogni riga matchata appare
appena viene letta.
5. Trovare e operare sui file (con find):
$ find . -name '*.py' -print0 | xargs -0 grep -l 'import requests'
6. Contare in modo composto:
$ grep '"GET ' access.log | awk '{print $1}' | sort -u | wc -l 347
7. Pattern multipli da file con -f:
$ cat > ip-da-bannare.txt <<'EOF' 192.0.2.5 198.51.100.12 203.0.113.8 EOF # cerca nel log tutte le righe che contengono uno qualunque di quegli IP $ grep -f ip-da-bannare.txt access.log
-f è oro: invece di mettere decine di alternative |
nella riga di comando, tieni i pattern in un file e li passi così.
Combinato con -F (stringhe letterali, niente regex) diventa anche
molto veloce su file enormi.
Il classico [n]ginx: evitare di trovare se stesso
Avevamo visto nella lezione 2 il problema: quando filtri l'output di
ps per trovare un processo, l'ultima riga del risultato è
il grep stesso, che ovviamente contiene la stringa che cercavi.
Le soluzioni che girano sono tre, in ordine crescente di eleganza:
Soluzione mediocre: aggiungere grep -v grep in coda.
$ ps aux | grep nginx | grep -v grep root 1234 0.0 0.1 nginx: master process www 1235 0.0 0.1 nginx: worker process
Soluzione elegante: il pattern [n]ginx.
$ ps aux | grep '[n]ginx' root 1234 0.0 0.1 nginx: master process www 1235 0.0 0.1 nginx: worker process
Il pattern [n]ginx matcha esattamente la stringa nginx
(perché [n] è una classe di un solo carattere: la lettera n). Ma la
riga del comando grep stesso, vista da ps, contiene letteralmente la
stringa [n]ginx — che non è uguale a nginx. Il
grep, cercando nginx, non trova se stesso. È un piccolo capolavoro
di logica.
Soluzione moderna: usa pgrep e basta.
$ pgrep -af nginx 1234 nginx: master process 1235 nginx: worker process
pgrep se è
disponibile (lo è su tutti i sistemi moderni), perché è chiaro e non ha il
problema. Ma conoscere il trucco [n]ginx è obbligatorio: lo trovi
negli script di tutto il mondo, e ti capiterà di doverlo leggere, mantenere, o
spiegare a qualcun altro.