Commenti
- Non sono daccordo con il fatto che questo sarebbe un duplicato. La risposta accettata risponde a come eseguire il ciclo su nomi di file con spazi; che non ha nulla a che fare con " perché sta andando in loop su find ' s output cattive pratiche ". Ho trovato questa domanda (non laltra) perché ho bisogno di scorrere i nomi dei file con spazi, come in: for file in $ LIST_OF_FILES; do … dove $ LIST_OF_FILES non è loutput di find; è ' è solo un elenco di nomi di file (separati da nuove righe).
- @CarloWood – i nomi dei file possono includere nuove righe, quindi la tua domanda è piuttosto unica: un elenco di nomi di file che possono contenere spazi ma non nuove righe. Penso che ' dovrai utilizzare la tecnica IFS, per indicare che linterruzione si verifica in ' \ n '
- @ Diagonowoah, non mi sono mai reso conto che i nomi dei file possono contenere nuove righe. Uso principalmente (solo) linux / UNIX e persino gli spazi sono rari; Di certo non ho mai visto in vita mia usare i newline: p. Potrebbero anche vietare che imho.
- @CarloWood – i nomi dei file terminano con un null (' \ 0 ' , come ' '). Qualsiasi altra cosa è accettabile.
- @CarloWood Devi ricordare che le persone votano per prime e leggono per seconde …
Risposta
Risposta breve (più simile alla tua risposta, ma gestisce gli spazi)
OIFS="$IFS" IFS=$"\n" for file in `find . -type f -name "*.csv"` do echo "file = $file" diff "$file" "/some/other/path/$file" read line done IFS="$OIFS"
Risposta migliore (gestisce anche caratteri jolly e nuove righe nei nomi dei file)
find . -type f -name "*.csv" -print0 | while IFS= read -r -d "" file; do echo "file = $file" diff "$file" "/some/other/path/$file" read line </dev/tty done
Migliore risposta (basata su Gilles ” answer )
find . -type f -name "*.csv" -exec sh -c " file="$0" echo "$file" diff "$file" "/some/other/path/$file" read line </dev/tty " exec-sh {} ";"
O ancora meglio, per evitare di eseguirne uno sh
per file:
find . -type f -name "*.csv" -exec sh -c " for file do echo "$file" diff "$file" "/some/other/path/$file" read line </dev/tty done " exec-sh {} +
Risposta lunga
Hai tre problemi:
- Per impostazione predefinita, la shell divide loutput di un comando su spazi, tabulazioni e nuove righe
- I nomi dei file possono contenere caratteri jolly che verrebbe espanso
- E se fosse presente una directory il cui nome termina con
*.csv
?
1. Suddivisione solo sulle nuove righe
Per capire cosa impostare file
, la shell deve prendere loutput di find
e interpretarlo in qualche modo, altrimenti file
sarebbe solo lintero output di find
.
La shell legge la variabile IFS
, che è impostata su <space><tab><newline>
per impostazione predefinita.
Quindi esamina ogni carattere nelloutput di find
. Non appena vede un carattere che “è in IFS
, pensa che segna la fine del nome del file, quindi imposta file
a qualunque carattere abbia visto fino ad ora ed esegue il ciclo. Quindi inizia da dove era stato interrotto per ottenere il nome del file successivo, ed esegue il ciclo successivo, ecc., fino a raggiungere la fine delloutput.
Quindi sta effettivamente facendo questo:
for file in "zquery" "-" "abc" ...
Per dirgli di dividere linput solo su newline, devi farlo
IFS=$"\n"
prima del comando for ... find
.
Questo imposta IFS
su un singola nuova riga, quindi si divide solo in caso di nuova riga e non anche di spazi e tabulazioni.
Se utilizzi sh
o dash
invece di ksh93
, bash
o zsh
, devi scrivere IFS=$"\n"
in questo modo:
IFS=" "
Probabilmente è sufficiente per far funzionare il tuo script, ma se sei interessato a gestire correttamente alcuni altri casi dangolo, continua a leggere …
2. Espandibile $file
senza caratteri jolly
Allinterno del ciclo in cui fai
diff $file /some/other/path/$file
la shell tenta di espandere $file
(di nuovo!).
Potrebbe contenere spazi, ma poiché abbiamo già impostato IFS
sopra, non sarà un problema qui.
Ma potrebbe anche contenere caratteri jolly come *
o ?
, che porterebbero a un comportamento imprevedibile. (Grazie a Gilles per averlo sottolineato.)
Per dire alla shell di non espandere i caratteri jolly, inserisci la variabile tra virgolette doppie, ad esempio
diff "$file" "/some/other/path/$file"
Lo stesso problema potrebbe morderci anche
for file in `find . -name "*.csv"`
Ad esempio, se avessi questi tre file
file1.csv file2.csv *.csv
(molto improbabile, ma ancora possibile)
Sarebbe come se avessi eseguito
for file in file1.csv file2.csv *.csv
che verrà espanso in
for file in file1.csv file2.csv *.csv file1.csv file2.csv
causando file1.csv
e file2.csv
da elaborare due volte.
Invece, dobbiamo fare
find . -name "*.csv" -print | while IFS= read -r file; do echo "file = $file" diff "$file" "/some/other/path/$file" read line </dev/tty done
read
legge le righe dallo standard input, divide la riga in parole in base a IFS
e le memorizza nei nomi delle variabili che specifichi.
Qui, lo stiamo dicendo per non dividere la riga in parole e per memorizzare la riga in $file
.
Nota inoltre che è stato modificato in read line </dev/tty
.
Ciò è dovuto al fatto che allinterno del ciclo, linput standard proviene da find
tramite la pipeline.
Se facessimo solo read
, consumerebbe parte o tutto il nome di un file e alcuni file verrebbero ignorati .
/dev/tty
è il terminale da cui lutente esegue lo script. Nota che questo causerà un errore se lo script viene eseguito tramite cron, ma presumo che questo non sia importante in questo caso.
Allora, cosa succede se un nome di file contiene nuove righe?
Possiamo gestirlo modificando -print
in -print0
e utilizzando read -d ""
alla fine di un pipeline:
find . -name "*.csv" -print0 | while IFS= read -r -d "" file; do echo "file = $file" diff "$file" "/some/other/path/$file" read char </dev/tty done
Questo fa sì che find
inserisca un byte nullo alla fine di ogni nome di file. I byte nulli sono gli unici caratteri non consentiti nei nomi di file, quindi questo dovrebbe gestire tutti i nomi di file possibili, non importa quanto strani.
Per ottenere il nome del file dallaltra parte, usiamo IFS= read -r -d ""
.
Dove abbiamo utilizzato read
sopra, abbiamo utilizzato il delimitatore di riga predefinito di newline, ma ora find
utilizza null come delimitatore di riga. In bash
, non puoi “passare un carattere NUL in un argomento a un comando (anche quelli incorporati), ma bash
capisce -d ""
significa delimitato da NUL . Quindi utilizziamo -d ""
per creare read
utilizza lo stesso delimitatore di riga di find
. Tieni presente che -d $"\0"
, incidentalmente, funziona anche, perché bash
che non supporta i byte NUL la considera come una stringa vuota.
Per essere corretti, aggiungiamo anche -r
, che dice di non gestire i backslash in nomi di file appositamente. Ad esempio, senza -r
, \<newline>
vengono rimossi e \n
viene convertito in n
.
Un modo più portatile di scrivere questo “t” che non richiede bash
o zsh
o ricordando tutte le regole precedenti sui byte nulli (di nuovo, grazie a Gilles):
find . -name "*.csv" -exec sh -c " file="$0" echo "$file" diff "$file" "/some/other/path/$file" read char </dev/tty " exec-sh {} ";"
* 3. Saltare le directory di cui i nomi terminano con .csv
find . -name "*.csv"
corrisponderanno anche a directory chiamate something.csv
.
Per evitare ciò, aggiungi -type f
al comando find
.
find . -type f -name "*.csv" -exec sh -c " file="$0" echo "$file" diff "$file" "/some/other/path/$file" read line </dev/tty " exec-sh {} ";"
Come glenn jackman sottolinea, in entrambi questi esempi, i comandi da eseguire per ogni file sono essendo eseguito in una subshell, quindi se modifichi delle variabili allinterno del ciclo, verranno dimenticate.
Se devi impostare le variabili e averle ancora impostate alla fine del ciclo, puoi riscriverlo per utilizzare la sostituzione del processo in questo modo:
i=0 while IFS= read -r -d "" file; do echo "file = $file" diff "$file" "/some/other/path/$file" read line </dev/tty i=$((i+1)) done < <(find . -type f -name "*.csv" -print0) echo "$i files processed"
Nota che se provi a copiarlo e incollarlo dalla riga di comando , read line
utilizzerà echo "$i files processed"
, in modo che il comando non venga eseguito.
Per evitare ciò, potrebbe rimuovere read line </dev/tty
e inviare il risultato a un cercapersone come less
.
NOTE
Ho rimosso i punti e virgola (;
) allinterno del ciclo continuo. Puoi rimetterli a posto se vuoi, ma non sono necessari.
Oggigiorno, $(command)
è più comune di `command`
. Ciò è principalmente dovuto al fatto che “è più facile scrivere $(command1 $(command2))
che `command1 \`command2\``
.
read char
non legge davvero un carattere.Legge unintera riga, quindi lho modificata in read line
.
Commenti
- mettendo
while
in una pipeline può creare problemi con la subshell creata (le variabili nel blocco loop non sono visibili dopo il completamento del comando per esempio). Con bash, utilizzerei il reindirizzamento dellinput e la sostituzione del processo:while read -r -d $'\0' file; do ...; done < <(find ... -print0)
- Certo, o utilizzando un heredoc:
while read; do; done <<EOF "$(find)" EOF
. Tuttavia, non è così facile da leggere. - @glenn jackman: ho provato ad aggiungere ulteriori spiegazioni proprio ora. Lho solo migliorato o peggiorato?
- Non ' bisogno di
IFS, -print0, while
eread
se gestisci completamentefind
, come mostrato di seguito nella mia soluzione. - La tua prima soluzione gestirà qualsiasi carattere tranne la nuova riga se disattivi anche il globbing con
set -f
.
Rispondi
Questo script non riesce se un nome file contiene spazi o caratteri di globbing della shell \[?*
. Il comando find
restituisce un nome file per riga. Quindi la sostituzione del comando `find …`
viene valutata dalla shell come segue:
- Esegui il comando
find
, prendine loutput. - Suddividi loutput di
find
in parole separate. Qualsiasi carattere di spazio è un separatore di parole. - Per ogni parola, se è un pattern globbing, espanderla nellelenco dei file a cui corrisponde.
Ad esempio, supponiamo che ci siano tre file nella directory corrente, denominati `foo* bar.csv
, foo 1.txt
e foo 2.txt
.
- Il comando
find
restituisce./foo* bar.csv
. - La shell divide questa stringa nello spazio, producendo due parole:
./foo*
ebar.csv
. - Poiché
./foo*
contiene un metacarattere globbing, viene espanso allelenco dei file corrispondenti:./foo 1.txt
e./foo 2.txt
. - Pertanto il ciclo
for
viene eseguito successivamente con./foo 1.txt
,./foo 2.txt
ebar.csv
.
Puoi evitare la maggior parte dei problemi in questa fase attenuando la suddivisione delle parole e ing off globbing. Per attenuare la suddivisione delle parole, imposta la variabile IFS
su un singolo carattere di nuova riga; in questo modo loutput di find
verrà diviso solo in corrispondenza di nuove righe e gli spazi rimarranno. Per disattivare il globbing, esegui set -f
. Quindi questa parte del codice funzionerà finché nessun nome di file contiene un carattere di nuova riga.
IFS=" " set -f for file in $(find . -name "*.csv"); do …
(Questo non fa parte del tuo problema, ma io consigliamo di utilizzare $(…)
su `…`
. Hanno lo stesso significato, ma la versione a rovescio ha strane regole di citazione.)
Cè un altro problema di seguito: diff $file /some/other/path/$file
dovrebbe essere
diff "$file" "/some/other/path/$file"
In caso contrario, il valore di $file
è diviso in parole e le parole sono trattate come schemi glob, come con il comando substitutio sopra. Se devi ricordare una cosa sulla programmazione della shell, ricorda questo: usa sempre le virgolette doppie attorno alle espansioni delle variabili ($foo
) e le sostituzioni dei comandi ( $(bar)
) , a meno che tu non sappia di voler dividere. (Sopra, sapevamo di voler dividere loutput di find
in righe.)
Un modo affidabile per chiamare find
gli dice di eseguire un comando per ogni file che trova:
find . -name "*.csv" -exec sh -c " echo "$0" diff "$0" "/some/other/path/$0" " {} ";"
In questo caso, un altro approccio è confrontare le due directory, anche se devi escludi esplicitamente tutti i file “noiosi”.
diff -r -x "*.txt" -x "*.ods" -x "*.pdf" … . /some/other/path
Commenti
- I ' avevo dimenticato i caratteri jolly come un altro motivo per citare correttamente. Grazie! 🙂
- invece di
find -exec sh -c 'cmd 1; cmd 2' ";"
, dovresti usarefind -exec cmd 1 {} ";" -exec cmd 2 {} ";"
, perché la shell deve mascherare i parametri, ma non trova ' t. Nel caso speciale qui, echo " $ 0 " ' non deve essere un parte dello script, aggiungi -print dopo';'
. Non hai ' incluso una domanda per procedere, ma anche questo può essere fatto da find, come mostrato di seguito nella mia anima. 😉 - @userunknown: luso di
{}
come sottostringa di un parametro infind -exec
non è portabile, ecco perché ' è necessaria la shell.Non ' t capisco cosa intendi con “la shell deve mascherare i parametri”; se ' riguarda le citazioni, la mia soluzione è citata correttamente. ' hai ragione che la parteecho
potrebbe essere eseguita da-print
.-okdir
è unestensione find GNU abbastanza recente, ' non è disponibile ovunque. Non ho ' incluso lattesa per procedere perché ritengo che linterfaccia utente estremamente scadente e il richiedente possano facilmente inserireread
nello snippet della shell se vuole. - La citazione è una forma di mascheramento, non è ' vero? Non ' t capisco la tua osservazione su cosa è portatile e cosa no. Il tuo esempio (il secondo dal basso) utilizza -exec per richiamare
sh
e utilizza{}
– quindi dovè il mio esempio (accanto a -okdir) meno portatile?find . -name "*.csv" -exec diff {} /some/other/path/{} ";" -print
- “Mascheramento” non è ' t terminologia comune nella letteratura sulla shell, quindi ' dovrò spiegare cosa intendi se vuoi essere capito. Il mio esempio utilizza
{}
solo una volta e in un argomento separato; altri casi (usati due volte o come sottostringa) non sono portabili. “Portatile” significa che ' funzionerà su tutti i sistemi unix; una buona linea guida è la specifica POSIX / Single Unix .
Answer
Sono “sorpreso di non vedere readarray
menzionato. Lo rende molto facile se usato in combinazione con <<<
:
$ touch oneword "two words" $ readarray -t files <<<"$(ls)" $ for file in "${files[@]}"; do echo "|$file|"; done |oneword| |two words|
Lutilizzo del costrutto <<<"$expansion"
consente anche di dividere le variabili contenenti le nuove righe in array, come :
$ string=$(dmesg) $ readarray -t lines <<<"$string" $ echo "${lines[0]}" [ 0.000000] Initializing cgroup subsys cpuset
readarray
è in Bash da anni ormai, quindi questo dovrebbe probabilmente essere il modo canonico di farlo questo in Bash.
Risposta
Afaik find ha tutto ciò di cui hai bisogno.
find . -okdir diff {} /some/other/path/{} ";"
find si prende cura di chiamare i programmi con cautela. -okdir ti chiederà prima del diff (sei sicuro di sì / no).
Nessuna shell coinvolta, nessun globbing, jolly, pi, pa, po.
Come nota a margine: se combini find con for / while / do / xargs, nella maggior parte dei casi, y lo stai sbagliando. 🙂
Commenti
- Grazie per la risposta. Perché stai sbagliando se combini find con for / while / do / xargs?
- Find itera già su un sottoinsieme di file. La maggior parte delle persone che si presentano con domande potrebbero semplicemente utilizzare una delle azioni (-ok (dir) -exec (dir), -delete) in combinazione con "; " o + (in seguito per invocazione parallela). La ragione principale per farlo è che non ' devi armeggiare con i parametri del file, mascherandoli per la shell. Non è così importante: non sono necessari ' t nuovi processi tutto il tempo, meno memoria, più velocità. programma più breve.
- Non qui per schiacciare il tuo spirito, ma confronta:
time find -type f -exec cat "{}" \;
contime find -type f -print0 | xargs -0 -I stuff cat stuff
. La versionexargs
era più veloce di 11 secondi durante lelaborazione di 10000 file vuoti. Fai attenzione quando affermi che nella maggior parte dei casi la combinazione difind
con altre utilità è sbagliata.-print0
e-0
sono lì per gestire gli spazi nei nomi dei file utilizzando uno zero byte come separatore di elementi anziché uno spazio. - @JonathanKomar: Il tuo comando find / exec ha impiegato 11,7 s sul mio sistema con 10.000 file, la versione xargs 9,7 s,
time find -type f -exec cat {} +
come suggerito nel mio commento precedente ha impiegato 0,1 S. Nota la sottile differenza tra " è sbagliato " e " tu ' sta sbagliando ", specialmente se decorato con una faccina. Ad esempio, hai sbagliato? 😉 A proposito, gli spazi nel nome del file non sono un problema per il comando precedente e trovano in generale. Programmatore cult del carico? E a proposito, combinare find con altri strumenti va bene, solo xargs è il più delle volte superflous. - @userunknown ho spiegato come il mio codice gestisce gli spazi per i posteri (educazione dei futuri spettatori), ed è stato non implica che il tuo codice non lo faccia. Il
+
per le chiamate parallele è molto veloce, come hai detto. Non direi programmatore cargo cult, perché questa capacità di usarexargs
in questo modo è utile in numerose occasioni. Sono più daccordo con la filosofia Unix: fai una cosa e fallo bene (usa i programmi separatamente o in combinazione per portare a termine un lavoro).find
sta camminando su una linea sottile lì.
Risposta
Fai scorrere tutti i file ( qualsiasi carattere speciale incluso) con il ricerca completamente sicura (vedi il link per la documentazione):
exec 9< <( find "$absolute_dir_path" -type f -print0 ) while IFS= read -r -d "" -u 9 do file_path="$(readlink -fn -- "$REPLY"; echo x)" file_path="${file_path%x}" echo "START${file_path}END" done
Commenti
- Grazie per aver menzionato
-d ''
. Non ' non mi ero reso conto che$'\0'
era uguale a''
, ma sembra essere. Anche una buona soluzione. - Mi piace il disaccoppiamento di find e while, grazie.
Answer
Sono” sorpreso che nessuno abbia menzionato lovvia zsh
soluzione qui ancora:
for file (**/*.csv(ND.)) { do-something-with $file }
((D)
per includere anche file nascosti, (N)
per evitare lerrore se “non cè corrispondenza, (.)
per limitare i file normali .)
bash4.3
e versioni successive ora lo supportano anche parzialmente:
shopt -s globstar nullglob dotglob for file in **/*.csv; do [ -f "$file" ] || continue [ -L "$file" ] && continue do-something-with "$file" done
Risposta
I nomi di file con spazi sembrano più nomi sulla riga di comando, se ” non è citato. Se il tuo file si chiama “Hello World.txt”, la riga del diff si espande in:
diff Hello World.txt /some/other/path/Hello World.txt
che assomiglia a quattro nomi di file. virgolette attorno agli argomenti:
diff "$file" "/some/other/path/$file"
Commenti
- Questo aiuta ma non ' t risolvere il mio problema. Vedo ancora casi in cui il file viene suddiviso in più token.
- Questa risposta è fuorviante. Il problema è il comando
for file in `find . -name "*.csv"`
. Se è presente un file denominatoHello World.csv
,file
verrà impostato su./Hello
e quindi suWorld.csv
. Citando$file
' t aiuto.
Risposta
La doppia citazione è tua amica.
diff "$file" "/some/other/path/$file"
Altrimenti i contenuti della variabile vengono suddivisi in parole.
Commenti
- Questo è fuorviante. Il problema è il comando
for file in `find . -name "*.csv"`
. Se è presente un file chiamatoHello World.csv
,file
verrà impostato su./Hello
e quindi suWorld.csv
. La citazione$file
non ha ' t aiuto.
Risposta
Con bash4, puoi anche utilizzare la funzione mapfile incorporata per impostare un array contenente ogni riga e iterare su questo array.
$ tree . ├── a │ ├── a 1 │ └── a 2 ├── b │ ├── b 1 │ └── b 2 └── c ├── c 1 └── c 2 3 directories, 6 files $ mapfile -t files < <(find -type f) $ for file in "${files[@]}"; do > echo "file: $file" > done file: ./a/a 2 file: ./a/a 1 file: ./b/b 2 file: ./b/b 1 file: ./c/c 2 file: ./c/c 1
Risposta
Gli spazi nei valori possono essere evitati con un semplice costrutto ciclo for
for CHECK_STR in `ls -l /root/somedir` do echo "CHECKSTR $CHECK_STR" done
ls -l root / somedir c mantiene il mio file con spazi
Output di sopra il mio file con spazi
per evitare questo output, soluzione semplice (notare le virgolette doppie)
for CHECK_STR in "`ls -l /root/somedir`" do echo "CHECKSTR $CHECK_STR" done
visualizza il mio file con spazi
provato su bash
Commenti
- “Looping through files “- questo è ciò che dice la domanda. La tua soluzione produrrà l intero
ls -l
output in una volta . È effettivamente equivalente aecho "CHECKSTR `ls -l /root/somedir`"
.