Boucler sur une liste de fichier
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 boucle
for
- la boucle
while
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 ;)