J'ai écrit le script suivant pour différencier les sorties de deux répertoires contenant tous les mêmes fichiers en tant que tels:
#!/bin/bash
for file in `find . -name "*.csv"`
do
echo "file = $file";
diff $file /some/other/path/$file;
read char;
done
Je sais qu'il existe d'autres moyens d'y parvenir. Curieusement, ce script échoue lorsque les fichiers contiennent des espaces. Comment puis-je gérer cela?
Exemple de sortie de find:
./zQuery - abc - Do Not Prompt for Date.csv
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 nouvelles lignes 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 réponse de Gilles )
find . -type f -name '*.csv' -exec sh -c '
file="$0"
echo "$file"
diff "$file" "/some/other/path/$file"
read line </dev/tty
' {} ';'
Ou encore mieux, pour éviter d'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
' sh {} +
Réponse longue
Vous avez trois problèmes:
*.csv
?1. Fractionnement uniquement sur les nouvelles lignes
Pour savoir sur quoi définir file
, le Shell doit prendre la sortie de find
et l'interpréter d'une manière ou d'une autre, sinon file
ne serait que la sortie entière de find
.
Le shell lit la variable IFS
, qui est définie par défaut sur <space><tab><newline>
.
Ensuite, il examine chaque caractère dans la sortie de find
. Dès qu'il voit un caractère qui se trouve dans IFS
, il pense que marque la fin du nom de fichier, donc il définit file
sur les caractères qu'il a vus jusqu'à présent et exécute la boucle. Ensuite, il commence là où il s'était arrêté pour obtenir le nom de fichier suivant et exécute la boucle suivante, etc., jusqu'à la fin de la sortie.
Il fait donc cela efficacement:
for file in "zquery" "-" "abc" ...
Pour lui dire de ne diviser l'entrée que sur les retours à la ligne, vous devez faire
IFS=$'\n'
avant votre commande for ... find
.
Cela définit IFS
sur une seule nouvelle ligne, donc elle ne se divise que sur les nouvelles lignes, et non pas 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='
'
C'est probablement suffisant pour faire fonctionner votre script, mais si vous êtes intéressé à gérer correctement d'autres cas d'angle, lisez la suite ...
2. Extension de $file
Sans caractères génériques
À l'intérieur de la boucle où vous faites
diff $file /some/other/path/$file
le Shell essaie de développer $file
(encore!).
Il pourrait contenir des espaces, mais comme nous avons déjà défini IFS
ci-dessus, ce ne sera pas un problème ici.
Mais il pourrait également contenir des caractères génériques tels que *
Ou ?
, Ce qui entraînerait un comportement imprévisible. (Merci à Gilles de l'avoir signalé.)
Pour dire au shell de ne pas développer les caractères génériques, placez la variable entre guillemets doubles, par ex.
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 tu avais couru
for file in file1.csv file2.csv *.csv
qui sera étendu à
for file in file1.csv file2.csv *.csv file1.csv file2.csv
entraînant le traitement de file1.csv
et file2.csv
deux fois.
Au lieu de cela, 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 de l'entrée standard, divise la ligne en mots selon IFS
et les stocke dans les noms de variables que vous spécifiez.
Ici, nous lui disons de ne pas diviser la ligne en mots et de la stocker dans $file
.
Notez également que read line
Est devenu read line </dev/tty
.
En effet, à l'intérieur de la boucle, l'entrée standard provient de find
via le pipeline.
Si nous venions de faire read
, cela consommerait une partie ou la totalité d'un nom de fichier, et certains fichiers seraient ignorés.
/dev/tty
Est le terminal à partir duquel l'utilisateur exécute le script. Notez que cela provoquera une erreur si le script est exécuté via cron, mais je suppose que ce n'est pas important dans ce cas.
Et si un nom de fichier contient des retours à la ligne?
Nous pouvons gérer cela en remplaçant -print
Par -print0
Et en utilisant read -d ''
À la fin d'un 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 que find
met 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 du fichier de l'autre 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 la 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 celles intégrées), mais bash
comprend -d ''
Comme signifiant délimité NUL . Nous utilisons donc -d ''
Pour que read
utilise le même délimiteur de ligne que find
. Notez que -d $'\0'
, D'ailleurs, fonctionne aussi, 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 de ne pas gérer spécialement les antislashs dans les noms de fichiers. Par exemple, sans -r
, \<newline>
Sont supprimés et \n
Est converti en n
.
Une façon plus portable d'écrire cela qui ne nécessite pas bash
ou zsh
ou de se souvenir de toutes les règles ci-dessus concernant les octets nuls (encore une fois, grâce à Gilles):
find . -name '*.csv' -exec sh -c '
file="$0"
echo "$file"
diff "$file" "/some/other/path/$file"
read char </dev/tty
' {} ';'
3. Ignorer les répertoires dont les noms se terminent par * .csv
find . -name "*.csv"
correspondra é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
' {} ';'
Comme le souligne glenn jackman , dans ces deux exemples, les commandes à exécuter pour chaque fichier sont exécutées dans un sous-shell, donc si vous modifiez des variables à l'intérieur de la boucle, elles seront oubliées.
Si vous devez définir des variables et les avoir encore définies à la fin de la boucle, vous pouvez le 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 et coller cela 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 pouvez supprimer read line </dev/tty
Et envoyer le résultat à un pager comme less
.
[~ # ~] notes [~ # ~]
J'ai supprimé les points-virgules (;
) À l'intérieur de la 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`
. C'est principalement parce qu'il 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 l'ai changé en read line
.
Ce script échoue si un nom de fichier contient des espaces ou des caractères de globalisation Shell \[?*
. La commande find
génère un nom de fichier par ligne. Ensuite, la substitution de commande `find …`
Est évaluée par le shell comme suit:
find
, récupérez sa sortie.find
en mots séparés. Tout caractère d'espacement est un séparateur de mots.Par exemple, supposons qu'il y ait trois fichiers dans le répertoire actuel, appelés `foo* bar.csv
, foo 1.txt
Et foo 2.txt
.
find
renvoie ./foo* bar.csv
../foo*
Et bar.csv
../foo*
Contient un métacaractère de globalisation, il est étendu à la liste des fichiers correspondants: ./foo 1.txt
Et ./foo 2.txt
.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 le fractionnement de Word et en désactivant la globalisation. Pour atténuer le fractionnement de Word, 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 qu'aux nouvelles lignes et les espaces resteront. Pour désactiver la globalisation, exécutez set -f
. Ensuite, cette partie du code fonctionnera tant qu'aucun 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 d'utiliser $(…)
sur `…`
. Ils ont la même signification, mais la version de la citation arrière a des règles de citation étranges.)
Il y a 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ée en mots et les mots sont traités comme des motifs globaux, comme avec la substitution de commande ci-dessus. Si vous devez vous rappeler une chose à propos de la programmation Shell, rappelez-vous ceci: tilisez toujours des guillemets doubles autour des extensions de variable ($foo
) Et des substitutions de commande ($(bar)
), sauf si vous savez que vous voulez vous séparer. (Ci-dessus, nous savions que nous voulions diviser la sortie find
en lignes.)
Un moyen fiable d'appeler find
lui dit d'exécuter une commande pour chaque fichier qu'il 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, mais vous devez exclure explicitement tous les fichiers "ennuyeux".
diff -r -x '*.txt' -x '*.ods' -x '*.pdf' … . /some/other/path
Je suis surpris de ne pas voir readarray
mentionné. Cela rend cela très facile lorsqu'il 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|
En utilisant le <<<"$expansion"
construct vous permet également de diviser des variables contenant des sauts de 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 le faire dans Bash.
Parcourez tous les fichiers ( tout caractère spécial inclus) avec 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
Afaik find a tout ce dont vous avez besoin.
find . -okdir diff {} /some/other/path/{} ";"
find prend soin de bien appeler les programmes. -okdir vous demandera avant le diff (êtes-vous sûr oui/non).
Aucun Shell impliqué, aucun globbing, jokers, pi, pa, po.
En guise de note: si vous combinez find avec for/while/do/xargs, dans la plupart des cas, vous vous trompez. :)
Je suis surpris que personne n'ait mentionné la solution évidente de zsh
ici:
for file (**/*.csv(ND.)) {
do-something-with $file
}
((D)
pour inclure également les fichiers cachés, (N)
pour éviter l'erreur s'il n'y a pas de correspondance, (.)
pour limiter à normal fichiers.)
bash4.3
et au-dessus le supporte désormais également partiellement:
shopt -s globstar nullglob dotglob
for file in **/*.csv; do
[ -f "$file" ] || continue
[ -L "$file" ] && continue
do-something-with "$file"
done
Les noms de fichiers contenant des espaces ressemblent à plusieurs noms sur la ligne de commande s'ils ne sont pas cités. Si votre fichier est nommé "Hello World.txt", la ligne diff se développe pour:
diff Hello World.txt /some/other/path/Hello World.txt
qui ressemble à quatre noms de fichiers. Mettez simplement des guillemets autour des arguments:
diff "$file" "/some/other/path/$file"
La double cotation est votre ami.
diff "$file" "/some/other/path/$file"
Sinon, le contenu de la variable est divisé en mots.
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