Scorrere su un elenco di file con spazi


201

Voglio scorrere su un elenco di file. Questo elenco è il risultato di un findcomando, quindi ho trovato:

getlist() {
  for f in $(find . -iname "foo*")
  do
    echo "File found: $f"
    # do something useful
  done
}

Va bene tranne se un file ha spazi nel suo nome:

$ ls
foo_bar_baz.txt
foo bar baz.txt

$ getlist
File found: foo_bar_baz.txt
File found: foo
File found: bar
File found: baz.txt

Cosa posso fare per evitare la divisione degli spazi?


Questa è sostanzialmente una sottocassa specifica di Quando avvolgere le virgolette attorno a una variabile shell?
Tripleee

Risposte:


253

È possibile sostituire l'iterazione basata su parole con una iterazione basata su riga:

find . -iname "foo*" | while read f
do
    # ... loop body
done

31
Questo è estremamente pulito. E mi fa sentire più bello che cambiare IFS in congiunzione con un ciclo for
Derrick il

15
Questo dividerà un singolo percorso di file che contiene un \ n. OK, quelli non dovrebbero essere in giro ma possono essere creati:touch "$(printf "foo\nbar")"
Ollie Saunders,

4
Per impedire qualsiasi interpretazione dell'input (barre rovesciate, spazi iniziali e finali), utilizzare IFS= while read -r finvece.
mklement0,

2
Questa risposta mostra una combinazione più sicura di finde un ciclo while.
moi,

5
Sembra che sottolineando l'ovvio, ma in quasi tutti i casi semplici, -execsta per essere più pulita di un ciclo esplicito: find . -iname "foo*" -exec echo "File found: {}" \;. Inoltre, in molti casi puoi sostituirlo \;con l' ultimo +per mettere molti file in un solo comando.
naught101

152

Esistono diversi modi praticabili per ottenere questo risultato.

Se si desidera attenersi strettamente alla versione originale, è possibile farlo in questo modo:

getlist() {
        IFS=$'\n'
        for file in $(find . -iname 'foo*') ; do
                printf 'File found: %s\n' "$file"
        done
}

Ciò continuerà a fallire se i nomi dei file contengono letteralmente nuove righe, ma gli spazi non lo spezzeranno.

Tuttavia, non è necessario scherzare con IFS. Ecco il mio modo preferito per farlo:

getlist() {
    while IFS= read -d $'\0' -r file ; do
            printf 'File found: %s\n' "$file"
    done < <(find . -iname 'foo*' -print0)
}

Se trovi la < <(command)sintassi sconosciuta, dovresti leggere sulla sostituzione del processo . Il vantaggio di questo for file in $(find ...)è che i file con spazi, newline e altri caratteri vengono gestiti correttamente. Questo funziona perché findcon -print0verrà utilizzato un null(aka\0 ) come terminatore per ogni nome di file e, a differenza di newline, null non è un carattere legale in un nome di file.

Il vantaggio di questo rispetto alla versione quasi equivalente

getlist() {
        find . -iname 'foo*' -print0 | while read -d $'\0' -r file ; do
                printf 'File found: %s\n' "$file"
        done
}

È che qualsiasi assegnazione variabile nel corpo del ciclo while sia preservata. Cioè, se si esegue il pipe whilecome sopra, quindi il corpo diwhile è in una subshell che potrebbe non essere quello che si desidera.

Il vantaggio della versione di sostituzione del processo find ... -print0 | xargs -0è minimo: la xargsversione va bene se tutto ciò che serve è stampare una riga o eseguire una singola operazione sul file, ma se è necessario eseguire più passaggi la versione del ciclo è più semplice.

EDIT : Ecco un bel script di test in modo da poter farti un'idea della differenza tra diversi tentativi di risolvere questo problema

#!/usr/bin/env bash

dir=/tmp/getlist.test/
mkdir -p "$dir"
cd "$dir"

touch       'file not starting foo' foo foobar barfoo 'foo with spaces'\
    'foo with'$'\n'newline 'foo with trailing whitespace      '

# while with process substitution, null terminated, empty IFS
getlist0() {
    while IFS= read -d $'\0' -r file ; do
            printf 'File found: '"'%s'"'\n' "$file"
    done < <(find . -iname 'foo*' -print0)
}

# while with process substitution, null terminated, default IFS
getlist1() {
    while read -d $'\0' -r file ; do
            printf 'File found: '"'%s'"'\n' "$file"
    done < <(find . -iname 'foo*' -print0)
}

# pipe to while, newline terminated
getlist2() {
    find . -iname 'foo*' | while read -r file ; do
            printf 'File found: '"'%s'"'\n' "$file"
    done
}

# pipe to while, null terminated
getlist3() {
    find . -iname 'foo*' -print0 | while read -d $'\0' -r file ; do
            printf 'File found: '"'%s'"'\n' "$file"
    done
}

# for loop over subshell results, newline terminated, default IFS
getlist4() {
    for file in "$(find . -iname 'foo*')" ; do
            printf 'File found: '"'%s'"'\n' "$file"
    done
}

# for loop over subshell results, newline terminated, newline IFS
getlist5() {
    IFS=$'\n'
    for file in $(find . -iname 'foo*') ; do
            printf 'File found: '"'%s'"'\n' "$file"
    done
}


# see how they run
for n in {0..5} ; do
    printf '\n\ngetlist%d:\n' $n
    eval getlist$n
done

rm -rf "$dir"

1
Ha accettato la tua risposta: la più completa e interessante - di cui non sapevo $IFSe la < <(cmd)sintassi. Rimane ancora una cosa oscura per me, perché l' $in $'\0'? Molte grazie.
Gregreg

2
+1, ma dovresti aggiungere ... while IFS= read... per gestire i file che iniziano o finiscono con spazi bianchi.
Gordon Davisson,

1
C'è un avvertimento per la soluzione di sostituzione del processo. Se hai qualche prompt all'interno del loop (o stai leggendo da STDIN in qualsiasi altro modo), l'input sarà riempito dalle cose che inserisci nel loop. (forse questo dovrebbe essere aggiunto alla risposta?)
Andsens

2
@uvsmtid: questa domanda è stata taggata, bashquindi mi sono sentita al sicuro usando le funzionalità specifiche di bash. La sostituzione del processo non è portabile con altre shell (è probabile che sh stesso non riceva mai un aggiornamento così significativo).
sorpigale,

2
La combinazione IFS=$'\n'con forimpedisce la suddivisione delle parole all'interno della linea, ma rende comunque le linee risultanti soggette a globbing, quindi questo approccio non è completamente robusto (a meno che non si disattivi anche prima il globbing). Mentre read -d $'\0'funziona, è leggermente fuorviante in quanto suggerisce che è possibile utilizzare $'\0'per creare NUL - non è possibile: a \0in una stringa tra virgolette ANSI termina effettivamente la stringa, quindi -d $'\0'è effettivamente la stessa -d ''.
mklement0,

29

Esiste anche una soluzione molto semplice: affidarsi al bash globbing

$ mkdir test
$ cd test
$ touch "stupid file1"
$ touch "stupid file2"
$ touch "stupid   file 3"
$ ls
stupid   file 3  stupid file1     stupid file2
$ for file in *; do echo "file: '${file}'"; done
file: 'stupid   file 3'
file: 'stupid file1'
file: 'stupid file2'

Nota che non sono sicuro che questo comportamento sia quello predefinito ma non vedo alcuna impostazione speciale nel mio negozio, quindi vorrei dire che dovrebbe essere "sicuro" (testato su osx e ubuntu).


13
find . -iname "foo*" -print0 | xargs -L1 -0 echo "File found:"

6
come nota a margine, funzionerà solo se si desidera eseguire un comando. Un built-in della shell non funzionerà in questo modo.
Alex,

11
find . -name "fo*" -print0 | xargs -0 ls -l

Vedere man xargs.


6

Dal momento che non stai eseguendo nessun altro tipo di filtro con find, puoi utilizzare quanto segue a partire da bash4.0:

shopt -s globstar
getlist() {
    for f in **/foo*
    do
        echo "File found: $f"
        # do something useful
    done
}

Il **/corrisponderà a zero o più directory, in modo che il pattern completo corrisponderà foo*nella directory corrente e in ogni sottodirectory.


3

Mi piace molto per i loop e l'iterazione dell'array, quindi immagino che aggiungerò questa risposta al mix ...

Mi è piaciuto anche lo stupido esempio di file di marchelbling. :)

$ mkdir test
$ cd test
$ touch "stupid file1"
$ touch "stupid file2"
$ touch "stupid   file 3"

All'interno della directory di test:

readarray -t arr <<< "`ls -A1`"

Ciò aggiunge ogni riga dell'elenco dei file in un array bash chiamato arrcon qualsiasi nuova riga finale rimossa.

Diciamo che vogliamo dare a questi file nomi migliori ...

for i in ${!arr[@]}
do 
    newname=`echo "${arr[$i]}" | sed 's/stupid/smarter/; s/  */_/g'`; 
    mv "${arr[$i]}" "$newname"
done

$ {! arr [@]} espande a 0 1 2 in modo "$ {arr [$ i]}" è l'i esimo elemento dell'array. Le virgolette attorno alle variabili sono importanti per preservare gli spazi.

Il risultato sono tre file rinominati:

$ ls -1
smarter_file1
smarter_file2
smarter_file_3

2

findha un -execargomento che passa in rassegna i risultati di ricerca ed esegue un comando arbitrario. Per esempio:

find . -iname "foo*" -exec echo "File found: {}" \;

Qui {}rappresenta i file trovati e li avvolge"" consente al comando della shell risultante di gestire gli spazi nel nome del file.

In molti casi è possibile sostituire l'ultimo \;(che avvia un nuovo comando) con un \+, che inserirà più file in un comando (non necessariamente tutti contemporaneamente, vedere man findper maggiori dettagli).


0

In alcuni casi, qui se hai solo bisogno di copiare o spostare un elenco di file, puoi anche reindirizzare tale elenco su awk.
Importante \"" "\"tutto il campo $0(in breve i tuoi file, un elenco di righe = un file).

find . -iname "foo*" | awk '{print "mv \""$0"\" ./MyDir2" | "sh" }'

0

Ok - il mio primo post su Stack Overflow!

Anche se i miei problemi con questo sono sempre stati in csh, non bash la soluzione che presento funzionerà in entrambi. Il problema è con l'interpretazione della shell dei ritorni "ls". Possiamo rimuovere "ls" dal problema semplicemente usando l'espansione della shell del *carattere jolly - ma questo dà un errore "nessuna corrispondenza" se non ci sono file nella cartella corrente (o specificata) - per aggirare questo semplicemente estendiamo il espansione per includere dot-file così: * .*- questo produrrà sempre risultati dal momento che i file. e .. sarà sempre presente. Quindi in csh possiamo usare questo costrutto ...

foreach file (* .*)
   echo $file
end

se vuoi filtrare i file dot standard allora è abbastanza facile ...

foreach file (* .*)
   if ("$file" == .) continue
   if ("file" == ..) continue
   echo $file
end

Il codice nel primo post su questa discussione verrebbe scritto così: -

getlist() {
  for f in $(* .*)
  do
    echo "File found: $f"
    # do something useful
  done
}

Spero che questo ti aiuti!


0

Un'altra soluzione per lavoro ...

L'obiettivo era:

  • seleziona / filtra i nomi dei file in modo ricorsivo nelle directory
  • gestire ogni nome (qualunque sia lo spazio nel percorso ...)
#!/bin/bash  -e
## @Trick in order handle File with space in their path...
OLD_IFS=${IFS}
IFS=$'\n'
files=($(find ${INPUT_DIR} -type f -name "*.md"))
for filename in ${files[*]}
do
      # do your stuff
      #  ....
done
IFS=${OLD_IFS}


Grazie per un'osservazione costruttiva, ma: 1- questo è un problema reale, 2- shell potrebbe essersi evoluta nel tempo ... come tutti presumo; 3- Nessuna risposta sopra potrebbe soddisfare una risoluzione DIRETTA del pb senza cambiare il problema o dissertare :-)
Vince B
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.