Article publié dans Linux Magazine 95, juin 2007.
La perle de ce mois-ci a été rédigée par Sébastien Aperghis-Tramoni
(sebastien@aperghis.net
), de Marseille.pm et Sophia.pm.
zipdiff
J'ai récemment eu un besoin particulier : un programme exécuté chaque
jour par cron(1)
envoie par mail le contenu de logs. Comme ces logs
contiennent les noms des machines sur lesquelles un autre programme n'a
pas pu se connecter, c'est bien pratique pour voir quelles sont celles
qui posent problème. Mais comme les listes restent importantes, un
collègue m'a fait remarquer qu'il serait intéressant de mettre en lumière
les machines qui apparaissent chaque jour dans ces listes, pour se charger
d'elles en priorité (la majeure partie des autres ne pouvant être simplement
corrigées pour des raisons architecturales).
On pourrait penser qu'un simple appel à diff(1)
suffirait, mais la
sortie de cette commande ne comporte que les portions autour des lignes
en différence. Certes on pourrait augmenter le nombre de lignes constituant
le contexte, mais c'est n'est pas une solution satisfaisante :-)
Fort heureusement, on peut trouver sur le CPAN le module Algorithm::Diff
qui est une réimplémentation de l'algorithme utilisé par le programme.
Cela permet par exemple de personnaliser le format de sortie des différences,
ce qui est justement ce que je voulais faire.
La documentation du module est touffue mais un peu confuse à mon goût car on peut s'en servir de plusieurs manières différentes. Il m'a donc fallu un petit moment pour comprendre comment l'utiliser dans le sens que je voulais. Après pas mal d'essais un peu monstrueux, je suis arrivé à ce programme au final très simple :
#!/usr/bin/perl use strict; use Algorithm::Diff; use File::Slurp; my @file_a = read_file(shift); my @file_b = read_file(shift); print zipdiff(\@file_a, \@file_b); sub zipdiff { my ($file_a, $file_b) = @_; my @result = (); my $diff = Algorithm::Diff->new($file_a, $file_b); while ($diff->Next) { if ($diff->Same) { push @result, " $_" for $diff->Items(1); } else { push @result, "- $_" for $diff->Items(1); push @result, "+ $_" for $diff->Items(2); } } return @result; }
L'opérateur zip : ¥
Cet opérateur apparaîtra dans Perl 6 et utilisera le symbole du yen
japonais ¥
. Il accepte des tableaux en arguments et renvoie une liste
construite en prenant le premier élément de chaque tableau, puis le second,
puis le troisième et ainsi de suite jusqu'à épuisement de tous les tableaux.
Avec deux tableaux en entrée, cela correspond à alterner un élément de l'un
puis de l'autre, comme quand on croise ses doigts ou qu'on ferme une
fermeture éclair... Zip!
Vous pouvez dès maintenant utiliser cette fonction en Perl 5 grâce
au module List::MoreUtils
, qui offre un bel ensemble de fonctions de
manipulation des listes extrêmement pratiques.
Je l'ai appelé zipdiff
en référence à l'opérateur zip car comme ce
dernier alterne les éléments des listes en entrée, ce programme alterne
les données provenant de l'ancienne et de la nouvelle version d'un même
fichier, en utilisant le format unifié.
Algorithm::Diff
propose plusieurs méthodes pour fournir les différences
entre deux sources de données :
les fonctions LCS()
, LCS_length()
, LCSidx()
qui renvoient
les plus longues sous-séquences communes sous forme de tableaux
une interface objet qui donne un intérateur
les fonctions diff()
, sdiff()
, compact_diff()
qui renvoient
les données de chaque fichier découpées dans des tableaux, et accompagnées
de tableaux d'indices pour accéder à chaque portion (chunk)
les fonctions traverse_sequences()
et traverse_balanced()
qui
fonctionnent par appel de callbacks
De ces différentes méthodes d'utilisation, j'ai retenue l'interface objet,
à mon avis la plus simple à appréhender, et qui est aussi celle recommandée
par une note de la documentation. En effet Algorithm::Diff->new()
renvoie un itérateur, c'est-à-dire un objet qui fournit à chaque itération
(chaque nouvel appel) l'élément suivant de l'ensemble traité. Ici, les
éléments traités sont les morceaux qui constituent la différence entre les
deux fichiers : parties communes, délétions et ajouts.
L'itérateur permet de naviguer entre les différents morceaux aux travers des méthodes suivantes :
Next()
permet d'aller au morceau suivant, ou, si un nombre est passé
en argument, de se déplacer de ce nombre de morceaux en avant.
Prev()
permet d'aller au morceau précédent, ou, si un nombre est passé
en argument, de se déplacer de ce nombre de morceaux en arrière.
Reset()
repositionne l'itérateur au premier morceau, ou au morceau dont
la position est passée en argument.
Le programme étant ici très simple, on utilise juste Next()
pour
avancer au fur et à mesure. La méthode Same()
permet alors de savoir
si le morceau courant est commun au deux fichiers ou pas. On récupère
enfin les lignes de chaque morceau avec la méthode Item()
qui fournit
suivant l'argument passé les lignes du premier ou du second fichier.
Dans le cas où Same()
a renvoyé vrai, c'est une partie commune et on
peut donc prendre les lignes de n'importe quel fichier. Sinon, les lignes
du premier fichier correspondent aux délétions et celles du second fichier
aux ajouts.
Le stockage dans @result
est trivial, et suit la convention de préfixer
les délétions avec un signe moins, les ajouts avec un signe plus et les
parties en commun avec une espace.
Une fois qu'on a la différence complète, on peut mettre en lumière les changements en utilisant un peu de HTML avec le code suivant :
sub highlight { my @lines = (); my %class = ( '+' => 'diff_plus', '-' => 'diff_minus' ); for my $line (@_) { $line =~ /^([+-]) /; push @lines, $1 ? qq|<span class="$class{$1}">$line</span>| : $line; } return @lines }
Et en utilisant la CSS associée :
.diff_minus { background-color: #ffcaca; } .diff_plus { background-color: #caffca; }
faisant ainsi apparaître les ajouts sur fond vert et les délétions sur fond rouge, ce qui facilite grandement la lecture de la différence.
Envoyez vos perles à perles@mongueurs.net
, elles seront peut-être
publiées dans un prochain numéro de Linux Magazine.
Copyright © Les Mongueurs de Perl, 2001-2011
pour le site.
Les auteurs conservent le copyright de leurs articles.