Come posso sfuggire allo spazio bianco in un elenco di loop bash?


121

Ho uno script di shell bash che esegue il ciclo di tutte le directory figlio (ma non dei file) di una determinata directory. Il problema è che alcuni dei nomi di directory contengono spazi.

Ecco i contenuti della mia directory di test:

$ls -F test
Baltimore/  Cherry Hill/  Edison/  New York City/  Philadelphia/  cities.txt

E il codice che scorre nelle directory:

for f in `find test/* -type d`; do
  echo $f
done

Ecco l'output:

test / Baltimora
test / Cherry
collina
test / Edison 
test / Nuovo
York
Città
test / Philadelphia

Cherry Hill e New York City sono trattate come 2 o 3 voci separate.

Ho provato a citare i nomi dei file, in questo modo:

for f in `find test/* -type d | sed -e 's/^/\"/' | sed -e 's/$/\"/'`; do
  echo $f
done

ma inutilmente.

Deve esserci un modo semplice per farlo.


Le risposte di seguito sono ottime. Ma per rendere tutto più complicato, non voglio sempre utilizzare le directory elencate nella mia directory di test. A volte desidero invece passare i nomi delle directory come parametri della riga di comando.

Ho seguito il suggerimento di Charles di impostare l'IFS e ho pensato quanto segue:

dirlist="${@}"
(
  [[ -z "$dirlist" ]] && dirlist=`find test -mindepth 1 -type d` && IFS=$'\n'
  for d in $dirlist; do
    echo $d
  done
)

e questo funziona bene a meno che non ci siano spazi negli argomenti della riga di comando (anche se questi argomenti sono tra virgolette). Ad esempio, chiamando lo script in questo modo: test.sh "Cherry Hill" "New York City"produce il seguente output:

ciliegia
collina
Nuovo
York
Città

re: edit, list="$@"scarta completamente la list-ness del valore originale, riducendolo in una stringa. Per favore, segui le pratiche nella mia risposta esattamente come fornite - un tale incarico non è incoraggiato da nessuna parte in essa; se vuoi passare un elenco di argomenti della riga di comando a un programma, dovresti raccoglierli in un array ed espandere quell'array direttamente.
Charles Duffy

Risposte:


105

Primo, non farlo in questo modo. L'approccio migliore è usare find -execcorrettamente:

# this is safe
find test -type d -exec echo '{}' +

L'altro approccio sicuro consiste nell'utilizzare l'elenco terminato da NUL, sebbene ciò richieda che il supporto per la ricerca -print0:

# this is safe
while IFS= read -r -d '' n; do
  printf '%q\n' "$n"
done < <(find test -mindepth 1 -type d -print0)

Puoi anche popolare un array da find e passare quell'array in un secondo momento:

# this is safe
declare -a myarray
while IFS= read -r -d '' n; do
  myarray+=( "$n" )
done < <(find test -mindepth 1 -type d -print0)
printf '%q\n' "${myarray[@]}" # printf is an example; use it however you want

Se la tua ricerca non supporta -print0, il tuo risultato non è sicuro: quanto segue non si comporterà come desiderato se esistono file contenenti nuove righe nei loro nomi (il che, sì, è legale):

# this is unsafe
while IFS= read -r n; do
  printf '%q\n' "$n"
done < <(find test -mindepth 1 -type d)

Se non si intende utilizzare uno dei precedenti, un terzo approccio (meno efficiente in termini sia di tempo che di utilizzo della memoria, poiché legge l'intero output del sottoprocesso prima di eseguire la suddivisione delle parole) consiste nell'usare una IFSvariabile che non non contengono il carattere spazio. Spegnere globbing ( set -f) per evitare stringhe contenenti caratteri glob quali [], *o ?di essere ampliato:

# this is unsafe (but less unsafe than it would be without the following precautions)
(
 IFS=$'\n' # split only on newlines
 set -f    # disable globbing
 for n in $(find test -mindepth 1 -type d); do
   printf '%q\n' "$n"
 done
)

Infine, per il caso del parametro della riga di comando, dovresti usare gli array se la tua shell li supporta (cioè è ksh, bash o zsh):

# this is safe
for d in "$@"; do
  printf '%s\n' "$d"
done

manterrà la separazione. Nota che la citazione (e l'uso di $@piuttosto che $*) è importante. Gli array possono essere popolati anche in altri modi, come le espressioni glob:

# this is safe
entries=( test/* )
for d in "${entries[@]}"; do
  printf '%s\n' "$d"
done

1
non sapevo di quel sapore '+' per -exec. dolce
Johannes Schaub - litb

1
anche se sembra che possa anche, come xargs, inserire gli argomenti solo alla fine del comando dato: / a volte mi ha
infastidito

Penso che -exec [nome] {} + sia un'estensione GNU e 4.4-BSD. (Almeno, non appare su Solaris 8, e non penso nemmeno che fosse in AIX 4.3.) Immagino che il resto di noi potrebbe essere bloccato con il
collegamento

2
Non ho mai visto la sintassi $ '\ n' prima. Come funziona? (Avrei pensato che IFS = '\ n' o IFS = "\ n" funzionassero, ma nessuno dei due funziona.)
MCS

1
@crosstalk è sicuramente in Solaris 10, l'ho appena usato.
Nick

26
find . -type d | while read file; do echo $file; done

Tuttavia, non funziona se il nome del file contiene delle nuove righe. Quanto sopra è l'unica soluzione che conosco quando si desidera effettivamente avere il nome della directory in una variabile. Se vuoi solo eseguire qualche comando, usa xargs.

find . -type d -print0 | xargs -0 echo 'The directory is: '

Non c'è bisogno di xargs, vedi find -exec ... {} +
Charles Duffy

4
@Charles: per un gran numero di file, xargs è molto più efficiente: genera solo un processo. L'opzione -exec crea un nuovo processo per ogni file, che può essere un ordine di grandezza più lento.
Adam Rosenfield

1
Mi piacciono di più gli xargs. Questi due sembrano essenzialmente fare lo stesso entrambi, mentre xargs ha più opzioni, come correre in parallelo
Johannes Schaub - litb

2
Adam, no, quel "+" aggregherà il maggior numero di nomi di file possibile e poi verrà eseguito. ma non avrà funzioni così
precise

2
Nota che se vuoi fare qualcosa con i nomi dei file, dovrai citarli. Ad esempio:find . -type d | while read file; do ls "$file"; done
David Moles

23

Ecco una semplice soluzione che gestisce le tabulazioni e / o gli spazi bianchi nel nome del file. Se hai a che fare con altri strani caratteri nel nome del file come le nuove righe, scegli un'altra risposta.

La directory di test

ls -F test
Baltimore/  Cherry Hill/  Edison/  New York City/  Philadelphia/  cities.txt

Il codice da inserire nelle directory

find test -type d | while read f ; do
  echo "$f"
done

Il nome del file deve essere tra virgolette ( "$f") se usato come argomento. Senza virgolette, gli spazi fungono da separatore di argomenti e al comando richiamato vengono forniti più argomenti.

E l'output:

test/Baltimore
test/Cherry Hill
test/Edison
test/New York City
test/Philadelphia

grazie, questo ha funzionato per l'alias che stavo creando per elencare quanto spazio sta usando ciascuna directory nella cartella corrente, si stava soffocando su alcune directory con spazi nella precedente incarnazione. Questo funziona in zsh, ma alcune delle altre risposte no:alias duc='ls -d * | while read D; do du -sh "$D"; done;'
Ted Naleid il

2
Se stai usando zsh, puoi anche farlo:alias duc='du -sh *(/)'
cbliard

@cbliard Questo è ancora bacato. Prova a eseguirlo con un nome di file, ad esempio, una sequenza di tabulazioni o più spazi; noterai che cambia qualcuno di questi in un singolo spazio, perché non stai citando nel tuo eco. E poi c'è il caso di nomi di file contenenti nuove righe ...
Charles Duffy

@CharlesDuffy ho provato con sequenze di tabulazioni e spazi multipli. Funziona con le virgolette. Ho provato anche con le nuove righe e non funziona affatto. Ho aggiornato la risposta di conseguenza. Grazie per averlo fatto notare.
cbliard

1
@cbliard Giusto - l'aggiunta di virgolette al tuo comando echo era quello che stavo arrivando. Per quanto riguarda le nuove righe, puoi farlo funzionare usando find -print0e IFS='' read -r -d '' f.
Charles Duffy

7

Questo è estremamente complicato in Unix standard, e la maggior parte delle soluzioni non funziona con le nuove righe o con qualche altro carattere. Tuttavia, se stai usando il set di strumenti GNU, puoi sfruttare l' findopzione -print0e usarla xargscon l'opzione corrispondente -0(meno zero). Ci sono due caratteri che non possono apparire in un semplice nome di file; quelli sono barra e NUL "\ 0". Ovviamente, la barra appare nei nomi di percorso, quindi la soluzione GNU di utilizzare un NUL '\ 0' per contrassegnare la fine del nome è ingegnosa e infallibile.


4

Perché non solo mettere

IFS='\n'

davanti al comando for? Questo cambia il separatore di campo da <Spazio> <Tab> <Nuova riga> a solo <Nuova riga>


4
find . -print0|while read -d $'\0' file; do echo "$file"; done

1
-d $'\0'è esattamente lo stesso di -d ''- poiché bash usa stringhe terminate con NUL, il primo carattere di una stringa vuota è un NUL e per lo stesso motivo, i NUL non possono essere rappresentati all'interno di stringhe C.
Charles Duffy

4

Io uso

SAVEIFS=$IFS
IFS=$(echo -en "\n\b")
for f in $( find "$1" -type d ! -path "$1" )
do
  echo $f
done
IFS=$SAVEIFS

Non sarebbe abbastanza?
Idea tratta da http://www.cyberciti.biz/tips/handling-filenames-with-spaces-in-bash.html


ottimo consiglio: è molto utile per le opzioni di un osascript da riga di comando (OS X AppleScript), dove gli spazi dividono un argomento in più parametri dove solo uno è inteso
tim

No, non è abbastanza. È inefficiente (a causa dell'uso non necessario di $(echo ...)), non gestisce correttamente i nomi di file con espressioni glob, non gestisce correttamente i nomi di file che contengono i caratteri $'\b'o $ '\ n' e inoltre converte più sequenze di spazi bianchi in singoli caratteri di spazio bianco sul lato di uscita a causa di quotazioni errate.
Charles Duffy

4

Non memorizzare elenchi come stringhe; memorizzali come array per evitare tutta questa confusione di delimitatori. Ecco uno script di esempio che funzionerà su tutte le sottodirectory di test o sull'elenco fornito sulla sua riga di comando:

#!/bin/bash
if [ $# -eq 0 ]; then
        # if no args supplies, build a list of subdirs of test/
        dirlist=() # start with empty list
        for f in test/*; do # for each item in test/ ...
                if [ -d "$f" ]; then # if it's a subdir...
                        dirlist=("${dirlist[@]}" "$f") # add it to the list
                fi
        done
else
        # if args were supplied, copy the list of args into dirlist
        dirlist=("$@")
fi
# now loop through dirlist, operating on each one
for dir in "${dirlist[@]}"; do
        printf "Directory: %s\n" "$dir"
done

Ora proviamo su una directory di test con una o due curve inserite:

$ ls -F test
Baltimore/
Cherry Hill/
Edison/
New York City/
Philadelphia/
this is a dirname with quotes, lfs, escapes: "\''?'?\e\n\d/
this is a file, not a directory
$ ./test.sh 
Directory: test/Baltimore
Directory: test/Cherry Hill
Directory: test/Edison
Directory: test/New York City
Directory: test/Philadelphia
Directory: test/this is a dirname with quotes, lfs, escapes: "\''
'
\e\n\d
$ ./test.sh "Cherry Hill" "New York City"
Directory: Cherry Hill
Directory: New York City

1
Guardando indietro a questo, in realtà c'era una soluzione con POSIX sh: potresti riutilizzare l' "$@"array, aggiungendolo con set -- "$@" "$f".
Charles Duffy,

4

È possibile utilizzare IFS (separatore di campo interno) temporaneamente utilizzando:

OLD_IFS=$IFS     # Stores Default IFS
IFS=$'\n'        # Set it to line break
for f in `find test/* -type d`; do
    echo $f
done

$IFS=$OLD_IFS


Si prega di fornire una spiegazione.
Steve K

IFS ha specificato quale sia il simbolo separatore, quindi il nome del file con uno spazio bianco non verrebbe troncato.
stupefacente il

$ IFS = $ OLD_IFS alla fine dovrebbe essere: IFS = $ OLD_IFS
Michel Donais

3

ps se si tratta solo di spazio nell'input, alcune virgolette doppie hanno funzionato senza problemi per me ...

read artist;

find "/mnt/2tb_USB_hard_disc/p_music/$artist" -type f -name *.mp3 -exec mpg123 '{}' \;

2

Per aggiungere a ciò che ha detto Jonathan : usa l' -print0opzione per findinsieme a xargsquanto segue:

find test/* -type d -print0 | xargs -0 command

Ciò eseguirà il comando commandcon gli argomenti appropriati; le directory con spazi verranno citate correttamente (ovvero verranno passate come un argomento).


1
#!/bin/bash

dirtys=()

for folder in *
do    
 if [ -d "$folder" ]; then    
    dirtys=("${dirtys[@]}" "$folder")    
 fi    
done    

for dir in "${dirtys[@]}"    
do    
   for file in "$dir"/\*.mov   # <== *.mov
   do    
       #dir_e=`echo "$dir" | sed 's/[[:space:]]/\\\ /g'`   -- This line will replace each space into '\ '   
       out=`echo "$file" | sed 's/\(.*\)\/\(.*\)/\2/'`     # These two line code can be written in one line using multiple sed commands.    
       out=`echo "$out" | sed 's/[[:space:]]/_/g'`    
       #echo "ffmpeg -i $out_e -sameq -vcodec msmpeg4v2 -acodec pcm_u8 $dir_e/${out/%mov/avi}"    
       `ffmpeg -i "$file" -sameq -vcodec msmpeg4v2 -acodec pcm_u8 "$dir"/${out/%mov/avi}`    
   done    
done

Il codice precedente convertirà i file .mov in .avi. I file .mov si trovano in cartelle diverse ei nomi delle cartelle hanno spazi bianchi . Il mio script sopra convertirà i file .mov in file .avi nella stessa cartella stessa. Non so se vi aiuti, popoli.

Astuccio:

[sony@localhost shell_tutorial]$ ls
Chapter 01 - Introduction  Chapter 02 - Your First Shell Script
[sony@localhost shell_tutorial]$ cd Chapter\ 01\ -\ Introduction/
[sony@localhost Chapter 01 - Introduction]$ ls
0101 - About this Course.mov   0102 - Course Structure.mov
[sony@localhost Chapter 01 - Introduction]$ ./above_script
 ... successfully executed.
[sony@localhost Chapter 01 - Introduction]$ ls
0101_-_About_this_Course.avi  0102_-_Course_Structure.avi
0101 - About this Course.mov  0102 - Course Structure.mov
[sony@localhost Chapter 01 - Introduction]$ CHEERS!

Saluti!


echo "$name" | ...non funziona se nameè -n, e come si comporta con i nomi con sequenze backslash-escape dipende dalla tua implementazione - POSIX rende il comportamento echoin quel caso esplicitamente indefinito (mentre POSIX esteso XSI rende l'espansione delle sequenze backslash-escape standard definito e sistemi GNU - incluso bash - senza POSIXLY_CORRECT=1infrangere lo standard POSIX implementandolo -e(mentre le specifiche richiedono echo -edi stampare -ein output). printf '%s\n' "$name" | ...è più sicuro.
Charles Duffy,

1

Doveva avere a che fare anche con gli spazi bianchi nei nomi di percorso. Quello che ho finalmente fatto è stato usare una ricorsione e for item in /path/*:

function recursedir {
    local item
    for item in "${1%/}"/*
    do
        if [ -d "$item" ]
        then
            recursedir "$item"
        else
            command
        fi
    done
}

1
Non usare la functionparola chiave: rende il tuo codice incompatibile con POSIX sh, ma non ha altri scopi utili. Puoi semplicemente definire una funzione con recursedir() {, aggiungendo le due parentesi e rimuovendo la parola chiave function, e questo sarà compatibile con tutte le shell compatibili con POSIX.
Charles Duffy

1

Converti l'elenco dei file in un array Bash. Questo utilizza l'approccio di Matt McClure per restituire un array da una funzione Bash: http://notes-matthewlmcclure.blogspot.com/2009/12/return-array-from-bash-function-v-2.html Il risultato è un modo per convertire qualsiasi input multilinea in un array Bash.

#!/bin/bash

# This is the command where we want to convert the output to an array.
# Output is: fileSize fileNameIncludingPath
multiLineCommand="find . -mindepth 1 -printf '%s %p\\n'"

# This eval converts the multi-line output of multiLineCommand to a
# Bash array. To convert stdin, remove: < <(eval "$multiLineCommand" )
eval "declare -a myArray=`( arr=(); while read -r line; do arr[${#arr[@]}]="$line"; done; declare -p arr | sed -e 's/^declare -a arr=//' ) < <(eval "$multiLineCommand" )`"

for f in "${myArray[@]}"
do
   echo "Element: $f"
done

Questo approccio sembra funzionare anche quando sono presenti caratteri non validi ed è un modo generale per convertire qualsiasi input in un array Bash. Lo svantaggio è che se l'input è lungo potresti superare i limiti di dimensione della riga di comando di Bash o utilizzare grandi quantità di memoria.

Gli approcci in cui il ciclo che alla fine sta funzionando sulla lista ha anche la lista inserita in pipe hanno lo svantaggio che leggere lo stdin non è facile (come chiedere l'input all'utente), e il ciclo è un nuovo processo quindi potresti chiederti perché le variabili impostati all'interno del ciclo non sono disponibili al termine del ciclo.

Inoltre non mi piace impostare IFS, può rovinare altro codice.


Se si utilizza IFS='' read, sulla stessa riga, l'impostazione IFS è presente solo per il comando di lettura e non ne esce. Non c'è motivo di non gradire l'impostazione IFS in questo modo.
Charles Duffy

1

Bene, vedo troppe risposte complicate. Non voglio passare l'output dell'utility find o scrivere un ciclo, perché find ha l'opzione "exec" per questo.

Il mio problema era che volevo spostare tutti i file con estensione dbf nella cartella corrente e alcuni di essi contenevano spazi bianchi.

L'ho affrontato così:

 find . -name \*.dbf -print0 -exec mv '{}'  . ';'

Mi sembra molto semplice


0

ho appena scoperto che ci sono alcune somiglianze tra la mia domanda e la tua. A prima vista se vuoi passare argomenti nei comandi

test.sh "Cherry Hill" "New York City"

per stamparli in ordine

for SOME_ARG in "$@"
do
    echo "$SOME_ARG";
done;

notare che $ @ è circondato da virgolette doppie, alcune note qui


0

Avevo bisogno dello stesso concetto per comprimere sequenzialmente più directory o file da una determinata cartella. Ho risolto usando awk per parsel l'elenco da ls e per evitare il problema dello spazio vuoto nel nome.

source="/xxx/xxx"
dest="/yyy/yyy"

n_max=`ls . | wc -l`

echo "Loop over items..."
i=1
while [ $i -le $n_max ];do
item=`ls . | awk 'NR=='$i'' `
echo "File selected for compression: $item"
tar -cvzf $dest/"$item".tar.gz "$item"
i=$(( i + 1 ))
done
echo "Done!!!"

cosa ne pensi?


Penso che questo non funzionerà correttamente se i nomi dei file contengono una nuova riga. Forse dovresti provarlo.
user000001


-3

Per me funziona ed è praticamente "pulito":

for f in "$(find ./test -type d)" ; do
  echo "$f"
done

4
Ma questo è peggio. Le virgolette doppie attorno alla ricerca fanno sì che tutti i nomi di percorso vengano concatenati come una singola stringa. Cambia l' eco in un ls per vedere il problema.
NVRAM

-4

Ho appena avuto un semplice problema di variante ... Converti file di .flv digitato in .mp3 (sbadiglio).

for file in read `find . *.flv`; do ffmpeg -i ${file} -acodec copy ${file}.mp3;done

trova ricorsivamente tutti i file flash degli utenti Macintosh e trasformali in audio (copia, no transcodifica) ... è come nel frattempo, notando che read invece di solo 'for file in ' scapperà.


2
Il readdopo inè un'altra parola nell'elenco su cui stai ripetendo. Quello che hai pubblicato è una versione leggermente rotta di ciò che aveva il richiedente, il che non funziona. Potresti aver intenzione di postare qualcosa di diverso, ma probabilmente è comunque coperto da altre risposte qui.
Gilles "SO- smettila di essere malvagio"
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.