Boucler sur une liste de fichier

Le 20 Juin 2015
shell bash

Boucler sur une liste de fichier sans se soucier des espaces avec bash :

1
2
3
4
while read file
do
    echo "fichier .txt : [$file]"
done < <(find . | grep .txt)

Ici, find . | grep .txt me donne la liste de fichier qui m'interesse.

Explications

Durant tous les exemples, mon repertoire courant contient les fichiers suivant :

1
2
3
4
5
$ ls -l
total 0
-rw-r--r--  1 debona  staff  0 17 oct 22:51 du_texte.txt
-rw-r--r--  1 debona  staff  0 17 oct 19:58 un nom de merde.txt
-rw-r--r--  1 debona  staff  0 18 oct 20:20 un_autre.html

Je vais détailler deux manières naïves de parcourir une liste dans un shellscript :

La version chiante : la boucle for

En shell, la boucle for utilise le séparateur $IFS (Internal Field Separator) qui n'est pas le retour à la ligne par défaut. Pour bash, l’$IFS vaut whitespace par défaut. C'est à dire l'espace, la tabulation et le retour à la ligne. Ça commence déjà à être chiant…

1
2
3
4
for file in `find . | grep .txt`
do
    echo "[$file]"
done

Va afficher :

1
2
3
4
5
[un]
[nom]
[de]
[merde.txt]
[du_texte.txt]

Facile ! Il suffit de changer la valeur de $IFS par le retour à la ligne !

1
2
3
4
5
6
IFS='
' # Si seulement IFS="\n" pouvait marcher...
for file in `find . | grep .txt`
do
    echo "[$file]"
done

Yeah, ça boucle sur chaque fichier :

1
2
[un nom de merde.txt]
[du_texte.txt]

Du coup, qu'est qu'il y a de chiant ?

Typiquement, changer l’$IFS commence à devenir casse couille quand on a des boucles imbriquées parce que ça implique de changer l’$IFS à l'interieur des boucles : et ça, c'est casse gueule.

La version sympa : la boucle while

1
2
3
4
find . | grep .txt | while read file
do
    echo "[$file]"
done

La commande “builtin” read de bash lit ligne par ligne depuis l'entrée standard. (Attention, ça lit les lignes vides alors que for ignore les items vide.) Par contre, encore un piège, le corps du while est executé dans un subshell.

Comme un subshell est un un fork (un processus fils), il y a deux trucs casse-gueules :

La mémoire du subshell n'est pas partagée avec le reste du script : Tout ce qu'il y a dedans sera inaccessible à la fin de l'execution du subshell. Si on place un exit dans le subshell : surprise ! On arrête le subshell, et le script principal reprend son execution !

En tant que tel, le mot clef while ne créer pas de subshell par lui même. En vérité, c'est le pipe (le |) qui en créer forcément un !

OK alors comment éviter le pipe ? Comme ça ?

1
2
3
4
5
6
7
8
9
10
11
# redirection de la sortie de la commande dans un fichier
find . | grep .txt > tmp.txt

# redirection du fichier dans l'entré du while
while read file
do
    echo "[$file]"
done < tmp.txt

# suppression du fichier
rm tmp.txt

L'avantage de cette methode c'est que pour lire le fichier, le while ne fait pas de subshell. L'inconvenient, c'est qu'il faut gérer le fichier, c'est à dire le créer et le supprimer.

Avec bash, il y a une astuce pour faire ça. Elle s'appelle process substitution. On s'en sert en utilisant <(ma_commande), exemple :

1
<(find . | grep .txt)

Et comment ça marche ça ?

1
2
3
4
5
echo <(find . | grep .txt) # affiche le chemin vers un "fichier" contenant la sortie de la commande
# => /dev/fd/63
cat <(find . | grep .txt) # du coup, si on `cat` ce "fichier"...
# => un nom de merde.txt
# => du_texte.txt

Et voila comment on abouti à une ecriture élégante et pas trop criptic d'une boucle qui parcour gentillement une liste de fichier.

Mais bon, c'était juste pour le fun. Il vaut mieux faire ça avec ruby ou n'importe quel autre vrai langage de script ;)