Come scorrere i nomi dei file restituiti da find?


223
x=$(find . -name "*.txt")
echo $x

se eseguo il pezzo di codice sopra nella shell Bash, quello che ottengo è una stringa contenente diversi nomi di file separati da spazi vuoti, non un elenco.

Certo, posso separarli ulteriormente in bianco per ottenere un elenco, ma sono sicuro che esiste un modo migliore per farlo.

Quindi qual è il modo migliore per scorrere i risultati di un findcomando?


3
Il modo migliore per passare in rassegna i nomi dei file dipende un po 'da ciò che vuoi effettivamente fare con esso, ma a meno che tu non possa garantire che nessun file abbia uno spazio nel loro nome, questo non è un ottimo modo per farlo. Quindi cosa vuoi fare in loop sui file?
Kevin,

1
Per quanto riguarda la generosità : l'idea principale qui è quella di ottenere una risposta canonica che copra tutti i casi possibili (nomi di file con nuove righe, caratteri problematici ...). L'idea è quindi di usare questi nomi di file per fare alcune cose (chiamare un altro comando, eseguire un po 'di rinomina ...). Grazie!
fedorqui "SO smettere di danneggiare" il

Non dimenticare che un nome di file o cartella può contenere ".txt" seguito da spazio e un'altra stringa, ad esempio "qualcosa.txt qualcosa" o "qualcosa.txt"
Yahya Yahyaoui,

Usa array, non var x=( $(find . -name "*.txt") ); echo "${x[@]}"Quindi puoi passare for item in "${x[@]}"; { echo "$item"; }
Ivan il

Risposte:


392

TL; DR: Se sei qui solo per la risposta più corretta, probabilmente vuoi la mia preferenza personale, find . -name '*.txt' -exec process {} \;(vedi il fondo di questo post). Se hai tempo, leggi tutto il resto per vedere diversi modi e problemi con la maggior parte di essi.


La risposta completa:

Il modo migliore dipende da cosa vuoi fare, ma qui ci sono alcune opzioni. Finché nessun file o cartella nella sottostruttura ha spazi vuoti nel suo nome, puoi semplicemente passare in rassegna i file:

for i in $x; do # Not recommended, will break on whitespace
    process "$i"
done

Marginalmente meglio, ritaglia la variabile temporanea x:

for i in $(find -name \*.txt); do # Not recommended, will break on whitespace
    process "$i"
done

È molto meglio glob quando puoi. Spazio vuoto sicuro, per i file nella directory corrente:

for i in *.txt; do # Whitespace-safe but not recursive.
    process "$i"
done

Abilitando l' globstaropzione, puoi glob tutti i file corrispondenti in questa directory e in tutte le sottodirectory:

# Make sure globstar is enabled
shopt -s globstar
for i in **/*.txt; do # Whitespace-safe and recursive
    process "$i"
done

In alcuni casi, ad esempio se i nomi dei file sono già in un file, potrebbe essere necessario utilizzare read:

# IFS= makes sure it doesn't trim leading and trailing whitespace
# -r prevents interpretation of \ escapes.
while IFS= read -r line; do # Whitespace-safe EXCEPT newlines
    process "$line"
done < filename

readpuò essere utilizzato in sicurezza in combinazione con l' findimpostazione del delimitatore in modo appropriato:

find . -name '*.txt' -print0 | 
    while IFS= read -r -d '' line; do 
        process "$line"
    done

Per ricerche più complesse, probabilmente vorrai utilizzare find, con la sua -execopzione o con -print0 | xargs -0:

# execute `process` once for each file
find . -name \*.txt -exec process {} \;

# execute `process` once with all the files as arguments*:
find . -name \*.txt -exec process {} +

# using xargs*
find . -name \*.txt -print0 | xargs -0 process

# using xargs with arguments after each filename (implies one run per filename)
find . -name \*.txt -print0 | xargs -0 -I{} process {} argument

findpuò anche eseguire il cd nella directory di ciascun file prima di eseguire un comando utilizzando -execdirinvece di -exece può essere reso interattivo (prompt prima di eseguire il comando per ciascun file) utilizzando -okinvece di -exec(o -okdirinvece di-execdir ).

*: Tecnicamente, entrambi finde xargs(per impostazione predefinita) eseguiranno il comando con tutti gli argomenti che possono adattarsi alla riga di comando, tutte le volte che ci vuole per passare attraverso tutti i file. In pratica, a meno che tu non abbia un numero molto elevato di file, non importa, e se superi la lunghezza ma ne hai bisogno tutti sulla stessa riga di comando, sei SOL a trovare un modo diverso.


4
Vale la pena notare che nel caso con done < filenamee il seguente con la pipe lo stdin non può più essere utilizzato (→ non più roba interattiva all'interno del loop), ma nei casi in cui è necessario si può usare al 3<posto di <e aggiungere <&3o -u3a la readparte, fondamentalmente usando un descrittore di file separato. Inoltre, credo che read -d ''sia lo stesso read -d $'\0'ma non riesco a trovare alcuna documentazione ufficiale su questo in questo momento.
phk,

1
per i in * .txt; non funziona, se nessun file corrisponde. È necessario un test xtra, ad esempio [[-e $ i]]
Michael Brux,

2
Mi sono perso con questa parte: -exec process {} \;e la mia ipotesi è che sia tutta un'altra domanda: cosa significa e come posso manipolarlo? Dov'è un buon Q / A o doc. su di essa?
Alex Hall,

1
@AlexHall puoi sempre guardare le pagine man ( man find). In questo caso, -execdice finddi eseguire il seguente comando, terminato da ;(o +), in cui {}sarà sostituito dal nome del file che sta elaborando (o, se +usato, tutti i file che lo hanno reso a quella condizione).
Kevin,

3
@phk -d ''è meglio di -d $'\0'. Quest'ultimo non è solo più lungo, ma suggerisce anche che potresti passare argomenti contenenti byte null, ma non puoi. Il primo byte null segna la fine della stringa. In bash $'a\0bc'è uguale aed $'\0'è uguale $'\0abc'o solo la stringa vuota ''. help readafferma che " Il primo carattere di delim viene utilizzato per terminare l'input ", quindi l'utilizzo ''come delimitatore è un po 'un hack. Il primo carattere nella stringa vuota è il byte null che segna sempre la fine della stringa (anche se non la scrivi esplicitamente).
Socowi,

114

Qualunque cosa tu faccia, non usare un forloop :

# Don't do this
for file in $(find . -name "*.txt")
do
    code using "$file"
done

Tre motivi:

  • Affinché il ciclo for sia avviato, è findnecessario eseguire il completamento.
  • Se il nome di un file contiene spazi bianchi (incluso spazio, tabulazione o nuova riga), verrà trattato come due nomi separati.
  • Anche se ora è improbabile, puoi sovraccaricare il buffer della riga di comando. Immagina se il buffer della riga di comando contiene 32 forKB e il tuo loop restituisce 40 KB di testo. Gli ultimi 8 KB verranno eliminati dal forloop e non lo saprai mai.

Usa sempre un while readcostrutto:

find . -name "*.txt" -print0 | while read -d $'\0' file
do
    code using "$file"
done

Il ciclo verrà eseguito mentre il findcomando è in esecuzione. Inoltre, questo comando funzionerà anche se viene restituito un nome file con spazi bianchi. E non sovraccarichi il buffer della riga di comando.

Il -print0utilizzerà il NULL come un separatore di file invece di un ritorno a capo e la -d $'\0'userà NULL come separatore durante la lettura.


3
Non funzionerà con le nuove righe nei nomi dei file. Usa -execinvece trova.
utente sconosciuto

2
@userunknown - Hai ragione. -execè il più sicuro poiché non utilizza affatto la shell. Tuttavia, la NL nei nomi dei file è piuttosto rara. Gli spazi nei nomi dei file sono abbastanza comuni. Il punto principale non è usare un forloop consigliato da molti poster.
David W.

1
@userunknown - Qui. Ho risolto questo problema, quindi ora si occuperà dei file con nuove linee, schede e qualsiasi altro spazio bianco. Il punto centrale del post è quello di dire all'OP di non usare la for file $(find)causa dei problemi ad essa associati.
David W.

4
Se puoi usare -exec è meglio, ma ci sono momenti in cui hai davvero bisogno del nome restituito alla shell. Ad esempio, se si desidera rimuovere le estensioni di file.
Ben Reser,

5
Dovresti usare l' -ropzione per read: -r raw input - disables interpretion of backslash escapes and line-continuation in the read data
Daira Hopwood il

102
find . -name "*.txt"|while read fname; do
  echo "$fname"
done

Nota: questo metodo e il (secondo) metodo mostrato da bmargulies sono sicuri da usare con uno spazio bianco nei nomi di file / cartelle.

Per avere anche il caso - un po 'esotico - di newline nei nomi di file / cartelle, dovrai ricorrere al -execpredicato findcome questo:

find . -name '*.txt' -exec echo "{}" \;

Il {}è il segnaposto per l'oggetto trovato e la \;si usa per terminare il-exec predicato.

E per completezza, lasciami aggiungere un'altra variante: devi amare i modi * nix per la loro versatilità:

find . -name '*.txt' -print0|xargs -0 -n 1 echo

Ciò separerebbe gli elementi stampati con un \0carattere che non è consentito in nessuno dei file system nei nomi di file o cartelle, per quanto ne so, e quindi dovrebbe coprire tutte le basi. xargsli prende uno per uno poi ...


3
Non riesce se newline nel nome file.
utente sconosciuto

2
@utente sconosciuto: hai ragione, è un caso che non avevo mai preso in considerazione e che, penso, è molto esotico. Ma ho adattato la mia risposta di conseguenza.
0xC0000022L

5
Probabilmente la pena sottolineare che find -print0e xargs -0sono entrambe le estensioni GNU e argomenti non portatili (POSIX). Incredibilmente utile su quei sistemi che li hanno, però!
Toby Speight,

1
Ciò fallisce anche con i nomi di file contenenti barre rovesciate (che read -rpotrebbero essere corretti) o con i nomi di file che terminano in spazi bianchi (che IFS= readverrebbero corretti). Quindi BashFAQ n. 1 suggeriscewhile IFS= read -r filename; do ...
Charles Duffy,

1
Un altro problema con questo è che sembra che il corpo del ciclo stia eseguendo nella stessa shell, ma non lo è, quindi ad esempio exitnon funzionerà come previsto e le variabili impostate nel corpo del ciclo non saranno disponibili dopo il ciclo.
EM0

17

I nomi dei file possono includere spazi e persino caratteri di controllo. Gli spazi sono delimitatori (predefiniti) per l'espansione della shell in bash e di conseguenza x=$(find . -name "*.txt")dalla domanda non è affatto raccomandato. Se find ottiene un nome file con spazi, ad esempio "the file.txt", otterrai 2 stringhe separate per l'elaborazione, se elaborate xin un ciclo. Puoi migliorarlo cambiando delimitatore (bash IFSVariable) ad es. In \r\n, ma i nomi dei file possono includere caratteri di controllo - quindi questo non è un metodo (completamente) sicuro.

Dal mio punto di vista, ci sono 2 modelli consigliati (e sicuri) per l'elaborazione dei file:

1. Utilizzare per l'espansione loop e nome file:

for file in ./*.txt; do
    [[ ! -e $file ]] && continue  # continue, if file does not exist
    # single filename is in $file
    echo "$file"
    # your code here
done

2. Utilizzare la sostituzione find-read-while e process

while IFS= read -r -d '' file; do
    # single filename is in $file
    echo "$file"
    # your code here
done < <(find . -name "*.txt" -print0)

Osservazioni

sul modello 1:

  1. bash restituisce il modello di ricerca ("* .txt") se non viene trovato alcun file corrispondente, quindi è necessaria la riga aggiuntiva "continua, se il file non esiste". vedere il Manuale di Bash, Espansione del nome file
  2. L'opzione shell nullglobpuò essere utilizzata per evitare questa riga aggiuntiva.
  3. "Se l' failglobopzione shell è impostata e non viene trovata alcuna corrispondenza, viene stampato un messaggio di errore e il comando non viene eseguito." (dal manuale Bash sopra)
  4. opzione shell globstar: "Se impostato, il modello '**' utilizzato in un contesto di espansione del nome del file corrisponderà a tutti i file e zero o più directory e sottodirectory. Se il modello è seguito da un '/', corrispondono solo le directory e le sottodirectory." vedere il Manuale di Bash, Shopt Builtin
  5. altre opzioni per l'espansione dei nomi: extglob,nocaseglob , dotglobe variabile di shellGLOBIGNORE

sul modello 2:

  1. i nomi dei file possono contenere spazi vuoti, tabulazioni, spazi, newline, ... per elaborare i nomi dei file in modo sicuro, findcon -print0viene utilizzato: il nome file viene stampato con tutti i caratteri di controllo e termina con NUL. vedi anche Gnu Findutils Manpage, Unsafe Manipolazione Nome file , sicuro Nome file Handling , personaggi insoliti nei nomi dei file . Vedi David A. Wheeler di seguito per una discussione dettagliata di questo argomento.

  2. Esistono alcuni schemi possibili per elaborare i risultati di ricerca in un ciclo while. Altri (Kevin, David W.) hanno mostrato come fare usando le pipe:

    files_found=1 find . -name "*.txt" -print0 | while IFS= read -r -d '' file; do # single filename in $file echo "$file" files_found=0 # not working example # your code here done [[ $files_found -eq 0 ]] && echo "files found" || echo "no files found"
    Quando provi questo pezzo di codice, vedrai che non funziona: files_foundè sempre "vero" e il codice fa sempre eco "nessun file trovato". Il motivo è: ogni comando di una pipeline viene eseguito in una subshell separata, quindi la variabile modificata all'interno del ciclo (subshell separata) non cambia la variabile nello script della shell principale. Questo è il motivo per cui consiglio di utilizzare la sostituzione di processo come modello "migliore", più utile, più generale.
    Vedi ho impostato le variabili in un ciclo che è in una pipeline. Perché scompaiono ... (dalle FAQ di Greg's Bash) per una discussione dettagliata su questo argomento.

Riferimenti e fonti aggiuntive:


8

(Aggiornato per includere l'eccezionale miglioramento della velocità di @ Socowi)

Con chiunque $SHELLlo supporti (trattino / zsh / bash ...):

find . -name "*.txt" -exec $SHELL -c '
    for i in "$@" ; do
        echo "$i"
    done
' {} +

Fatto.


Risposta originale (più breve, ma più lenta):

find . -name "*.txt" -exec $SHELL -c '
    echo "$0"
' {} \;

1
Lento come melassa (dal momento che lancia una shell per ogni file) ma questo funziona. +1
dawg,

1
Invece di \;te puoi usare +per passare quanti più file possibili a un singolo exec. Quindi utilizzare "$@"all'interno dello script della shell per elaborare tutti questi parametri.
Socowi,

3
C'è un bug in questo codice. Nel loop manca il primo risultato. Questo perché lo $@omette poiché in genere è il nome dello script. Dobbiamo solo aggiungere una via dummydi mezzo 'e {}così può prendere il posto del nome dello script, assicurando che tutte le corrispondenze vengano elaborate dal loop.
BCartolo,

E se avessi bisogno di altre variabili esterne alla shell appena creata?
Jodo

OTHERVAR=foo find . -na.....dovrebbe consentire all'utente di accedere $OTHERVARdall'interno della shell appena creata.
user569825

6
# Doesn't handle whitespace
for x in `find . -name "*.txt" -print`; do
  process_one $x
done

or

# Handles whitespace and newlines
find . -name "*.txt" -print0 | xargs -0 -n 1 process_one

3
for x in $(find ...)si romperà per qualsiasi nome di file con spazi bianchi al suo interno. Lo stesso find ... | xargs-print0-0
vale

1
Usa find . -name "*.txt -exec process_one {} ";"invece. Perché dovremmo usare xargs per raccogliere risultati, che abbiamo già?
utente sconosciuto

@userunknown Bene, tutto dipende da ciò che process_oneè. Se è un segnaposto per un comando effettivo , sicuramente funzionerebbe (se correggi l'errore di battitura e aggiungi dopo le virgolette di chiusura "*.txt). Ma se process_oneè una funzione definita dall'utente, il codice non funzionerà.
Toxalot

@toxalot: Sì, ma non sarebbe un problema scrivere la funzione in uno script da chiamare.
utente sconosciuto

4

È possibile memorizzare l' findoutput nell'array se si desidera utilizzare l'output in seguito come:

array=($(find . -name "*.txt"))

Ora per stampare ogni elemento in una nuova riga, puoi usare l' foriterazione ciclica su tutti gli elementi dell'array oppure puoi usare l'istruzione printf.

for i in ${array[@]};do echo $i; done

o

printf '%s\n' "${array[@]}"

Puoi anche usare:

for file in "`find . -name "*.txt"`"; do echo "$file"; done

Questo stamperà ogni nome di file in newline

Per stampare solo l' findoutput in forma di elenco, è possibile utilizzare uno dei seguenti:

find . -name "*.txt" -print 2>/dev/null

o

find . -name "*.txt" -print | grep -v 'Permission denied'

Ciò rimuoverà i messaggi di errore e fornirà il nome file solo come output in una nuova riga.

Se desideri fare qualcosa con i nomi dei file, archiviarlo nella matrice è buono, altrimenti non è necessario consumare quello spazio e puoi stampare direttamente l'output find.


1
Il ciclo sull'array non riesce con spazi nei nomi dei file.
EM0

Devi eliminare questa risposta. Non funziona con spazi nei nomi di file o directory.
jww

4

Se puoi assumere che i nomi dei file non contengano newline, puoi leggere l'output di findin un array Bash usando il seguente comando:

readarray -t x < <(find . -name '*.txt')

Nota:

  • -t cause readarray spogliare le nuove righe.
  • Non funzionerà se readarray trova in una pipe, quindi la sostituzione del processo.
  • readarray è disponibile da Bash 4.

Bash 4.4 e versioni successive supportano anche il -dparametro per specificare il delimitatore. L'uso del carattere null, anziché newline, per delimitare i nomi dei file funziona anche nel raro caso in cui i nomi dei file contengano newline:

readarray -d '' x < <(find . -name '*.txt' -print0)

readarray può anche essere invocato come mapfile con le stesse opzioni.

Riferimento: https://mywiki.wooledge.org/BashFAQ/005#Loading_lines_from_a_file_or_stream


Questa è la risposta migliore! Funziona con: * Spazi nei nomi dei file * Nessun file corrispondente * exitdurante il ciclo dei risultati
EM0

Non funziona con tutti i possibili nomi di file, tuttavia - per questo, dovresti usarereadarray -d '' x < <(find . -name '*.txt' -print0)
Charles Duffy,

3

Mi piace usare find che è prima assegnato alla variabile e IFS è passato alla nuova linea come segue:

FilesFound=$(find . -name "*.txt")

IFSbkp="$IFS"
IFS=$'\n'
counter=1;
for file in $FilesFound; do
    echo "${counter}: ${file}"
    let counter++;
done
IFS="$IFSbkp"

Nel caso in cui si desideri ripetere più azioni sullo stesso set di DATI e trovare è molto lento sul server (utilizzo elevato I / 0)


2

È possibile inserire i nomi dei file restituiti findin un array come questo:

array=()
while IFS=  read -r -d ''; do
    array+=("$REPLY")
done < <(find . -name '*.txt' -print0)

Ora puoi semplicemente passare in rassegna l'array per accedere a singoli elementi e fare quello che vuoi con loro.

Nota: è sicuro per gli spazi bianchi.


1
Con bash 4.4 o superiore è possibile utilizzare un unico comando, invece di un ciclo: mapfile -t -d '' array < <(find ...). L'impostazione IFSnon è necessaria per mapfile.
Socowi,

1

basato su altre risposte e commenti di @phk, usando fd # 3:
(che consente ancora di usare stdin all'interno del loop)

while IFS= read -r f <&3; do
    echo "$f"

done 3< <(find . -iname "*filename*")

-1

find <path> -xdev -type f -name *.txt -exec ls -l {} \;

Questo elencherà i file e fornirà dettagli sugli attributi.


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.