GNU/Linux >> Tutoriels Linux >  >> Linux

Boucler à travers des fichiers avec des espaces dans les noms ? ?

Cette question a déjà des réponses ici  : Pourquoi le bouclage sur la sortie de find est-il une mauvaise pratique ?

(8 réponses)
Fermé il y a 3 ans.

J'ai écrit le script suivant pour différencier les sorties de deux répertoires avec 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 acceptée :

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 la 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
' exec-sh {} ';'

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
' exec-sh {} +

Réponse longue

Vous avez trois problèmes :

  1. Par défaut, le shell divise la sortie d'une commande en espaces, tabulations et retours à la ligne
  2. Les noms de fichiers pourraient contenir des caractères génériques qui seraient développés
  3. Que se passe-t-il s'il existe un répertoire dont le nom se termine par *.csv ? ?

1. Fractionnement uniquement sur les sauts de ligne

Pour savoir quoi définir file pour cela, le shell doit prendre la sortie de find et l'interpréter d'une manière ou d'une autre, sinon file serait juste la sortie entière de find .

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

Ensuite, il examine chaque caractère dans la sortie de find . Dès qu'il voit un caractère qui est dans IFS , il pense que cela marque la fin du nom de fichier, il définit donc file à tous 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'à ce qu'il atteigne la fin de la sortie.

Donc, il fait effectivement ceci :

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

Pour lui dire de diviser uniquement l'entrée sur les sauts de ligne, vous devez le faire

IFS=$'n'

avant votre for ... find commande.

Cela définit IFS à une seule nouvelle ligne, de sorte qu'elle 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='
'

C'est probablement suffisant pour que votre script fonctionne, mais si vous souhaitez gérer correctement d'autres cas particuliers, lisez la suite…

2. Développer $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 d'étendre $file (encore !).

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

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

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

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

Le même problème pourrait également 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)

En relation :Si je modifie les autorisations sur un fichier tar, cela s'appliquera-t-il aux fichiers qu'il contient ?

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

causant file1.csv et file2.csv à traiter 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 à partir de l'entrée standard, divise la ligne en mots selon IFS et les stocke dans les noms de variable que vous spécifiez.

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

Notez également que read line a changé pour read line </dev/tty .

C'est parce qu'à l'intérieur de la boucle, l'entrée standard provient de find via le pipeline.

Si nous faisions juste 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.

Alors, que se passe-t-il si un nom de fichier contient des retours à la ligne ?

Nous pouvons gérer cela en changeant -print à -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 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 étranges soient-ils.

Pour obtenir le nom du fichier de l'autre côté, nous utilisons IFS= read -r -d '' .

Où nous avons utilisé read ci-dessus, nous avons utilisé le délimiteur de ligne par défaut de newline, 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 NUL délimité . Nous utilisons donc -d '' faire read utiliser le même délimiteur de ligne que find . Notez que -d $'

Linux
  1. Copier des fichiers dans le terminal Linux

  2. Déplacer des fichiers dans le terminal Linux

  3. Linux - Remplacement des espaces dans les noms de fichiers

  4. Utiliser le trait de soulignement dans les noms de fichiers ?

  5. AWK et les noms de fichiers contenant de l'espace.

Comment rechercher des fichiers avec la commande fd sous Linux

Un moyen facile de fusionner des fichiers avec la commande Cat

Manipuler des fichiers avec le gestionnaire de fichiers du panneau de contrôle de Plesk

Comment gérer les noms de fichiers avec des espaces sous Linux

Comment rechercher des fichiers avec des dizaines de critères avec la commande Bash Find

Sécurisez Linux avec le fichier Sudoers