Utiliser une boucle telle que
for i in `find . -name \*.txt`
se cassera si certains noms de fichiers contiennent des espaces.
Quelle technique puis-je utiliser pour éviter ce problème?
Utiliser une boucle telle que
for i in `find . -name \*.txt`
se cassera si certains noms de fichiers contiennent des espaces.
Quelle technique puis-je utiliser pour éviter ce problème?
Réponses:
Idéalement, vous ne le faites pas du tout, car analyser correctement les noms de fichiers dans un script shell est toujours difficile (corrigez-le pour les espaces, vous aurez toujours des problèmes avec d'autres caractères incorporés, en particulier les sauts de ligne). Ceci est même répertorié comme la première entrée de la page BashPitfalls.
Cela dit, il existe un moyen de faire presque ce que vous voulez:
oIFS=$IFS
IFS=$'\n'
find . -name '*.txt' | while read -r i; do
# use "$i" with whatever you're doing
done
IFS=$oIFS
N'oubliez pas de citer également $i
lors de son utilisation, afin d'éviter d'autres choses d'interprétation ultérieure des espaces. N'oubliez pas également de $IFS
reculer après l'avoir utilisé, car ne pas le faire entraînera des erreurs déroutantes plus tard.
Cela a une autre mise en garde attachée: ce qui se passe à l'intérieur de la while
boucle peut avoir lieu dans un sous-shell, selon le shell exact que vous utilisez, donc les paramètres variables peuvent ne pas persister. La for
version en boucle évite cela, mais au prix que, même si vous appliquez la $IFS
solution pour éviter les problèmes d'espace, vous aurez alors des ennuis si le find
retourne trop de fichiers.
À un moment donné, le correctif correct pour tout cela devient de le faire dans un langage tel que Perl ou Python au lieu de shell.
Utilisez-le find -print0
et dirigez-le vers xargs -0
, ou écrivez votre propre petit programme C et dirigez-le vers votre petit programme C. C'est pour cela -print0
et ils -0
ont été inventés.
Les scripts shell ne sont pas le meilleur moyen de gérer les noms de fichiers avec des espaces: vous pouvez le faire, mais cela devient maladroit.
Vous pouvez définir le "séparateur de champ interne" ( IFS
) sur autre chose que de l'espace pour la division des arguments de boucle, par exemple
ORIGIFS=${IFS}
NL='
'
IFS=${NL}
for i in $(find . -name '*.txt'); do
IFS=${ORIGIFS}
#do stuff
done
IFS=${ORIGIFS}
Je réinitialise IFS
après son utilisation dans la recherche, principalement parce qu'il a l'air sympa, je pense. Je n'ai vu aucun problème à le régler sur newline, mais je pense que c'est "plus propre".
Une autre méthode, en fonction de ce que vous voulez faire avec la sortie find
, consiste soit à l'utiliser directement -exec
avec la find
commande, soit à l'utiliser -print0
et à la diriger vers xargs -0
. Dans le premier cas, find
le nom de fichier s'échappe. Dans le -print0
cas, find
imprime sa sortie avec un séparateur nul, puis xargs
se divise sur cela. Puisqu'aucun nom de fichier ne peut contenir ce caractère (ce que je sais), c'est toujours aussi sûr. Cela est surtout utile dans les cas simples; et n'est généralement pas un excellent substitut pour une for
boucle complète .
find -print0
avecxargs -0
L'utilisation find -print0
combinée avec xargs -0
est complètement robuste contre les noms de fichiers légaux et est l'une des méthodes les plus extensibles disponibles. Par exemple, supposons que vous vouliez une liste de tous les fichiers PDF du répertoire actuel. Tu pourrais écrire
$ find . -iname '*.pdf' -print0 | xargs -0 -n 1 echo
Ceci trouvera chaque PDF (via -iname '*.pdf'
) dans le répertoire courant ( .
) et n'importe quel sous-répertoire, et passera chacun d'eux en argument à la echo
commande. Parce que nous avons spécifié l' -n 1
option, xargs
ne passera qu'un seul argument à la fois à echo
. Si nous avions omis cette option, nous en xargs
aurions transmis autant que possible echo
. (Vous pouvez echo short input | xargs --show-limits
voir combien d'octets sont autorisés dans une ligne de commande.)
xargs
exactement?Nous pouvons clairement voir l'effet xargs
a sur son entrée - et l'effet de -n
en particulier - en utilisant un script qui fait écho à ses arguments d'une manière plus précise que echo
.
$ cat > echoArgs.sh <<'EOF'
#!/bin/bash
echo "Number of arguments: $#"
[[ $# -eq 0 ]] && exit
for i in $(seq 1 $#); do
echo "Arg $i: <$1>"
shift
done
EOF
$ find . -iname '*.pdf' -print0 | xargs -0 ./echoArgs.sh
$ find . -iname '*.pdf' -print0 | xargs -0 -n 1 ./echoArgs.sh
Notez qu'il gère parfaitement les espaces et les nouvelles lignes,
$ touch 'A space-age
new line of vending machines.pdf'
$ find . -iname '*space*' -print0 | xargs -0 -n 1 ./echoArgs.sh
ce qui serait particulièrement gênant avec la solution commune suivante:
chmod +x ./echoArgs.sh
for file in $(ls *spacey*); do
./echoArgs.sh "$file"
done
Remarques
Je ne suis pas d'accord avec les bash
bashers, car bash
, avec l'ensemble d'outils * nix, est tout à fait apte à gérer les fichiers (y compris ceux dont les noms ont des espaces intégrés).
En fait, find
vous donne un contrôle fin sur le choix des fichiers à traiter ... Du côté bash, vous n'avez vraiment besoin de réaliser que vous devez faire de vos chaînes bash words
; généralement en utilisant des "guillemets doubles", ou un autre mécanisme comme l'utilisation d'IFS, ou de trouver{}
Notez que dans la plupart des situations, vous n'avez pas besoin de définir et de réinitialiser IFS; utilisez simplement IFS localement comme indiqué dans les exemples ci-dessous. Les trois poignées permettent de bien espacer les espaces. De plus, vous n'avez pas besoin d'une structure de boucle "standard", car la recherche \;
est en fait une boucle; mettez simplement votre logique de boucle dans une fonction bash (si vous n'appelez pas un outil standard).
IFS=$'\n' find ~/ -name '*.txt' -exec function-or-util {} \;
Et, deux autres exemples
IFS=$'\n' find ~/ -name '*.txt' -exec printf 'Hello %s\n' {} \;
IFS=$'\n' find ~/ -name '*.txt' -exec echo {} \+ |sed 's/home//'
'trouver also allows you to pass multiple filenames as args to you script ..(if it suits your need: use
+ instead
\; `)
find -print0
etxargs -0
.