Vous parcourez des fichiers avec des espaces dans les noms? [dupliquer]

Cette question a déjà des réponses ici :

Commentaires

  • Je ne suis pas daccord avec le fait que ce serait un doublon. La réponse acceptée explique comment boucler sur les noms de fichiers avec des espaces; cela na rien à voir avec " pourquoi une boucle sur la sortie de ' s est une mauvaise pratique ". Jai trouvé cette question (pas lautre) car jai besoin de boucler sur les noms de fichiers avec des espaces, comme dans: for file in $ LIST_OF_FILES; do … où $ LIST_OF_FILES nest pas la sortie de find; il ' est juste une liste de noms de fichiers (séparés par des retours à la ligne).
  • @CarloWood – les noms de fichiers peuvent inclure des retours à la ligne, votre question est donc plutôt unique: boucle sur une liste de noms de fichiers pouvant contenir des espaces mais pas de sauts de ligne. Je pense que vous ' allez devoir utiliser la technique IFS, pour indiquer que la rupture se produit à ' \ n '
  • @ Diagonwoah, je nai jamais réalisé que les noms de fichiers peuvent contenir des retours à la ligne. Jutilise principalement (uniquement) linux / UNIX et là même les espaces sont rares; Je nai certainement jamais vu de toute ma vie utiliser des nouvelles lignes: p. Ils pourraient tout aussi bien interdire cela à mon humble avis.
  • @CarloWood – les noms de fichiers se terminent par un nul (' \ 0 ' , identique à ' '). Tout le reste est acceptable.
  • @CarloWood Vous devez vous rappeler que les gens votent en premier et lisent ensuite …

Réponse

Réponse courte (la plus proche de votre réponse, mais gère les espaces)

OIFS="$IFS" IFS=$"\n" for file in `find . -type f -name "*.csv"` do echo "file = $file" diff "$file" "/some/other/path/$file" read line done IFS="$OIFS" 

Meilleure réponse (gère également les caractères génériques et les retours à la ligne dans les noms de fichiers)

find . -type f -name "*.csv" -print0 | while IFS= read -r -d "" file; do echo "file = $file" diff "$file" "/some/other/path/$file" read line </dev/tty done 

Meilleure réponse (basée sur Gilles  » answer )

find . -type f -name "*.csv" -exec sh -c " file="$0" echo "$file" diff "$file" "/some/other/path/$file" read line </dev/tty " exec-sh {} ";" 

Ou mieux encore, pour éviter den exécuter un sh par fichier:

find . -type f -name "*.csv" -exec sh -c " for file do echo "$file" diff "$file" "/some/other/path/$file" read line </dev/tty done " exec-sh {} + 

Réponse longue

Vous avez trois problèmes:

  1. Par défaut, le shell divise la sortie dune commande sur les espaces, les tabulations et les retours à la ligne
  2. Les noms de fichiers peuvent contenir des caractères génériques serait développé
  3. Et sil y avait un répertoire dont le nom se termine par *.csv?

1. Fractionnement uniquement sur les retours à la ligne

Pour savoir à quoi définir file, le shell doit prendre la sortie de find et linterpréter dune manière ou dune autre, sinon file ne serait que la sortie entière de find .

Le shell lit la variable IFS, qui est définie sur <space><tab><newline> par défaut.

Ensuite, il regarde chaque caractère dans la sortie de find. Dès quil voit un caractère qui « est dans IFS, il pense que cela marque la fin du nom de fichier, il définit donc file à nimporte quel caractère quil a vu jusquà présent et exécute la boucle. Ensuite, il commence là où il sest arrêté pour obtenir le nom de fichier suivant, et exécute la boucle suivante, etc., jusquà ce quil atteigne la fin de la sortie.

Donc, il fait effectivement ceci:

for file in "zquery" "-" "abc" ... 

Pour lui dire de ne diviser lentrée que sur les nouvelles lignes, vous devez faire

IFS=$"\n" 

avant votre commande for ... find.

Cela définit IFS sur un retour à la ligne unique, donc il ne se divise que sur les nouvelles lignes, et non sur les espaces et les tabulations.

Si vous utilisez sh ou dash au lieu de ksh93, bash ou zsh, vous devez écrire IFS=$"\n" comme ceci à la place:

IFS=" " 

Cest probablement suffisant pour faire fonctionner votre script, mais si vous « êtes intéressé à gérer correctement dautres cas de coin, lisez la suite …

2. Extension de $file sans caractères génériques

Dans la boucle où vous faites

diff $file /some/other/path/$file 

le shell essaie de développer $file (encore une fois!).

Il pourrait contenir des espaces, mais puisque nous avons déjà défini IFS ci-dessus, cela ne sera pas un problème ici.

Mais il peut également contenir des caractères génériques tels que * ou ?, ce qui entraînerait un comportement imprévisible. (Merci à Gilles de lavoir signalé.)

Pour dire au shell de ne pas développer les caractères génériques, mettez la variable entre guillemets doubles, par exemple

diff "$file" "/some/other/path/$file" 

Le même problème pourrait aussi nous mordre

for file in `find . -name "*.csv"` 

Par exemple, si vous aviez ces trois fichiers

file1.csv file2.csv *.csv 

(très peu probable, mais toujours possible)

Ce serait comme si vous aviez exécuté

for file in file1.csv file2.csv *.csv 

qui sera étendu à

for file in file1.csv file2.csv *.csv file1.csv file2.csv 

provoquant file1.csv et file2.csv à traiter deux fois.

À la place, nous devons faire

find . -name "*.csv" -print | while IFS= read -r file; do echo "file = $file" diff "$file" "/some/other/path/$file" read line </dev/tty done 

read lit les lignes à partir de lentrée standard, divise la ligne en mots selon IFS et les stocke dans les noms de variables que vous spécifiez.

Ici, nous le disons ne pas diviser la ligne en mots et stocker la ligne dans $file.

Notez également que est devenu read line </dev/tty.

En effet, à lintérieur de la boucle, lentrée standard provient de find via le pipeline.

Si nous faisions simplement read, cela consommerait une partie ou la totalité dun nom de fichier, et certains fichiers seraient ignorés .

/dev/tty est le terminal à partir duquel lutilisateur exécute le script. Notez que cela provoquera une erreur si le script est exécuté via cron, mais je suppose que ce nest pas important dans ce cas.

Alors, que se passe-t-il si un nom de fichier contient des nouvelles lignes?

Nous pouvons gérer cela en remplaçant -print par -print0 et en utilisant read -d "" à la fin dun pipeline:

find . -name "*.csv" -print0 | while IFS= read -r -d "" file; do echo "file = $file" diff "$file" "/some/other/path/$file" read char </dev/tty done 

Cela fait find mettre un octet nul à la fin de chaque nom de fichier. Les octets nuls sont les seuls caractères non autorisés dans les noms de fichiers, donc cela devrait gérer tous les noms de fichiers possibles, aussi bizarres soient-ils.

Pour obtenir le nom de fichier de lautre côté, nous utilisons IFS= read -r -d "".

Là où nous avons utilisé read ci-dessus, nous avons utilisé le délimiteur de ligne par défaut de nouvelle ligne, mais maintenant, find utilise null comme délimiteur de ligne. Dans bash, vous ne pouvez « pas passer un caractère NUL dans un argument à une commande (même intégrée), mais bash comprend -d "" comme signifiant NUL délimité . Nous utilisons donc -d "" pour créer read utilisez le même délimiteur de ligne que find. Notez que -d $"\0", dailleurs, fonctionne également, car bash ne prenant pas en charge les octets NUL le traite comme une chaîne vide.

Pour être correct, nous ajoutons également -r, qui dit ne pas gérer les barres obliques inverses dans noms de fichiers spécialement. Par exemple, sans -r, \<newline> sont supprimés et \n est converti en n.

Une manière plus portable décrire ceci qui ne nécessite pas bash ou zsh ou en se souvenant de toutes les règles ci-dessus concernant les octets nuls (encore une fois, merci à Gilles):

find . -name "*.csv" -exec sh -c " file="$0" echo "$file" diff "$file" "/some/other/path/$file" read char </dev/tty " exec-sh {} ";" 

* 3. Sauter les répertoires dont les noms se terminant par .csv

find . -name "*.csv" 

correspondront également aux répertoires appelés something.csv.

Pour éviter cela, ajoutez -type f à la commande find.

find . -type f -name "*.csv" -exec sh -c " file="$0" echo "$file" diff "$file" "/some/other/path/$file" read line </dev/tty " exec-sh {} ";" 

Comme le souligne glenn jackman , dans ces deux exemples, les commandes à exécuter pour chaque fichier sont étant exécuté dans un sous-shell, donc si vous modifiez des variables à lintérieur de la boucle, elles seront oubliées.

Si vous avez besoin de définir des variables et de les avoir toujours définies à la fin de la boucle, vous pouvez la réécrire pour utiliser la substitution de processus comme ceci:

i=0 while IFS= read -r -d "" file; do echo "file = $file" diff "$file" "/some/other/path/$file" read line </dev/tty i=$((i+1)) done < <(find . -type f -name "*.csv" -print0) echo "$i files processed" 

Notez que si vous essayez de copier-coller ceci sur la ligne de commande , read line consommera le echo "$i files processed", de sorte que la commande ne sera pas exécutée.

Pour éviter cela, vous pourrait supprimer read line </dev/tty et envoyer le résultat à un pager comme less.


REMARQUES

Jai supprimé les points-virgules (;) à lintérieur du boucle. Vous pouvez les remettre si vous le souhaitez, mais ils ne sont pas nécessaires.

De nos jours, $(command) est plus courant que `command`. Cest principalement parce quil « est plus facile décrire $(command1 $(command2)) que `command1 \`command2\``.

read char ne lit pas vraiment un caractère.Il lit une ligne entière donc je lai changé en read line.

Commentaires

  • mettant while dans un pipeline peut créer des problèmes avec le sous-shell créé (les variables dans le bloc de boucle ne sont pas visibles une fois la commande terminée par exemple). Avec bash, jutiliserais la redirection dentrée et la substitution de processus: while read -r -d $'\0' file; do ...; done < <(find ... -print0)
  • Bien sûr, ou en utilisant un heredoc: while read; do; done <<EOF "$(find)" EOF . Pas si facile à lire cependant.
  • @glenn jackman: Jai essayé dajouter plus dexplications tout à lheure. Est-ce que je viens de laméliorer ou de lempirer?
  • Vous navez ' pas besoin de IFS, -print0, while et read si vous gérez find au maximum, comme indiqué ci-dessous dans ma solution.
  • Votre première solution prendra en charge nimporte quel caractère sauf le saut de ligne si vous désactivez également le glissement avec set -f.

Réponse

Ce script échoue si un nom de fichier contient des espaces ou des caractères globuleux du shell \[?*. La commande find génère un nom de fichier par ligne. Ensuite, la commande de substitution `find …` est évaluée par le shell comme suit:

  1. Exécutez la commande find, saisissez sa sortie.
  2. Divisez la sortie find en mots séparés. Tout caractère despacement est un séparateur de mot.
  3. Pour chaque mot, sil sagit dun motif de globulation, développez-le jusquà la liste des fichiers auxquels il correspond.

Par exemple, supposons quil y ait trois fichiers dans le répertoire courant, appelés `foo* bar.csv, foo 1.txt et foo 2.txt.

  1. La commande find renvoie ./foo* bar.csv.
  2. Le shell divise cette chaîne à lespace, produisant deux mots: ./foo* et bar.csv.
  3. Depuis ./foo* contient un métacaractère globuleux, il est étendu à la liste des fichiers correspondants: ./foo 1.txt et ./foo 2.txt.
  4. Par conséquent, la boucle for est exécutée successivement avec ./foo 1.txt, ./foo 2.txt et bar.csv.

Vous pouvez éviter la plupart des problèmes à ce stade en atténuant la division des mots et en ing hors globbing. Pour réduire la division des mots, définissez la variable IFS sur un seul caractère de nouvelle ligne; de cette façon, la sortie de find ne sera divisée quaux sauts de ligne et les espaces resteront. Pour désactiver la globalisation, exécutez set -f. Ensuite, cette partie du code fonctionnera tant quaucun nom de fichier ne contient de caractère de nouvelle ligne.

IFS=" " set -f for file in $(find . -name "*.csv"); do … 

(Cela ne fait pas partie de votre problème, mais je recommande dutiliser $(…) sur `…`. Ils ont la même signification, mais la version backquote a des règles de citation étranges.)

Il ya un autre problème ci-dessous: diff $file /some/other/path/$file devrait être

diff "$file" "/some/other/path/$file" 

Sinon, la valeur de $file est divisé en mots et les mots sont traités comme des motifs globaux, comme avec la commande substitutio ci-dessus. Si vous devez vous rappeler une chose à propos de la programmation shell, rappelez-vous ceci: utilisez toujours des guillemets doubles autour des extensions de variables ($foo) et des substitutions de commandes ( $(bar)) , sauf si vous savez que vous souhaitez fractionner. (Ci-dessus, nous savions que nous voulions diviser la sortie find en lignes.)

Un moyen fiable dappeler find lui dit dexécuter une commande pour chaque fichier quil trouve:

find . -name "*.csv" -exec sh -c " echo "$0" diff "$0" "/some/other/path/$0" " {} ";" 

Dans ce cas, une autre approche consiste à comparer les deux répertoires, bien que vous deviez exclure explicitement tous les fichiers «ennuyeux».

diff -r -x "*.txt" -x "*.ods" -x "*.pdf" … . /some/other/path 

Commentaires

  • I ' Jai oublié les jokers comme autre raison de citer correctement. Merci! 🙂
  • au lieu de find -exec sh -c 'cmd 1; cmd 2' ";", vous devez utiliser find -exec cmd 1 {} ";" -exec cmd 2 {} ";", car le shell doit masquer les paramètres, mais ne trouvez pas ' t. Dans le cas particulier ici, echo " $ 0 " ne ' doit être un partie du script, ajoutez simplement -print après ';'. Vous navez pas ' inclure une question pour continuer, mais même cela peut être fait par find, comme indiqué ci-dessous dans mon soulution. 😉
  • @userunknown: Lutilisation de {} comme sous-chaîne dun paramètre dans find -exec nest pas portable, que ' explique pourquoi le shell est nécessaire.Je ne ' pas comprendre ce que vous entendez par «le shell doit masquer les paramètres»; si ' s sur la citation, ma solution est correctement citée. Vous ' avez raison que la partie echo puisse être effectuée par -print à la place. -okdir est une extension GNU find assez récente, elle ' nest pas disponible partout. Je nai ' t inclure lattente pour continuer parce que je considère que linterface utilisateur extrêmement pauvre et le demandeur peut facilement mettre read dans lextrait de code si il veut.
  • La citation est une forme de masquage, nest-ce pas '? Je ne ' pas comprendre votre remarque sur ce qui est portable et ce qui ne l’est pas. Votre exemple (2e à partir du bas) utilise -exec pour invoquer sh et utilise {} – alors où est mon exemple (à côté de -okdir) moins portable? find . -name "*.csv" -exec diff {} /some/other/path/{} ";" -print
  • « Masking » isn ' t terminologie courante dans la littérature shell, donc vous ' Je devrai expliquer ce que vous voulez dire si vous voulez être compris. Mon exemple utilise {} une seule fois et dans un argument séparé; les autres cas (utilisés deux fois ou comme sous-chaîne) ne sont pas portables. «Portable» signifie quil ' fonctionnera sur tous les systèmes Unix; une bonne directive est la Spécification POSIX / Single Unix .

Réponse

Je « suis surpris de ne pas voir readarray mentionné. Cela rend cela très facile lorsquil est utilisé en combinaison avec le <<< opérateur:

$ touch oneword "two words" $ readarray -t files <<<"$(ls)" $ for file in "${files[@]}"; do echo "|$file|"; done |oneword| |two words| 

Lutilisation de la construction <<<"$expansion" vous permet également de diviser les variables contenant des retours à la ligne en tableaux, comme :

$ string=$(dmesg) $ readarray -t lines <<<"$string" $ echo "${lines[0]}" [ 0.000000] Initializing cgroup subsys cpuset 

readarray est dans Bash depuis des années maintenant, donc cela devrait probablement être la manière canonique de faire ceci dans Bash.

Réponse

Afaik find a tout ce dont vous avez besoin.

find . -okdir diff {} /some/other/path/{} ";" 

find se charge dappeler les programmes en toute sécurité. -okdir vous invitera avant le diff (êtes-vous sûr que oui / non).

Aucun shell impliqué, pas de globbing, jokers, pi, pa, po.

En guise de remarque: si vous combinez find avec for / while / do / xargs, dans la plupart des cas, y Vous le faites mal. 🙂

Commentaires

  • Merci pour la réponse. Pourquoi le faites-vous mal si vous combinez find avec for / while / do / xargs?
  • Find itère déjà sur un sous-ensemble de fichiers. La plupart des personnes qui se présentent avec des questions pourraient simplement utiliser lune des actions (-ok (dir) -exec (dir), -delete) en combinaison avec "; " ou + (plus tard pour un appel parallèle). La principale raison de le faire est que vous n’avez pas ' à manipuler les paramètres de fichier, en les masquant pour le shell. Pas si important que cela: vous avez besoin de ' t nouveaux processus tout le temps, moins de mémoire, plus de vitesse. programme plus court.
  • Pas ici pour écraser votre esprit, mais comparez: time find -type f -exec cat "{}" \; avec time find -type f -print0 | xargs -0 -I stuff cat stuff. La version xargs était plus rapide de 11 secondes lors du traitement de 10 000 fichiers vides. Soyez prudent lorsque vous affirmez que dans la plupart des cas, la combinaison de find avec dautres utilitaires est erronée. -print0 et -0 sont là pour gérer les espaces dans les noms de fichiers en utilisant un octet zéro comme séparateur délément plutôt quun espace.
  • @JonathanKomar: Votre commande find / exec a pris 11,7 s sur mon système avec 10 000 fichiers, la version xargs 9.7 s, time find -type f -exec cat {} + comme suggéré dans mon commentaire précédent a pris 0,1 s. Notez la différence subtile entre " cest faux " et " vous ' vous faites une erreur ", surtout lorsquil est décoré dun smiley. Avez-vous, par exemple, fait une erreur? 😉 BTW, les espaces dans le nom de fichier ne posent aucun problème pour la commande ci-dessus et trouvent en général. Programmeur culte du fret? Et au fait, combiner la recherche avec dautres outils est très bien, juste xargs est la plupart du temps superfleux.
  • @userunknown Jai expliqué comment mon code traite des espaces pour la postérité (éducation des futurs téléspectateurs), et était nimpliquant pas que votre code ne le fait pas. Le + pour les appels parallèles est très rapide, comme vous lavez mentionné. Je ne dirais pas de programmeur culte du fret, car cette capacité à utiliser xargs de cette manière est utile à de nombreuses reprises. Je suis plus daccord avec la philosophie Unix: faire une chose et bien la faire (utiliser les programmes séparément ou en combinaison pour faire un travail). find y marche sur une ligne fine.

Réponse

Parcourez tous les fichiers ( tout caractère spécial inclus) avec le recherche complètement sûre (voir le lien pour la documentation):

exec 9< <( find "$absolute_dir_path" -type f -print0 ) while IFS= read -r -d "" -u 9 do file_path="$(readlink -fn -- "$REPLY"; echo x)" file_path="${file_path%x}" echo "START${file_path}END" done 

Commentaires

  • Merci davoir mentionné -d ''. Je nai ' pas réalisé que $'\0' était identique à '', mais il semble être. Bonne solution aussi.
  • Jaime le découplage de find et while, merci.

Answer

Je » suis surpris que personne nait encore mentionné la solution zsh évidente ici:

for file (**/*.csv(ND.)) { do-something-with $file } 

((D) pour inclure également les fichiers cachés, (N) pour éviter lerreur sil « ny a pas de correspondance, (.) pour se limiter aux fichiers normaux .)

bash4.3 et les versions ultérieures le prennent désormais partiellement en charge:

shopt -s globstar nullglob dotglob for file in **/*.csv; do [ -f "$file" ] || continue [ -L "$file" ] && continue do-something-with "$file" done 

Réponse

Les noms de fichiers avec des espaces en eux ressemblent à plusieurs noms sur la ligne de commande sils  » nest pas entre guillemets. Si votre fichier est nommé « Hello World.txt », la ligne de diff se développe en:

diff Hello World.txt /some/other/path/Hello World.txt 

qui ressemble à quatre noms de fichier. Il suffit de mettre guillemets autour des arguments:

diff "$file" "/some/other/path/$file" 

Commentaires

  • Cela aide mais cela ne fonctionne pas ' t résoudre mon problème. Je vois toujours des cas où le fichier est divisé en plusieurs jetons.
  • Cette réponse est trompeuse. Le problème est la commande for file in `find . -name "*.csv"`. Sil existe un fichier appelé Hello World.csv, file sera défini sur ./Hello puis sur World.csv. En citant $file, ' aide.

Réponse

Les doubles guillemets sont votre ami.

diff "$file" "/some/other/path/$file" 

Sinon, le contenu de la variable est divisé en mots.

Commentaires

  • Ceci est trompeur. Le problème vient de la commande for file in `find . -name "*.csv"`. Sil existe un fichier appelé Hello World.csv, file sera défini sur ./Hello, puis sur World.csv. Citant $file a gagné ' laide.

Réponse

Avec bash4, vous pouvez également utiliser la fonction mapfile intégrée pour définir un tableau contenant chaque ligne et itérer sur ce tableau.

$ tree . ├── a │ ├── a 1 │ └── a 2 ├── b │ ├── b 1 │ └── b 2 └── c ├── c 1 └── c 2 3 directories, 6 files $ mapfile -t files < <(find -type f) $ for file in "${files[@]}"; do > echo "file: $file" > done file: ./a/a 2 file: ./a/a 1 file: ./b/b 2 file: ./b/b 1 file: ./c/c 2 file: ./c/c 1 

Réponse

Les espaces dans les valeurs peuvent être évités par une simple construction de boucle for

for CHECK_STR in `ls -l /root/somedir` do echo "CHECKSTR $CHECK_STR" done 

ls -l racine / somedir c contient mon fichier avec des espaces

Sortie de ci-dessus mon fichier avec des espaces

pour éviter cette sortie, solution simple (notez les guillemets)

for CHECK_STR in "`ls -l /root/somedir`" do echo "CHECKSTR $CHECK_STR" done 

afficher mon fichier avec des espaces

essayé sur bash

Commentaires

  • « Boucler les fichiers »- cest ce que dit la question. Votre solution affichera la sortie entière ls -l en une seule fois . Il est en fait équivalent à echo "CHECKSTR `ls -l /root/somedir`".

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *