per vs trovare in Bash


27

Quando si esegue il ciclo tra i file ci sono due modi:

  1. usa un for-loop:

    for f in *; do
        echo "$f"
    done
  2. usare find:

    find * -prune | while read f; do 
        echo "$f"
    done

Supponendo che questi due loop troveranno lo stesso elenco di file, quali sono le differenze tra queste due opzioni in termini di prestazioni e gestione?


1
Perché? findnon apre i file che trova. L'unica cosa che posso vedere ti morde qui rispetto a un gran numero di file è ARG_MAX .
Kojiro,

1
Vedi le risposte e i commenti che ti dicono che read fmangeranno i nomi dei file mentre li legge (ad esempio nomi con spazi vuoti iniziali). find * -pruneSembra anche essere un modo molto contorto per dire semplicemente ls -1sì?
Ian D. Allen,

4
Non dare per scontato che i due loop troveranno lo stesso set di file; nella maggior parte dei casi, non lo faranno. Inoltre, quello dovrebbe essere find ., no find *.
alexis

1
@terdon Sì, l'analisi ls -lè una cattiva idea. Ma l'analisi ls -1(che 1non è un l) non è peggio dell'analisi find * -prune. Entrambi falliscono nei file con newline nei nomi.
Ian D. Allen,

5
Sospetto che ognuno di noi abbia trascorso più tempo a leggere questa domanda e le risposte rispetto alla differenza totale nelle prestazioni durante la vita della sceneggiatura in questione.
mpez0

Risposte:


9

1.

Il primo:

for f in *; do
  echo "$f"
done

non riesce per file chiamati -n, -ee varianti come -nenee con alcune implementazioni bash, con i nomi dei file che contiene barre rovesciate.

Il secondo:

find * -prune | while read f; do 
  echo "$f"
done

fallisce anche per più casi (file chiamati !, -H, -name, (, i nomi dei file che iniziare o terminare con spazi vuoti o contenere caratteri di nuova riga ...)

È la shell che si espande *, findnon fa altro che stampare i file che riceve come argomenti. Avresti potuto anche usare printf '%s\n'invece il quale printfincorporato che eviterebbe anche il potenziale errore di troppi argomenti .

2.

L'espansione di *è ordinata, puoi renderla un po 'più veloce se non hai bisogno dell'ordinamento. In zsh:

for f (*(oN)) printf '%s\n' $f

o semplicemente:

printf '%s\n' *(oN)

bashnon ha equivalenti per quanto posso dire, quindi dovresti ricorrere a find.

3.

find . ! -name . -prune ! -name '.*' -print0 |
  while IFS= read -rd '' f; do
    printf '%s\n' "$f"
  done

(sopra usando -print0un'estensione non standard GNU / BSD ).

Ciò comporta comunque la generazione di un comando find e l'uso di un while readciclo lento , quindi sarà probabilmente più lento rispetto all'utilizzo del forciclo a meno che l'elenco dei file non sia enorme.

4.

Inoltre, contrariamente all'espansione dei caratteri jolly della shell, findeseguirà una lstatchiamata di sistema su ciascun file, quindi è improbabile che il non ordinamento lo compensi.

Con GNU / BSD find, questo può essere evitato usando la loro -maxdepthestensione che attiverà un'ottimizzazione salvando lstat:

find . -maxdepth 1 ! -name '.*' -print0 |
  while IFS= read -rd '' f; do
    printf '%s\n' "$f"
  done

Perché findinizia a produrre i nomi dei file non appena li trova (ad eccezione del buffering di output stdio), dove potrebbe essere più veloce se ciò che fai nel ciclo richiede molto tempo e l'elenco dei nomi dei file è più di un buffer stdio (4 / 8 kB). In tal caso, l'elaborazione all'interno del ciclo inizierà prima che findabbia terminato di trovare tutti i file. Sui sistemi GNU e FreeBSD, puoi usare stdbufper far sì che ciò accada prima (disabilitando il buffering stdio).

5.

Il modo POSIX / standard / portatile per eseguire comandi per ciascun file findè utilizzare il -execpredicato:

find . ! -name . -prune ! -name '.*' -exec some-cmd {} ';'

Nel caso di echociò, è meno efficiente che eseguire il looping nella shell poiché la shell avrà una versione integrata di echomentre finddovrà generare un nuovo processo ed eseguirlo /bin/echoper ogni file.

Se è necessario eseguire diversi comandi, è possibile eseguire:

find . ! -name . -prune ! -name '.*' -exec cmd1 {} ';' -exec cmd2 {} ';'

Ma attenzione che cmd2viene eseguito solo se ha cmd1esito positivo.

6.

Un modo canonico per eseguire comandi complessi per ogni file è chiamare una shell con -exec ... {} +:

find . ! -name . -prune ! -name '.*' -exec sh -c '
  for f do
    cmd1 "$f"
    cmd2 "$f"
  done' sh {} +

Quella volta, siamo tornati ad essere efficienti echodal momento che stiamo usando shquello incorporato e la -exec +versione viene generata il meno shpossibile.

7.

Nei miei test su una directory con 200.000 file con nomi brevi su ext4, zshquello (paragrafo 2.) è di gran lunga il più veloce, seguito dal primo semplice for i in *ciclo (anche se come al solito, bashè molto più lento di altre shell per quello).


cosa fa il !comando find?
rubo77,

@ rubo77, !è per negazione. ! -name . -prune more...farà -prune(e more...poiché -prunerestituisce sempre true) per ogni file ma .. Quindi lo farà more...su tutti i file in ., ma escluderà .e non scenderà nelle sottodirectory di .. Quindi è l'equivalente standard di GNU -mindepth 1 -maxdepth 1.
Stéphane Chazelas,

18

Ho provato questo su una directory con 2259 voci e ho usato il timecomando.

L'output di time for f in *; do echo "$f"; done(meno i file!) È:

real    0m0.062s
user    0m0.036s
sys     0m0.012s

L'output di time find * -prune | while read f; do echo "$f"; done(meno i file!) È:

real    0m0.131s
user    0m0.056s
sys     0m0.060s

Ho eseguito ogni comando più volte, in modo da eliminare i mancati cache. Ciò suggerisce che tenerlo in bash(per i in ...) è più veloce dell'uso finde del piping dell'output (to bash)

Solo per completezza, ho lasciato cadere la pipa da find, poiché nel tuo esempio è totalmente ridondante. L'output di just find * -pruneè:

real    0m0.053s
user    0m0.016s
sys     0m0.024s

Inoltre, time echo *(l'output non è separato da una nuova riga, ahimè):

real    0m0.009s
user    0m0.008s
sys     0m0.000s

A questo punto, sospetto che il motivo echo *sia più rapido se non sta producendo così tante nuove righe, quindi l'output non scorre tanto. Testiamo ...

time find * -prune | while read f; do echo "$f"; done > /dev/null

rendimenti:

real    0m0.109s
user    0m0.076s
sys     0m0.032s

mentre time find * -prune > /dev/nullproduce:

real    0m0.027s
user    0m0.008s
sys     0m0.012s

e time for f in *; do echo "$f"; done > /dev/nullrese:

real    0m0.040s
user    0m0.036s
sys     0m0.004s

e infine: time echo * > /dev/nullrese:

real    0m0.011s
user    0m0.012s
sys     0m0.000s

Alcune variazioni possono essere spiegate da fattori casuali, ma sembra chiaro:

  • l'uscita è lenta
  • le tubazioni costano un po '
  • for f in *; do ...è più lento di find * -prune, da solo, ma per le costruzioni sopra che coinvolgono tubi, è più veloce.

Inoltre, a parte, entrambi gli approcci sembrano gestire nomi con spazi perfetti.

MODIFICARE:

Tempi per find . -maxdepth 1 > /dev/nullvs find * -prune > /dev/null.:

time find . -maxdepth 1 > /dev/null:

real    0m0.018s
user    0m0.008s
sys     0m0.008s

find * -prune > /dev/null:

real    0m0.031s
user    0m0.020s
sys     0m0.008s

Quindi, conclusione aggiuntiva:

  • find * -pruneè più lento di find . -maxdepth 1- nel primo caso, la shell sta elaborando un glob, quindi costruendo una (grande) riga di comando per find. NB: find . -prunerestituisce solo ..

Più test time find . -maxdepth 1 -exec echo {} \; >/dev/null::

real    0m3.389s
user    0m0.040s
sys     0m0.412s

Conclusione:

  • Il modo più lento per farlo finora. Come è stato sottolineato nei commenti per la risposta in cui è stato suggerito questo approccio, ogni argomento genera una shell.

Quale pipe è ridondante? puoi mostrare la linea che hai usato senza pipe?
rubo77,

2
@ rubo77 find * -prune | while read f; do echo "$f"; doneha la pipe ridondante: tutto ciò che sta facendo è produrre esattamente ciò che findproduce da solo. Senza una pipe, sarebbe semplicemente find * -prune La pipe è ridondante solo perché la cosa sull'altro lato della pipe copia semplicemente lo stdin in stdout (per la maggior parte). È un costoso no-op. Se vuoi fare cose con l'output di find, oltre a sputarlo nuovamente, è diverso.
Phil

Forse il principale cronometro è il *. Come affermato da BitsOfNix : suggerisco ancora vivamente di non usare *e .per findinvece.
rubo77,

@ rubo77 sembra così. Immagino di averlo ignorato. Ho aggiunto risultati per il mio sistema. Presumo find . -prunesia più veloce perché findleggerà una voce di directory alla lettera, mentre la shell farà altrettanto, potenzialmente corrispondendo al glob (potrebbe ottimizzare per *), quindi costruendo la grande riga di comando per find.
Phil

1
find . -prunestampa solo .sul mio sistema. Non funziona quasi per niente. Non è affatto lo stesso find * -pruneche mostra tutti i nomi nella directory corrente. Un nudo read fmanipolerà i nomi dei file con spazi iniziali.
Ian D. Allen,

10

Andrei sicuramente con find anche se cambierei la tua scoperta in questo modo:

find . -maxdepth 1 -exec echo {} \;

Per quanto riguarda le prestazioni, findovviamente è molto più veloce a seconda delle tue esigenze. Quello che hai attualmente con foresso mostrerà solo i file / le directory nella directory corrente ma non i contenuti delle directory. Se usi find, mostrerà anche il contenuto delle sottodirectory.

Dico scoperta è meglio perché con il vostro forl' *dovrà essere ampliato prima e ho paura che se si dispone di una directory con una grande quantità di file potrebbe dare l'errore lista degli argomenti troppo lungo . Lo stesso vale perfind *

Ad esempio, in uno dei sistemi che uso attualmente ci sono un paio di directory con oltre 2 milioni di file (<100k ciascuno):

find *
-bash: /usr/bin/find: Argument list too long

Ho aggiunto -pruneper rendere i due esempi più simili. e preferisco la pipe con while, quindi è più facile applicare più comandi nel loop
rubo77


cambiare il limite rigido non è certo una soluzione adeguata dal mio POV. Specialmente quando si parla di oltre 2 milioni di file. Senza la digressione dalla domanda, per casi semplici in quanto una directory di livello è più veloce, ma se si modifica la struttura di file / directory sarà più difficile migrare. Mentre con find ed è un'enorme quantità di opzioni puoi essere meglio preparato. Consiglio comunque vivamente di non usare * e. per trovare invece. Sarebbe più portatile di * dove potresti non essere in grado di controllare l'hardlimit ...
BitsOfNix

4
Ciò genererà un processo di eco per file (mentre nella shell per loop, è l'eco incorporato che verrà utilizzato senza un processo aggiuntivo) e scenderà nelle directory, quindi sarà molto più lento . Si noti inoltre che includerà file dot.
Stéphane Chazelas,

Hai ragione, ho aggiunto il maxdepth 1 in modo che si attacchi solo al livello corrente.
BitsOfNix,

7
find * -prune | while read f; do 
    echo "$f"
done

è un uso inutile di find- Quello che stai dicendo è efficacemente "per ogni file nella directory ( *), non trovare alcun file. Inoltre, non è sicuro per diversi motivi:

  • Le barre rovesciate nei percorsi vengono trattate appositamente senza l' -ropzione read. Questo non è un problema con il forloop.
  • Le newline nei percorsi rompono qualsiasi funzionalità non banale all'interno del loop. Questo non è un problema con il forloop.

Gestire qualsiasi nome di file con findè difficile , quindi dovresti usare l' foropzione loop ogni volta che è possibile solo per quel motivo. Inoltre, l'esecuzione di un programma esterno come findin generale sarà più lenta rispetto all'esecuzione di un comando di ciclo interno come for.


@ I0b0 Che ne dite di trovare -path './*' -prune o find -path './[^.[*' -prune (per evitare file e directory nascosti) come un costrutto migliore - in forma completa: find -path ' ./* '-prune -print0 | xargs -0 sh -c '...'?
AsymLabs

1
find's -print0xargs' -0sono POSIX compatibili, e non si può mettere in comandi arbitrari sh -c ' ... '(apici non possono essere sfuggiti tra virgolette singole), quindi non è così semplice.
l0b0

4

Ma siamo fanatici delle domande sulle prestazioni! Questa richiesta di esperimento fa almeno due ipotesi che lo rendono non terribilmente valido.

A. Supponiamo che trovino gli stessi file ...

Ebbene, essi potranno trovare gli stessi file in un primo momento, perché sono entrambi iterazione rispetto allo stesso glob, vale a dire *. Ma find * -prune | while read fsoffre di diversi difetti che lo rendono del tutto possibile, non troverà tutti i file che ti aspetti:

  1. Trovare POSIX non è garantito per accettare più di un argomento di percorso. La maggior parte delle findimplementazioni lo fanno, ma comunque non dovresti fare affidamento su questo.
  2. find *può rompersi quando si colpisce ARG_MAX. for f in *non, perché si ARG_MAXapplica a exec, non builtin.
  3. while read fpuò rompersi con nomi di file che iniziano e finiscono con spazi bianchi, che verranno rimossi. Potresti superare questo con while reade il suo parametro predefinito REPLY, ma ciò non ti aiuterà ancora quando si tratta di nomi di file con nuove righe al loro interno.

B. echo. Nessuno lo farà solo per fare eco al nome del file. Se lo desideri, fai uno di questi:

printf '%s\n' *
find . -mindepth 1 -maxdepth 1 # for dotted names, too

La pipe per il whileloop qui crea una subshell implicita che si chiude alla fine del loop, il che potrebbe non essere intuitivo per alcuni.

Per rispondere alla domanda, ecco i risultati in una mia directory che contiene 184 file e directory.

$ time bash -c 'for i in {0..1000}; do find * -prune | while read f; do echo "$f"; done >/dev/null; done'

real    0m7.998s
user    0m5.204s
sys 0m2.996s
$ time bash -c 'for i in {0..1000}; do for f in *; do echo "$f"; done >/dev/null; done'

real    0m2.734s
user    0m2.553s
sys 0m0.181s
$ time bash -c 'for i in {0..1000}; do printf '%s\n' * > /dev/null; done'

real    0m1.468s
user    0m1.401s
sys 0m0.067s

$ time bash -c 'for i in {0..1000}; do find . -mindepth 1 -maxdepth 1 >/dev/null; done '

real    0m1.946s
user    0m0.847s
sys 0m0.933s

Non sono d'accordo con l'affermazione che il ciclo while genera una subshell - nel peggiore dei casi, un nuovo thread: quanto segue sta cercando di mostrare prima e dopo, scuse per la scarsa formattazione$ ps ax | grep bash 20784 pts/1 Ss 0:00 -bash 20811 pts/1 R+ 0:00 grep bash $ while true; do while true; do while true; do while true; do while true; do sleep 100; done; done; done; done; done ^Z [1]+ Stopped sleep 100 $ bg [1]+ sleep 100 & $ ps ax | grep bash 20784 pts/1 Ss 0:00 -bash 20924 pts/1 S+ 0:00 grep bash
Phil

Tecnicamente ho sbagliato a scrivere: la pipe causa la subshell implicita, non il ciclo while. Lo modificherò.
Kojiro,

2

find *non funzionerà correttamente se *produce token che sembrano predicati anziché percorsi.

Non puoi usare il solito --argomento per risolvere questo problema perché --indica la fine delle opzioni e le opzioni di find vengono prima dei percorsi.

Per risolvere questo problema è possibile utilizzare find ./*invece. Ma poi non sta producendo esattamente le stesse stringhe di for x in *.

Si noti che in find ./* -prune | while read f ..realtà non utilizza la funzionalità di scansione di find. È la sintassi globbing ./*che attraversa effettivamente la directory e genera nomi. Quindi il findprogramma dovrà eseguire almeno un statcontrollo su ciascuno di quei nomi. Hai il sovraccarico di avviare il programma e averlo accesso a questi file, quindi fare I / O per leggere il suo output.

È difficile immaginare come potrebbe essere tutt'altro che meno efficiente di for x in ./* ....


1

Bene per cominciare forè una parola chiave shell, integrata in Bash, mentre findè un eseguibile separato.

$ type -a for
for is a shell keyword

$ type -a find
find is /usr/bin/find

Il forloop troverà i file dal personaggio globstar solo quando si espanderà, non si ripeterà in nessuna directory trovata.

D'altra parte verrà anche fornito un elenco espanso dalla globstar, ma troverà ricorsivamente tutti i file e le directory al di sotto di questo elenco espanso e reindirizzerà ciascuno attraverso il whileloop.

Entrambi questi approcci potrebbero essere considerati pericolosi nel senso che non gestiscono percorsi o nomi di file che contengono spazi.

Questo è tutto ciò che riesco a pensare che valga la pena commentare questi 2 approcci.


Ho aggiunto -prune al comando find, quindi sono più simili.
rubo77,

0

Se tutti i file restituiti da find possono essere elaborati da un singolo comando (ovviamente non applicabile al tuo esempio di eco sopra), puoi usare xargs:

find * |xargs some-command

0

Da anni uso questo: -

find . -name 'filename'|xargs grep 'pattern'|more

per cercare alcuni file (ad es. * .txt) che contengono uno schema che grep può cercare e inserirlo in più in modo che non scorra fuori dallo schermo. A volte uso la >> pipe per scrivere i risultati in un altro file che posso vedere in seguito.

Ecco un esempio del risultato: -

./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2008-August.txt:Message-ID: <A165CE5C-61C5-4794-8651-66F5678ABCBF@usit.net>
./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2008-August.txt:In-Reply-To: <A165CE5C-61C5-4794-8651-66F5678ABCBF@usit.net>
./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2008-August.txt:  <A165CE5C-61C5-4794-8651-66F5678ABCBF@usit.net>
./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2008-August.txt:  <A165CE5C-61C5-4794-8651-66F5678ABCBF@usit.net>
./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2008-August.txt:Message-ID: <448E53556A3F442ABC58203D6281923E@hypermax>
./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2011-April.txt:URL: http://mylist.net/private/rodgersorganusers/attachments/20110420/3f
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.