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:
- Par défaut, le shell divise la sortie dune commande sur les espaces, les tabulations et les retours à la ligne
- Les noms de fichiers peuvent contenir des caractères génériques serait développé
- 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
etread
si vous gérezfind
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:
- Exécutez la commande
find
, saisissez sa sortie. - Divisez la sortie
find
en mots séparés. Tout caractère despacement est un séparateur de mot. - 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
.
- La commande
find
renvoie./foo* bar.csv
. - Le shell divise cette chaîne à lespace, produisant deux mots:
./foo*
etbar.csv
. - Depuis
./foo*
contient un métacaractère globuleux, il est étendu à la liste des fichiers correspondants:./foo 1.txt
et./foo 2.txt
. - Par conséquent, la boucle
for
est exécutée successivement avec./foo 1.txt
,./foo 2.txt
etbar.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 utiliserfind -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 dansfind -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 partieecho
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 mettreread
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 "{}" \;
avectime find -type f -print0 | xargs -0 -I stuff cat stuff
. La versionxargs
é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 defind
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é à utiliserxargs
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 surWorld.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 surWorld.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`"
.