Retrouvez cet article dans : Linux Magazine 95
Depuis le numéro 59, les Mongueurs de Perl vous proposent tous les mois de découvrir les scripts jetables qu’ils ont pu coder ou découvrir dans leur utilisation quotidienne de Perl. Bref, des choses trop courtes pour en faire un article, mais suffisamment intéressantes pour mériter d’être publiées. Ce sont les perles de Mongueurs.
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 ce 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 ité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()ettraverse_balanced()qui fonctionnent par appel de callbacks.
De ces différentes méthodes d’utilisation, j’ai retenu 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 au 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 aux 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.


