216 votes

Bash: itère la liste des fichiers avec des espaces

Je veux parcourir une liste de fichiers. Cette liste est le résultat d'une commande find , j'ai donc trouvé:

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

C'est bien sauf si un fichier a des espaces dans son nom:

 $ 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
 

Comment puis-je faire pour éviter la séparation des espaces?

271voto

martin clayton Points 41306

Vous pouvez remplacer l'itération basée sur les mots par une itération basée sur une ligne:

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

154voto

Sorpigal Points 10412

Il y a plusieurs réalisable façons de le faire.

Si vous voulais rester au plus près de votre version d'origine, il pourrait être fait de cette façon:

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

Cela va encore échouer si les noms de fichier ont littérale des retours à la ligne dans les, mais les espaces ne pas le casser.

Cependant, de jouer avec les FI n'est pas nécessaire. Voici ma façon préférée de faire ceci:

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

Si vous trouvez l' < <(command) de la syntaxe inconnu vous devriez lire à propos du processus de substitution. L'avantage de cette de plus de for file in $(find ...) , c'est que des fichiers avec des espaces, retours à la ligne et d'autres caractères sont traités correctement. Cela fonctionne parce qu' find avec -print0 va utiliser un null (aka \0), comme le terminateur de chaque nom de fichier et, à la différence de saut de ligne, la valeur null n'est pas un caractère dans un nom de fichier.

L'avantage de ce système au cours de la presque l'équivalent de la version

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

C'est que toute affectation de variable dans le corps de la boucle while est préservée. C'est, si vous pipe while comme ci-dessus puis le corps de l' while est dans un shell interne est exécuté, qui peut ne pas être ce que vous voulez.

L'avantage de ce processus de substitution version plus find ... -print0 | xargs -0 est minime: L' xargs version est très bien si vous avez besoin d'imprimer une ligne ou d'effectuer une opération sur le fichier, mais si vous devez effectuer plusieurs étapes de la boucle version est plus facile.

EDIT: Voici un beau script de test de sorte que vous pouvez obtenir une idée de la différence entre les différentes tentatives de résolution de ce problème

#!/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"

31voto

mrchlblng Points 637

Il existe également une solution très simple: compter sur la globalisation de bash

 $ 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'
 

Notez que je ne suis pas sûr que ce comportement soit le comportement par défaut, mais je ne vois aucun paramètre spécial dans mon shopt.

13voto

Karoly Horvath Points 45145
find . -iname "foo*" -print0 | xargs -L1 -0 echo "File found:"

11voto

Torp Points 5216
 find . -name "fo*" -print0 | xargs -0 ls -l
 

Voir man xargs .

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X