Perles de Mongueurs (33)

Article publié dans Linux Magazine 95, juin 2007.

[+ del.icio.us] [+ Developers Zone] [+ Bookmarks.fr] [Digg this] [+ My Yahoo!]

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 :

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 :

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.

À vous !

Envoyez vos perles à perles@mongueurs.net, elles seront peut-être publiées dans un prochain numéro de Linux Magazine.

[IE7, par Dean Edwards] [Validation du HTML] [Validation du CSS]