[couverture de Linux Magazine 74]

Perles de Mongueurs (15)

Article publié dans Linux Magazine 74, juillet/août 2005.

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

Petits unilignes entre amis

Découper un fichier diff (une rustine, quoi)

Pour produire un patch, il faut faire un diff. La commande suivante produit un fichier contenant l'intégralité des différences entre les fichiers des deux arborescences passées en paramètre.

   $ diff -Nru projet.new/ projet.HEAD/ > projet.patch

Le programme patch (écrit à l'origine par un certain Larry Wall) sait lire ce fichier rustine pour en appliquer le résultat à l'arborescence d'origine.

Si vous voulez récupérer les rustines individuelles (fichier source par fichier source), vous pouvez utiliser l'uniligne suivant :

    $ perl -MIO::File -pe '*STDOUT=IO::File->new(sprintf"> patch.%03d", ++$i) if /^diff/'

On profite de la boucle implicite créée par l'option -p pour lire le fichier de patch ligne à ligne et imprimer automatiquement chaque ligne sur la sortie standard (STDOUT). L'astuce consiste à changer le fichier correspondant à STDOUT à chaque fois qu'on détecte le début d'un nouveau diff.

L'interface fournie par le module standard IO::File et sa méthode new permet de retourner un filehandle à partir d'un nom de fichier, IO::File s'étant chargé d'ouvrir le fichier. Or un filehandle est la seule chose que l'on puisse affecter à un glob (au sens de perl) tel que *STDOUT. C'est ce qui est fait.

Pour ceux qui s'inquiètent de l'utilisation des ressources, sachez que les fichiers sont automatiquement fermés lors de l'association de STDOUT au fichier. Cela a été vérifié grâce à la commande lsof(1). Maintenant que nous connaissons le principe de base, imaginons que, en plein séance de compilation de RPM, nous modifions les sources en live dans ~/rpm/BUILD/package/, avec une arborescence de référence dans ~/package. Les fichiers dans ~/rpm/BUILD étant effacés à chaque recompilation par rpmbuild -ba package.spec, nous tenons à obtenir sous forme de patch (le format nécessaire à RPM) nos modifications.

Le réflexe premier est de faire un gros diff :

    $ diff -urN ~/package/ ~/rpm/BUILD/package/ | grep -v ^Binary > ~/tmp/mongros.patch

Déjà, on s'aperçoit que diff rencontre des fichiers binaires dont il ne sait que faire (d'où le grep), mais il va aussi rencontrer tout ce qui fichier texté créé par configure, comme les Makefile, fichiers de dépendance, etc. Le patch va donc être énorme, avec un quantité industrielle de déchets (essayez).

Or, ce qui nous intéresse, ce sont essentiellement les fichiers .c et .h qui ont été modifiés. Perl à la rescousse :

    $ perl -MIO::File -pe 'if(/^diff/){$n=m!.*/(.*\.[ch])$! ? ">$1.patch" : ">/dev/null" ;*STDOUT=IO::File->new($n)}' mongros.patch

Là, ayant construit le nom de fichier ($n) à ouvrir (*STDOUT=IO::File->new($n)) à partir des noms des fichiers ((.*\.[ch])$) dans le diff, on obtient les trois patchs sur 50 qui nous intéressent :

    $ echo *.patch
    check_disk.c.patch check_smtp.c.patch check_ups.c.patch

Notez l'utilisation de l'opérateur m// sous sa forme m!!, pour deux raisons : si on avait gardé la forme m//, il nous aurait fallu échapper le / dans l'expression rationnelle, pour éviter que perl ne le confonde avec la fin de l'expression ; et comme le shell utilise le même caractère que perl pour les échappements (\), il nous aurait fallu l'échapper deux fois (\\/). Les 47 rustines qui ne nous intéressent pas sont poubellisées grâce à ce cher /dev/null, bien pratique à utiliser.

Il nous faut néanmoins rajouter un test supplémentaire au début, de façon à ne réouvrir un nouveau fichier qu'à la ligne commençant par /^diff/. Sinon, vos patches n'auront qu'une ligne, et leur contenu sera parti à la poubelle (c'est du vécu).

Il ne nous reste plus qu'à concaténer nos trois fichiers pour avoir un joli patch à intégrer à notre package.spec :

    $ cat *.patch > monpetit.patch

Une autre solution est de tout concaténer grâce à Perl :

    $ perl -MIO::File -pe 'if(/^diff/){$n=m!.*/(.*\.[ch])$!?">>$ARGV.petit":">/dev/null";*STDOUT=IO::File->new($n)}' mongros.patch

Là, $ARGV est utilisé pour récupérer le nom du fichier lu par l'opérateur diamant <>, lui-même induit par le commutateur -p passé à perl. Vous trouverez plus d'informations en consultant les pages de manuel perlrun(1) et perlvar(1).

Ah, au fait, pourquoi faire compliqué quand on peut faire simple ? Notre ligne de commande commence à sérieusement s'allonger, allons la raccourcir en utilisant ce bon vieux open :

    $ perl -pe 'if(/^diff/){$n=m!.*/(.*\.[ch])$!?">>$ARGV.petit":">/dev/null";open STDOUT,$n}' mongros.patch

Ça fait quelques 23 caractères de gagnés, non négligeables pour les fainéants que nous sommes.

(Jérôme Fenal, Paris.pm - <jfenal@free.fr>, aidé de Philippe "BooK" Bruhat, Lyon.pm & Paris.pm - book@mongueurs.net)

Classer ses fichiers par date

Si vous avez un répertoire mal rangé, une première approche de sa réorganisation peut être de classer les fichiers par date, dans des répertoires judicieusement nommés.

    $ ls -l
    -rw-rw-r--  1 book book  123 2005-05-14 17:21 bang_eth
    -rw-rw-r--  1 book book   32 2005-05-14 16:54 clash
    -rw-rw-r--  1 book book 1023 2005-05-12 10:07 clunk
    -rw-rw-r--  1 book book  957 2005-05-19 11:18 crraack
    -rw-rw-r--  1 book book  342 2005-05-19 15:15 kayo
    -rw-rw-r--  1 book book  764 2005-05-12 10:07 pam
    -rw-rw-r--  1 book book 8764 2005-05-19 15:10 powie
    -rw-rw-r--  1 book book  723 2005-05-13 15:41 touche
    -rw-rw-r--  1 book book 1760 2005-05-18 21:32 uggh
    -rw-rw-r--  1 book book 3076 2005-05-19 15:15 zlonk

L'uniligne suivant va faire l'opération pour nous :

    $ perl -MPOSIX=strftime -MFile::Path -e 'for(glob"*"){mkpath$d=strftime"%Y-%m-%d",localtime((stat)[9]);rename$_,"$d/$_"}'

La fonction strftime() du module POSIX permet d'afficher une date en fonction d'un patron. mkpath() fournie par File::Path permet la création des répertoires.

Nous obtenons le résultat attendu :

    $ tree
    .
    |-- 2005-05-12
    |   |-- clunk
    |   `-- pam
    |-- 2005-05-13
    |   `-- touche
    |-- 2005-05-14
    |   |-- bang_eth
    |   `-- clash
    |-- 2005-05-18
    |   `-- uggh
    `-- 2005-05-19
        |-- crraack
        |-- kayo
        |-- powie
        `-- zlonk
    
    5 directories, 10 files

Sachant que mkpath() se comporte comme mkdir -p (en créant les répertoires intermédiaires si nécessaire), on peut même imaginer des patrons avec plusieurs niveaux de profondeur, comme %Y/%m/%d ou %Y/%U (%U, %V et %W sont trois manières de compter les semaine dans l'année).

Attention, rename(), tout comme son équivalent C (rename(2)) se contente de renommer le fichier ; il ne saura pas le déplacer physiquement d'un système de fichier à un autre si besoin est. Pour faire des copies d'un système de fichier à un autre, il faut utiliser File::Copy, qui fournit des fonctions move() et copy() qui fonctionnent comme les commandes mv et cp usuelles. (Mais ceci dépasse le cadre de cet uniligne.)

Merci à Jean-Charles Preaux, l'auteur de la question originale sur la liste perl@mongueurs.net.

(Philippe "BooK" Bruhat, Lyon.pm & Paris.pm - book@mongueurs.net)

À 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]