Comment analyser la sortie de la commande find lorsque les noms de fichiers contiennent des espaces?

12

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?

Scott C Wilson
la source
1
Notez que les fichiers peuvent également avoir des retours à la ligne dans leur nom de fichier. Voilà pourquoi il y a find -print0et xargs -0.
Daniel Beck

Réponses:

12

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 $ilors de son utilisation, afin d'éviter d'autres choses d'interprétation ultérieure des espaces. N'oubliez pas également de $IFSreculer 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 whileboucle peut avoir lieu dans un sous-shell, selon le shell exact que vous utilisez, donc les paramètres variables peuvent ne pas persister. La forversion en boucle évite cela, mais au prix que, même si vous appliquez la $IFSsolution pour éviter les problèmes d'espace, vous aurez alors des ennuis si le findretourne 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.

geekosaure
la source
1
J'aime l'idée d'utiliser simplement Python pour éviter tout cela.
Scott C Wilson
12

Utilisez-le find -print0et dirigez-le vers xargs -0, ou écrivez votre propre petit programme C et dirigez-le vers votre petit programme C. C'est pour cela -print0et ils -0ont é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.

DW
la source
Fonctionne sur ma machine ^ TM!
mcandre
2

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 IFSaprè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 -execavec la findcommande, soit à l'utiliser -print0et à la diriger vers xargs -0. Dans le premier cas, findle nom de fichier s'échappe. Dans le -print0cas, findimprime sa sortie avec un séparateur nul, puis xargsse 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 forboucle complète .

Daniel Andersson
la source
1

Utilisation find -print0avecxargs -0

L'utilisation find -print0combinée avec xargs -0est 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 echocommande. Parce que nous avons spécifié l' -n 1option, xargsne passera qu'un seul argument à la fois à echo. Si nous avions omis cette option, nous en xargsaurions transmis autant que possible echo. (Vous pouvez echo short input | xargs --show-limitsvoir combien d'octets sont autorisés dans une ligne de commande.)

Que fait xargsexactement?

Nous pouvons clairement voir l'effet xargsa sur son entrée - et l'effet de -nen 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
jpaugh
la source
1

Je ne suis pas d'accord avec les bashbashers, 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, findvous 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\; `)

Peter.O
la source
1
Les deux perspectives sont valables. Lorsque je ne travaillais que sur mes propres fichiers, j'utilisais simplement find et je ne m'en fais pas, car mes fichiers n'ont pas d'espaces (ou de retours chariot!) Dans leurs noms. Mais lorsque vous commencez à travailler avec des fichiers d'autres personnes, vous devez utiliser des techniques plus robustes.
Scott C Wilson