Comandi Unix buffi: find

Per la serie "tuttologi non si nasce, ma si diventa"[1], eccomi di nuovo al pubblico di BETA nella nuova incarnazione di esperto Unix. D'accordo, questo ormai non è più un titolo che incute rispetto nel volgo - chissà, l'ultimo colpo deve essere stato dato quando in Jurassic Park il bimbo, con aria saccente, ha esclamato "Non c'è problema: è Unix!" - ma in ogni caso mi permette di continuare a scrivere qualcosa che dovrebbe risultare utile a chi non ha voglia di dannarsi a capire i manuali in inglese o spendere soldi per acquistare libri in italiano che occorre ritradurre in inglese per riuscire a comprenderne certe parti. In questi primi articoli, comincerò col parlare dei comandi Unix "buffi", come li chiamo io: quelli cioè che non seguono l'interfaccia standard di Unix comando -opz1 -opz2 -opzn < filein > fileout e che fanno imbestialire chiunque era convinto fino a quel momento di avere ormai compreso tutto, e che in realtà non c'è poi tutta quella differenza con Windows 95. Sono sicuro che ci sarà qualcuno che commenterà acidamente "Ma come! Non è umanamente possibile ricordarsi a memoria tutte queste opzioni e opzioncine, e vieni ancora a dirmi che questi comandi sono divertenti?". Bene, avete appena assistito a un tipico esempio di humour per un hacker. D'altra parte, c'è anche un lato positivo in tutta questa faccenda: generalmente non ci sono equivalenti DOS oppure Windows di questi comandi, e quindi non potete sfruttare la loro potenza, a meno di acquistarli separatamente. Qui vi vengono offerti di default: una volta imparati, non potrete farne a meno![2].

L'unico problema è che questi comandi hanno avuto troppo successo. Capita pertanto che non esistono probabilmente due sistemi operativi in cui le opzioni siano di questi comandi siano le stesse, dato che ogni produttore tende ad aggiungere le proprie opzioni[3]; io cercherò di limitarmi a quelle standard e a quelle delle versioni GNU, che sono probabilmente le più comuni grazie a Linux. Cominciamo quindi con find, il programma per cercare sui file.

Generalità

Tra i comandi Unix, ce ne sono alcuni che permettono all'utente di scendere ricorsivamente nelle sottodirectory e fare qualche azione particolare: gli esempi canonici sono ls -R e rm -R. Bene. find è il comando ricorsivo. Tutte le volte che si pensa "Beh, dovrei fare questo e questo su tutti i file di quel tipo sulla mia partizione", si dovrebbe in realtà pensare "Beh, devo usare find. Nonostante quello che il suo nome promette, il fatto che esso trovi dei file è un semplice effetto collaterale; il vero scopo di tale programma è il valutare delle espressioni e eseguire delle operazioni.

La struttura fondamentale del comando è la seguente:

find path[...] expression[...]

Questa è almeno la sintassi della versione GNU; altre versioni non permettono di specificare più di un path, e comunque non è che la cosa sia così necessaria. La spiegazione a grandi linee della sintassi del comando non è poi così difficile: si dice da dove si vuole iniziare la ricerca (la parte col path; tra l'altro nella versione GNU si può anche ometterla, e fare partire il programma dalla directory corrente .), e che tipo di ricerca si vuole fare (la parte expression).

Il comportamento standard del programma non è propriamente intuitivo, quindi è meglio spiegarlo immediatamente. Supponiamo infatti che nella directory in cui ci troviamo ci sia una directory chiamata test, con al suo interno il file pippo. Digitiamo allegramente find . -name pippo (che, come avrete certamente immaginato, cerca i file di nome pippo), e otteniamo... nient'altro che il prompt che indica che l'esecuzione è terminata. Cosa è successo? Il guaio è che find è per default un comando silenzioso: tutto quello che fa è ritornare 0 se la ricerca è stata completata (che si siano trovati o no dei file è una cosa secondaria - ve l'avevo detto che è un comando buffo!) oppure un valore diverso da zero nel caso ci siano stati dei problemi. Insomma: bisogna dirglielo, che noi vogliamo vedere qualcosa... Ciò a dire il vero non capita se si usa la versione GNU come capita su Linux, ma è meglio in ogni caso sapere queste idiosincrasie in modo da non rimanere preoccupati nel caso si passi a un altro tipo di Unix.

Le espressioni

Le espressioni possono essere divise in quattro gruppi distinti: le opzioni, i test, le azioni, e gli operatori. Ciascuno di essi può ritornare i valori true/false (vero o falso), e può anche avere un effetto collaterale. La differenza concettuale tra i vari gruppi è spiegata qui sotto:

  1. Le opzioni si applicano a tutto il comando find, piuttosto che al processamento di un singolo file. Un esempio è -follow, che fa sì che il programma segua i link simbolici invece che considerare semplicemente il file corrispondente all'inodo[4]. Essi ritornano sempre true.
  2. I test sono test veri e propri: ad esempio, -empty controlla se il file è vuoto. Essi possono ritornare true (vero) oppure false (falso).
  3. Le azioni sono chiamate così perché hanno anche un effetto collaterale: ad esempio, -print visualizza il nome del file su cui si sta operando. Anch'esse possono ritornare true oppure false.
  4. Gli operatori, infine, non ritornano di per sé un valore - si può dire convenzionalmente che essi diano true - e sono usati per costruire espressioni complesse. Un esempio è -or, che fa l'OR logico di due sottoespressioni. Si noti che, se si scrivono semplicemente le espressioni l'una di fianco all'altra, un -and è implicitamente aggiunto.

Si noti inoltre che find sfrutta la shell per parsare la linea di comando: ciò significa che tutte le parole chiave devono essere circondate da spazi o tab, e che occorre proteggere tanti bei simpatici caratteri, che altrimenti verrebbero mangiati dalla shell stessa. La protezione si può fare in tutti i modi standard, con un backslash oppure con le virgolette semplici e doppie: negli esempi qui sotto, userò tipicamente il backslash \, poiché è il modo più semplice5.

Opzioni

Ecco una lista di tutte le opzioni che la versione GNU di find ritorna. Ricordatevi che esse ritornano sempre true.

-daystart
fa sì che "ieri" non venga definito come 24 ore fa, quanto piuttosto dalla mezzanotte di ieri. Un vero hacker non può probabilmente comprendere l'utilità di un'opzione simile, ma un programmatore che lavori dalle 8 alle 15 l'apprezza molto.
-depth
processa il contenuto di una directory prima dell'inodo corrispondente alla directory stessa. A dire il vero, non è che conosca tanti usi di questa opzione, a parte per emulare il comando rm -R (ovviamente non si può cancellare una directory senza avere prima cancellato tutti i file al suo interno...)
-follow
derefenzia (cioè segue) i link simbolici. Implica l'opzione -noleaf: vedi sotto.
-noleaf
non fa usare l'ottimizzazione che dice "una directory contiene esattamente due sottodirectory meno del suo numero di hard link". Se vivessimo nel migliore dei mondi, tutte le directory avrebbero un riferimento col proprio nome nella directory padre, uno come . all'interno di se stesse (ecco il valore di due indicato sopra) e uno come .. in tutte le directory figlie. Ma ci sono due eccezioni: un filesystem non Unix montato via NFS, e i link simbolici. La vita, a volte, è difficile...
-maxdepth livelli, -mindepth livelli
, dove livelli è un intero non negativo, dicono rispettivamente che al più o almeno un numero di livelli di directory pari a livelli deve venire cercato. Un paio di esempi sono d'obbligo, in questo caso: -maxdepth 0 significa che il comando deve essere eseguito solamente sui file delle directory indicate nella riga di comando, senza cioè discendere ricorsivamente; -mindepth 1 fa invece l'esatto opposto, non considerando i file nelle directory indicate nella linea di comando, ma solo quelli delle sottodirectory.
-version
scrive semplicemente la versione del programma che si sta usando.
-xdev
, nome piuttosto fuorviante, dice a find di non cambiare filesystem (cross device, in inglese). Ciò è molto utile quando occorre cercare qualcosa che si sa essere nel file system di root: essa è infatti generalmente una piccola partizione, ma per default find / esaminerebbe tutti i file!

Test

I primi due test sono molto semplici da comprendere: -false ritorna sempre falso, mentre -true ritorna sempre vero.

Altri test che non richiedono di specificare un valore sono -empty, che ritorna vero quando il file esiste ma è vuoto (di dimensione zero), e la coppia -nouser / -nogroup, che ritornano vero nel caso che non ci sia un nome associato all'utente/gruppo in /etc/passwd o rispettivamente /etc/group. Questo capita abbastanza spesso in un sistema multiutente; si cancella un utente e tutti i file nella sua home directory, ma va sempre a finire che rimangono dei file suoi in qualche remoto filesystem. E Murphy dice che questi file occupano molto spazio[6].

Naturalmente, si può anche cercare un utente o gruppo specifico. I test corrispondenti sono -uid nn e -gid nn. Sfortunatamente, però, non si può scrivere il nome dell'utente o del gruppo, ma bisogna per forza utilizzare l'identificativo numerico.

NOTA: In questa sezione, tutte le volte che scrivo il parametro nn si possono usare anche la forma +nn, vale a dire "un valore strettamente maggiore di nn", e -nn, "un valore strettamente minore di nn". Non so esattamente quale sia l'utilità pratica nel caso delle UID, ma con altri test la cosa risulta molto utile.

Un'altra utile opzione è -type c, che - l'avrete intuito - ritorna vero se il file considerato è di tipo c. I caratteri mnemonici per le scelte possibili sono gli stessi che si trovano nell'output di ls; quindi abbiamo b quando il file è un device a blocchi; c quando il file è un device a caratteri; d per le directory; p per le named pipes; l per i link simbolici, e s per i socket. I file regolari sono infine indicati con f.
Un test correlato a questo è -xtype, che è simile a -type tranne che nel caso che si abbia un link simbolico e sia anche data l'opzione -follow; in questo caso, si controlla il file puntato dal link, e non il link stesso.
Completamente scorrelato è invece il test -fstype type. In questo caso, si testa il tipo del filesystem. Presumo che l'informazione sia presa dal file /etc/mtab; sicuramente su Linux i tipi nfs, tmp, msdos ed ext2 sono riconosciuti.

I test -inum nn e -links nn controllano rispettivamente se il file ha numero di inodo nn o nn link, mentre -size nn è vero se il file ha allocati nn blocchi da 512 byte[7]. Visto che il blocco di 512 è una reliquia del passato (Linux ad esempio fa i conti in kilobyte), si può appendere a nn il carattere b, per dire di contare in byte, o k, per contare in kilobyte.

I bit di permessi sono controllabili per mezzo del test -perm mode. Se mode non è preceduto da alcun segno, allora i bit del permesso del file devono coincidere con quelli di mode. Se esso è preceduto da -, occorre che siano settati tutti (ma non necessariamente solo) i bit indicati; + serve per indicare che almeno uno dei bit deve essere settato. Oops... ho dimenticato di dire che il modo è scritto in ottale oppure simbolicamente, come in chmod.

I bit di permessi sono controllabili per mezzo del test -perm mode. Se mode non è preceduto da alcun segno, allora i bit del permesso del file devono coincidere con quelli di mode. Se esso è preceduto da -, occorre che siano settati tutti (ma non necessariamente solo) i bit indicati; + serve per indicare che almeno uno dei bit deve essere settato. Oops... ho dimenticato di dire che il modo è scritto in ottale oppure simbolicamente, come in chmod.

l gruppo seguente di test è relativo al tempo[8] di creazione o utilizzo di un file, il che torna comodo quando ci si trova il disco pieno, e occorre fare pulizia della roba che non si usa da una vita. Il problema è ovviamente trovarla, questa roba... e allora, via di find!
-atime nn è vero se il file è stato acceduto l'ultima volta nn giorni fa, -ctime nn se lo status del file è stato modificato l'ultima volta nn giorni fa - per esempio, con un chmod - e -mtime nn se il file è stato modificato nn giorni fa.
Alle volte occorre calcolare il tempo in maniera più precisa: il test -newer file ritorna vero se il file considerato è stato modificato dopo quello di riferimento file: basta così creare con touch un file con la data desiderata, e si è a cavallo.
Il GNU find aggiunge i test -anewer e -cnewer che si comportano in maniera simile, e i test -amin, -cmin che -mmin calcolano il tempo in minuti, invece che in periodi di 24 ore.

Ecco infine il test che si usa più spesso. -name pattern ritorna vero se il nome del file corrisponde esattamente a pattern, Però la cosa non è così semplice, visto che la shell espande i metacaratteri. Pertanto, se si vogliono trovare i file il cui nome inizia con pippo, non si può scrivere -name pippo* ma -name pippo\* oppure -name "pippo*". Questo è uno degli errori più comuni che si fanno; state bene attenti!
Un altro problema sta nel fatto che, come con ls, i file che iniziano con un punto non sono riconosciuti. Per ovviare a questo, si può usare il test -path pattern che non si cura di punti e barre quando confronta il path del file considerato rispetto a pattern.

Azioni

Ho scritto che le azioni sono quelle che fanno effettivamente qualcosa. Beh, -prune piuttosto non fa qualcosa, per la precisione discendere nell'albero delle directory (a meno che non sia dato anche -depth. Viene generalmente usato assieme a -fstype, per scegliere tra i vari filesystem quali si vogliono controllare.

Le altre azioni si possono dividere in due grandi categorie:

  1. Azioni che stampano qualcosa. La più ovvia di queste - e di fatto quella di default per la versione GNI - è -print, che scrive semplicemente il nome del (dei) file che soddisfano alle altre condizioni elencate nella linea di comando, oltre a ritornare true. Una semplice variante di -print è -fprint file, che scrive su file invece dello standard output. -ls lista i file nello stesso formato di ls -dils; -printf format si comporta più o meno come la funzione C printf(), lasciando così la possibilità di indicare come l'output debba essere formattato, e -fprintf file format fa lo stesso, sul file di output file. Anche queste azioni ritornano vero.
  2. Azioni che eseguono qualcosa. La loro sintassi è un po' strana, ma visto che si usano spesso è meglio studiarle attentamente.

Operatori

Ci sono parecchi operatori: eccone una lista, in ordine decrescente di precendenza.

\( expr \)
forza l'ordine di precedenza. Le parentesi devono naturalmente essere quotate, visto che hanno un significato particolare per la shell.
! expr
-not expr
cambiano il valore di verità dell'espressione, vale a dire che se expr era vera, diventa falsa. Il punto esclamativo non ha bisogno di essere protetto dalla shell, perché è seguito da uno spazio.
expr1 expr2
expr1 -a expr2
expr1 -and expr2
corrispondono tutti all'AND logico, implicito nel primo caso. Se expr1 ritorna falso, expr2 non viene valutata.
expr1 -o expr2
expr1 -or expr2
corrispondono all'OR logico. Se expr1 è vera, expr2 non viene valutata.
expr1 , expr2
è la lista: vengono valutate sia expr1 che expr2 (con tutti gli effetti collaterali, naturalmente!) e il valore finale dell'espressione è quello di expr2.

Esempi

Sì, find ha troppe opzioni da ricordare, lo so. Ma ci sono molti casi usati spesso che conviene tenere a memoria. Vediamone alcuni.

% find . -name pippo\* -print

cerca tutti i nomi che iniziano con pippo. Se la stringa da cercare fosse contenuta nel nome, sarebbe forse meglio scrivere "*pippo*" invece di \*pippo\*.

% find /usr/include -xtype f -exec grep pippo /dev/null {} \;

è un grep eseguito ricorsivamente a partire dalla directory /usr/include. In questo caso, ci interessano sia i file regolari che i link simbolici che puntano a file regolari, perciò usiamo il test -xtype. Molte volte, soprattutto se siamo certi che non ci sono occorrenze della stringa nei file binari, esso si può omettere. E a che serve il /dev/null nel comando? È un trucco per forzare grep a scrivere il nome del file quando trova la stringa. Infatti il comando viene applicato sempre a un file per volta, e quindi grep non pensa sia necessario indicare il nome del file. Ma adesso di file ce ne sono due, quello considerato e /dev/null!

% find / -atime +1 -fstype ext2 -name core -exec rm {} \;

è un classico lavoro per crontab. Il comando cancella tutti i file di nome core in filesystems di tipo ext2 che non sono stati acceduti nelle ultime 24 ore, e che quindi si è ragionevolmente certi che ormai sono incomprensibili...

% find /home -xdev -size +500k -ls > piggies

serve a trovare i file che hanno appena riempito il filesystem. Notate l'uso di -xdev, visto che non dobbiamo per il momento svuotare altri filesystem.

Avviso ai ricercanti

Tenete in mente che find è un comando che consuma molta CPU, visto che deve accedere a ogni inodo del sistema per operare. È perciò opportuno combinare il più possibile le operazioni da fare in una singola chiamata a find; questo lo si può fare facilmente nelle operazioni automatiche di "pulizia di casa", lanciate via crontab. Un esempio: supponiamo di volere cancellare i file che finiscono in .BAK, cambiare la protezione di tutte le directory a 771 e quella dei file che finiscono in .sh a 755. E magari si stanno montando filesystem NFS su una linea lenta, e non si vuole controllare anche questi. Perché scrivere tre comandi differenti? Molto meglio dire

% find . \( -fstype nfs   -prune \) -o \
         \( -type d       -exec chmod 771 {} \; \) -o \
         \( -name "*.BAK" -exec /bin/rm {}   \; \) -o \
         \( -name "*.sh"  -exec chmod 755 {} \; \) 

che sembra orrendo (e abusante di backslash!), ma in realtà è di lettura immediata. Ricordate sempre che quello che si fa dal punto di vista di find è una valutazione vero/falso; il resto è un effetto collaterale. Quindi in ogni azione tra parentesi, la parte di destra è eseguita solo se quella di sinistra corrisponde al vero, e in questo caso non si considerano le sottoespressioni successive. La cosa è più facile a farsi che a dirsi: dopo un po' di abitudine[9] il tutto sembrerà naturale.

Note & Chiose

[1]
E io modestamente lo divenni...
[2]
A meno ovviamente che non spegniate il calcolatore, cosa che d'altro canto conviene fare piuttosto spesso, ricordandosi che ci sono anche altre cose nella vita.
[3]
Secondo voi, chi è stato a inventare gli opzional?
[4]
L'inodo è la parte del file che contiene le informazioni sul file stesso (come la sua lunghezza), e non il suo contenuto. Visto che ogni tanto ci sono anche definizioni utili?
[5]
Almeno a mio parere. Ma visto che sono io a scrivere questo articolo...
[6]
Beh, più che Murphy qui può la logica. Se un file è grande, riempie la propria directory, quindi si cerca di sbolognarlo altrove!
[7]
Non proprio precisamente - ai file con blocchi sparsi vengono contati anche i "buchi". Non sapete cosa sono i file con blocchi sparsi? Male.
[8]
Ormai lo sanno tutti che il tempo è relativo!
[9]
E si spera non troppi backup da riesumare :-)
.mau.
Copyright © 1996 Maurizio Codogno e BETA. Questo testo può essere liberamente distribuito purché non a fini di lucro. Per ogni altro utilizzo, si prega contattare l'autore.