$ grep — corso
Lezione 05 di 10

Ricerca ricorsiva nei progetti

Con un solo flag, -r, grep diventa un motore di ricerca a tutti gli effetti sopra un albero di file. È il modo per usarlo dentro repository di codice, cartelle di log, archivi di documenti — purché si sappia escludere il rumore.

Teoria

-r, -R, e i filtri di percorso

Con -r grep scende ricorsivamente nelle sottocartelle. Se non gli passi un percorso, parte da . (la directory corrente). C'è anche -R: differisce da -r solo per il fatto che segue i link simbolici. Nel dubbio si usa -r: è più sicuro perché non rischia di entrare in loop su link circolari.

I flag che fanno la differenza nell'uso reale sono i filtri di percorso:

flagcosa fa
--include=GLOBcerca solo nei file il cui nome matcha il glob
--exclude=GLOBsalta i file il cui nome matcha il glob
--exclude-dir=NAMEsalta intere directory che si chiamano così

Senza queste opzioni una ricerca ricorsiva in un progetto JavaScript apre node_modules, in un progetto Python apre __pycache__, in un repository .git e ti restituisce un fiume di rumore in cui non trovi nulla. Queste opzioni sono la chiave di una grep ricorsiva utilizzabile.

Su macOS BSD grep: -r e --include sono supportati, ma --exclude-dir richiede una versione recente. Se non funziona, installa ggrep (visto nella lezione 1).
Pratica

Cercare dentro un progetto

Immaginiamo un classico progetto web. Vogliamo trovare in quale file è definita la funzione calculatePrice:

cercare una funzione
$ grep -rn "calculatePrice" .
./src/checkout.js:42:function calculatePrice(items) {
./src/checkout.js:78:  total = calculatePrice(cart);
./node_modules/lib-bigone/src/util.js:1234:  // see calculatePrice
./node_modules/lib-bigone/test/cart.test.js:88:  it("calculatePrice")
... centinaia di altre righe dentro node_modules ...

Disastro: tutto ciò che ci interessa è annegato. Aggiungiamo le esclusioni:

ricerca pulita
$ grep -rn "calculatePrice" . \
    --include="*.js" \
    --exclude-dir=node_modules \
    --exclude-dir=.git \
    --exclude-dir=dist
./src/checkout.js:42:function calculatePrice(items) {
./src/checkout.js:78:  total = calculatePrice(cart);
./src/api/orders.js:14:const price = calculatePrice(order.items);

Tre risultati, tutti pertinenti. --exclude-dir può essere ripetuto tante volte quante servono. --include accetta i normali wildcard della shell (*.js, *.{js,ts} non sempre — meglio ripetere --include due volte).

Caso opposto: cerca un testo dentro qualsiasi tipo di file ma evita le cartelle di build e i file binari (grep lo fa di default, mostrando Binary file ... matches):

solo file di testo, esclusi i binari
$ grep -rnI "API_KEY" . --exclude-dir={.git,node_modules,dist,build}
./.env.example:3:API_KEY=your-key-here
./README.md:42:Setta la variabile API_KEY prima di partire.
./src/config.js:8:const key = process.env.API_KEY;

Il flag -I (i maiuscola) salta i file binari. L'espansione {a,b,c} è una comodità della shell che permette di scrivere --exclude-dir una sola volta.

Trucco

Crea il tuo alias grepcode e non pensarci più

Le esclusioni sono sempre le stesse. Invece di scriverle ogni volta, mettiamole una volta sola in una funzione di shell e dimentichiamocele:

~/.bashrc o ~/.zshrc
# grep nel codice, saltando build/cache/git/vendor
grepcode() {
  grep -rnI "$@" . \
    --exclude-dir=.git \
    --exclude-dir=node_modules \
    --exclude-dir=.next \
    --exclude-dir=dist \
    --exclude-dir=build \
    --exclude-dir=vendor \
    --exclude-dir=__pycache__ \
    --exclude-dir=.venv \
    --exclude="*.min.js" \
    --exclude="*.lock"
}

Ora grepcode "calculatePrice" fa il giusto, e tutti gli argomenti di grep continuano a funzionare: grepcode -i "todo", grepcode -E "id|uuid", ecc. È una funzione e non un alias proprio perché vogliamo passare argomenti arbitrari.

Variante grepfn: per cercare una definizione di funzione invece che ogni occorrenza, basta cambiare il pattern in arrivo: grepfn() { grepcode -E "function $1|def $1|const $1\s*=|$1\s*\("; }. Lo personalizzi col tuo linguaggio.

Combinato con -l (visto nella lezione 2) diventa il modo più rapido per aprire nell'editor tutti i file che contengono qualcosa:

apri tutti i file che contengono TODO
$ vim $(grepcode -l "TODO")
# oppure con VS Code
$ code $(grepcode -l "TODO")