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?
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
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"
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).
find . -iname "foo*" -print0 | xargs -L1 -0 echo "File found:"
find . -name "fo*" -print0 | xargs -0 ls -l
Voir man xargs
.
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.
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
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).
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" }'
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!
Une autre solution pour faire le travail ...
Le but était:
#!/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}