web-dev-qa-db-fra.com

Itérer sur une liste de fichiers avec des espaces

Je veux parcourir une liste de fichiers. Cette liste est le résultat d'une commande find. Je suis donc venu avec:

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

Que puis-je faire pour éviter la scission sur les espaces?

177
gregseth

Vous pouvez remplacer l'itération basée sur Word par une autre basée sur les lignes:

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

Il existe plusieurs moyens pratiques d'y parvenir. 

Si vous souhaitez vous en tenir à votre version originale, procédez comme suit:

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

Cela échouera quand même si les noms de fichiers contiennent des retours à la ligne, mais les espaces ne le sépareront pas.

Cependant, jouer avec IFS n'est pas nécessaire. Voici ma manière 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 la syntaxe < <(command) inconnue, vous devriez en savoir plus sur process substitution . L'avantage de cette fonction sur for file in $(find ...) est que les fichiers avec des espaces, des nouvelles lignes et d'autres caractères sont correctement gérés. Cela fonctionne parce que find avec -print0 utilisera une null (alias \0) comme terminateur pour chaque nom de fichier et, contrairement à newline, null n’est pas un caractère légal dans un nom de fichier.

L'avantage de ceci par rapport à la version presque équivalente

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

Est-ce que toute affectation de variable dans le corps de la boucle while est préservée? En d’autres termes, si vous dirigez vers while comme ci-dessus, le corps de while se trouve dans un sous-shell, ce qui peut ne pas être ce que vous voulez.

L'avantage de la version de substitution de processus par rapport à find ... -print0 | xargs -0 est minime: la version xargs convient si vous avez uniquement besoin d'imprimer une ligne ou d'effectuer une seule opération sur le fichier, mais si vous devez effectuer plusieurs étapes, la version de la boucle est plus simple.

EDIT: Voici un joli script de test vous permettant de vous faire une idée de la différence entre 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"
146
Sorpigal

Il existe également une solution très simple: compter sur 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'

Notez que je ne suis pas sûr que ce comportement est le comportement par défaut, mais je ne vois pas de paramètre spécial dans mon shopt. Je voudrais donc dire qu'il devrait être "sûr" (testé sur osx et ubuntu).

27
marchelbling
find . -iname "foo*" -print0 | xargs -L1 -0 echo "File found:"
12
Karoly Horvath
find . -name "fo*" -print0 | xargs -0 ls -l

Voir man xargs.

11
Torp

Puisque vous ne faites aucun autre type de filtrage avec find, vous pouvez utiliser les éléments suivants à partir de bash 4.0:

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

Le **/ correspondra à zéro ou plusieurs répertoires, ainsi le modèle complet correspondra à foo* dans le répertoire actuel ou dans tout sous-répertoire.

6
chepner

J'aime beaucoup les boucles et les itérations sur les tableaux, alors je pense que je vais ajouter cette réponse au mélange ...

J'ai aussi aimé l'exemple stupide du fichier de Marchchelbling. :)

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

Dans le répertoire de test:

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

Ceci ajoute chaque ligne de liste de fichiers dans un tableau bash nommé arr avec toute nouvelle ligne de fin supprimée.

Disons que nous voulons donner à ces fichiers de meilleurs noms ...

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

$ {! arr [@]} passe à 0 1 2, donc "$ {arr [$ i]}" est le i ème élément du tableau. Les guillemets autour des variables sont importants pour préserver les espaces.

Le résultat est trois fichiers renommés:

$ ls -1
smarter_file1
smarter_file2
smarter_file_3
2
terafl0ps

find a un argument -exec qui boucle sur les résultats de la recherche et exécute une commande arbitraire. Par exemple:

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

Ici, {} représente les fichiers trouvés et son encapsulation dans "" permet à la commande Shell résultante de traiter les espaces dans le nom du fichier. 

Dans de nombreux cas, vous pouvez remplacer ce dernier \; (qui lance une nouvelle commande) par un \+, ce qui placera plusieurs fichiers dans la même commande (mais pas nécessairement tous en même temps, voir man find pour plus de détails).

0
naught101

Dans certains cas, ici si vous avez juste besoin de copier ou de déplacer une liste de fichiers, vous pouvez également diriger cette liste vers awk.
Important le \"" "\" autour du champ $0 (bref, vos fichiers, une liste de lignes = un fichier).

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

Ok - mon premier post sur Stack Overflow!

Bien que mes problèmes avec cela aient toujours été dans csh, je ne suis pas sûr que la solution que je vais présenter fonctionne dans les deux cas. Le problème est lié à l'interprétation par Shell des retours "ls". Nous pouvons supprimer "ls" du problème en utilisant simplement l'extension Shell du caractère générique * - mais cela donne une erreur "aucune correspondance" s'il n'y a aucun fichier dans le dossier actuel (ou dans le dossier spécifié) - pour contourner ce problème, nous étendons simplement le développement pour inclure des fichiers point ainsi: * .* - cela donnera toujours des résultats depuis les fichiers. et .. sera toujours présent. Donc, en csh, nous pouvons utiliser cette construction ...

foreach file (* .*)
   echo $file
end

si vous voulez filtrer les fichiers dot standard, alors c'est assez simple ...

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

Le code dans le premier post sur ce fil serait écrit ainsi: -

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

J'espère que cela t'aides!

0
Andy Foster

Une autre solution pour faire le travail ...

Le but était:

  • sélectionner/filtrer les noms de fichiers de manière récursive dans des répertoires
  • gérer chaque nom (quel que soit l'espace dans le chemin ...)
#!/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}


0
Vince B