33
Petits scripts en Perl et Bash pour manipuler ses fichiers Pierre-Louis Cayrel, Th´ eophane Lumineau 30 mars 2009 Cet article propose quelques petits trucs en Perl et en Bash pour manipuler des fichiers. Mon id´ ee est la suivante, partir des scripts fait par les mongueurs et autres amateurs de Perl et Bash, les regrouper dans un fichier, les commenter de mani` ere claire et sans blabla inutile, ´ etayer avec d’autres sources (commande shell et script). Ceci dans le but d’avoir LE pdf sur la gestion des fichiers avec les scripts qui vont bien. Je vais continuer mes recherches sur le net d’uniligne pour la gestion des fichiers, cr´ eer les miens r´ epon- dant ` a certaines probl´ ematiques et j’enrichirai ce fichier au fur et ` a mesure. Ce fichier a pour sens d’´ evoluer, tous les lecteurs peuvent m’´ ecrire pour apporter leur contributions. Mots clefs : Perl, Bash, manipulation de fichiers. 1

Petits scripts en Perl et Bash pour manipuler ses chiers · (commande shell et script). Ceci dans le but d’avoir LE pdf sur la gestion des chiers avec les scripts qui vont bien

  • Upload
    others

  • View
    9

  • Download
    0

Embed Size (px)

Citation preview

  • Petits scripts en Perl et Bash pour manipuler ses fichiers

    Pierre-Louis Cayrel, Théophane Lumineau

    30 mars 2009

    Cet article propose quelques petits trucs en Perl et en Bash pour manipuler des fichiers.

    Mon idée est la suivante, partir des scripts fait par les mongueurs et autres amateurs de Perl et Bash, lesregrouper dans un fichier, les commenter de manière claire et sans blabla inutile, étayer avec d’autres sources(commande shell et script). Ceci dans le but d’avoir LE pdf sur la gestion des fichiers avec les scripts quivont bien.

    Je vais continuer mes recherches sur le net d’uniligne pour la gestion des fichiers, créer les miens répon-dant à certaines problématiques et j’enrichirai ce fichier au fur et à mesure.

    Ce fichier a pour sens d’évoluer, tous les lecteurs peuvent m’écrire pour apporter leur contributions.

    Mots clefs : Perl, Bash, manipulation de fichiers.

    1

  • Table des matières

    I Fichiers 5

    1 Comment convertir tous ces fichiers .toto en .tata ? 5

    2 Copie de fichiers 5

    3 Ajouter un préfixe aux fichiers traités 5

    4 Sauvegarder les originaux dans un répertoire 5

    5 Supprime les fichiers temporaires d’emacs 5

    6 Compte les paragraphes d’un fichier 5

    7 Imprime les lignes communes aux deux fichiers 5

    8 Imprime les lignes communes à 3 fichiers 5

    9 Détecte les fichiers texte 6

    10 Modifie des dates d’accès et de modification du fichier, pour affirmer qu’ils datent d’unmois dans le futur. 6

    11 Ajoute un COMMIT toutes les 500 lignes d’un gros fichier SQL d’insertion 6

    12 Décode et imprime un fichier encodé en base64 6

    13 dos2unix 6

    14 mac2unix 6

    15 Convertit tous les noms de fichiers du répertoire courant en minuscules, et meurt en casde problème 6

    16 Effaceur de fichiers temporaires 6

    17 Découper un fichier en blocs de n lignes 6

    18 Découper un fichier en blocs de n lignes suite 7

    19 Sélectionner une tranche d’un fichier texte 7

    20 Sélectionner une tranche d’un fichier texte suite 7

    21 Classer ses fichiers par date 8

    22 Remplacer une ligne par une autre (les deux passées en paramètre) dans un fichier : 9

    II Contenu 11

    23 Comment supprimer les doublons dans un fichier ? 11

    2

  • 24 Comment convertir un a en un b en ligne de commande dans toto.c ? 11

    25 Tris en Perl 1125.1 Trier numériquement une liste . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1125.2 La fonction sort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1125.3 La fonction sort 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1125.4 Tri avec référence . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1125.5 Tris multiples . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1225.6 Plus petit et le plus grand des éléments d’une liste : . . . . . . . . . . . . . . . . . . . . . . . 1225.7 Transformer deux mots . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12

    26 Remplace ”machin” par ”bidule” 12

    27 Supprime les lignes en doublon 12

    28 Calcule la somme du premier et dernier champ de chaque ligne : 12

    29 Extrait, trie et imprime les mots d’un fichier 12

    30 Affiche les lignes du fichier fichier (ou du flux reçu sur l’entrée standard) par ordrecroissant d’occurrence 12

    31 Pour convertir de ISO-Latin-1 vers UTF-8 13

    32 Pour convertir de UTF-16 vers ISO-Latin-1 13

    33 Mini-traducteur 13

    34 Affiche le premier paragraphe de la section Author de perl 13

    35 mgrep 14

    36 Supprimer des doublons 14

    37 Supprimer les doublons 2 15

    38 Calculer un handle de fichier 15

    39 La fonction reduce() 16

    40 Minimum et maximum d’une liste 17

    41 Compter le nombre de lignes dans une châıne 18

    42 La fonction pos() 18

    43 Découpage en tranches 19

    III Annexes 19

    44 Extrait l’en-tête d’un mail 19

    45 Extrait le corps d’un mail : 20

    3

  • 46 Supprime la plupart des commentaires d’un source C 20

    47 Trouve le premier UID non utilisé 20

    48 Numérote les lignes d’un fichier 20

    49 Conversion de secondes 20

    50 Retrouvez votre adresse IP 20

    51 Tester un compte POP 20

    52 Générer toutes les adresses IP de plusieurs sous-réseaux 21

    53 Générer une liste de nombres 21

    54 Valeurs hexadécimales des nombres de 27 à 33 21

    55 Générer une bête liste de nombres 21

    56 L’idiome substr() = ”toto” 22

    57 Visualisation de la progression 22

    58 Les parenthèses ne font pas les listes 22

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

    60 Récupérer ses mails 24

    61 Un (autre) robot de traduction 26

    62 Mesurer son débit avec l’aide de Free 27

    63 Fractionner une image 30

    64 Découper des MP3 avec Perl 31

    4

  • Première partie

    Fichiers

    1 Comment convertir tous ces fichiers .toto en .tata ?

    2 Copie de fichiers

    #!/bin/bash

    # "bkup" - copie les fichiers spécifiés dans le répertoire ~/Backup# de l’utilisateur après avoir vérifié qu’il n’y a pas de conflits de nom.

    a=$(date +’%Y%m%d%H%M%S’)cp -i $1 ~/Backup/$1.$achmod +x bkup# pour l’exécuter, saisissez simplement./bkup fichier.txt

    3 Ajouter un préfixe aux fichiers traités

    $ perl -i ’orig_*’ -pe ’s/\bfoo\b/toto/g;s/\bbar\b/titi/g’ fichier1 fichier2

    4 Sauvegarder les originaux dans un répertoire

    $ perl -i ’orig/*.bak’ -pe ’s/\bfoo\b/toto/g;s/\bbar\b/titi/g’ fichier1 fichier2

    5 Supprime les fichiers temporaires d’emacs

    $ find $HOME -name ’*~’ -print0 | perl -n0e unlink

    6 Compte les paragraphes d’un fichier

    $ perl -n000e ’END{print "$. paragraphes\n"}’ fich

    7 Imprime les lignes communes aux deux fichiers

    perl -ne ’print if ($seen{$_} .= @ARGV) =~ /10$/’ fichier1 fichier2

    8 Imprime les lignes communes à 3 fichiers

    perl -ne ’print if ($seen{$_} .= @ARGV) =~ /21+0$/’ fichier1 fichier2 fichier3

    5

  • 9 Détecte les fichiers texte

    perl -le ’for(@ARGV) {print if -f && -T _}’ *

    10 Modifie des dates d’accès et de modification du fichier, pouraffirmer qu’ils datent d’un mois dans le futur.

    perl -e ’$X=24*60*60; utime(time(),time() + 30 * $X,@ARGV)’ fichier

    11 Ajoute un COMMIT toutes les 500 lignes d’un gros fichier SQLd’insertion

    perl -ple ’print "COMMIT;" unless $. % 500’ fichier.sql

    12 Décode et imprime un fichier encodé en base64

    (tel que fourni par uuencode -m, par exemple)

    perl -MMIME::Base64 -pe ’$_ = decode_base64($_)’ fichier_base64

    13 dos2unix

    perl -pi -e ’s/\r\n/\n/g’ fichier_dos.txt

    14 mac2unix

    perl -w015l12pi.bak fichier_mac.txt

    15 Convertit tous les noms de fichiers du répertoire courant enminuscules, et meurt en cas de problème

    perl -e ’rename $_, lc or die $! for ’

    16 Effaceur de fichiers temporaires

    find $HOME -name ’*~’ -print0 | perl -n0e unlink

    17 Découper un fichier en blocs de n lignes

    Récemment, un collègue a eu besoin de découper un gros fichier en blocs de 65534 lignes (car Exceltronque les fichiers texte CSV qu’il importe à 65535, c’est embêtant).

    #!perl -wnBEGIN { $file = "partie00"; }if( $. % 65534 == 1) { # NOTE: $. commence à 1

    close F; # ferme le fichier précédent

    6

  • open F, "> $file.csv"or die "Impossible de créer $file.csv: $!";

    $file++; # auto-incrément magique}print F;

    18 Découper un fichier en blocs de n lignes suite

    Voici le script précédent modifié pour découper un gros fichier en morceaux tenant sur une disquette :

    #!perl -wnBEGIN {

    $file = "partie00";$/ = \1024; # lecture par blocs de 1 Ko$n = 0;

    }unless( $n++ % 1440 ) { # une disquette contient 1440 Ko

    close F;open F, "> $file.csv"or die "Impossible de créer $file.csv: $!";

    $file++;}print F;

    19 Sélectionner une tranche d’un fichier texte

    Découper un fichier texte en morceaux, c’est bien, mais il y a des fois où on voudrait pouvoir simplementne retenir qu’une partie du fichier, ne conserver qu’un bloc contenu entre certaines lignes. Il peut y avoirmoyen de bricoler avec des outils comme tail(1) et head(1), mais pourquoi perdre du temps à s’escaguasseravec ça quand il est si facile de le faire en Perl.

    $ perl -ne ’18..21 and print’ long_texte.txt

    20 Sélectionner une tranche d’un fichier texte suite

    Dans ce cas-ci, il n’affichera que les lignes 18 à 21 du fichier long_texte.txt. Toutefois il serait pluspratique d’en faire un script auquel on pourrait passer les lignes à afficher en paramètres. Écrivons donc cescript, que nous nommons splice pour faire référence à la fonction du même nom en Perl, mais qui travailleelle sur les tableaux.

    #!/usr/bin/perlmy($first,$last) = (shift,shift);$.==$first .. $.==$last and print while

    Si on invoque ce script ainsi :

    $ splice 185 202 long_texte.txt

    il affichera les lignes 15 à 20 (incluses) du fichier long_texte.txt.

    7

  • C’est pas mal, mais on peut faire mieux. Bien mieux. Si on change la manière d’indiquer les lignes à afficher,et qu’on adopte une syntaxe similaire à celle de cut(1), on peut alors indiquer plusieurs blocs de lignes.

    #!/usr/bin/perlsub usage { print STDERR "usage: splice LINES [file ...]\n" and exit -1 }my $lines = shift || usage();my(@first,@last,$i) = ();for my $block (split ’,’, $lines) {

    my @l = split ’-’, $block;push @first, $l[0];push @last, $l[1] || $first[-1];

    }($.==$first[$i]||($.==$first[$i+1]&&++$i)) .. $.==$last[$i] and print while

    L’exemple précédent s’écrit maintenant :

    $ man perl | splice 319-322

    NOTES The Perl motto is ”There’s more than one way to do it.” Divining how many more is left as anexercise to the reader.

    Plus intéressant, on peut maintenant indiquer plusieurs blocs de lignes à afficher. Pour illustrer cela, oncrée d’abord un fichier qui ne contient que ses numéros de lignes :

    $ pseq 1 20 "line %d" >text

    ou, pour ceux qui n’auraient pas conservé la Perle correspondante :

    $ perl -le ’print"line $_"for 1..20’ >text

    Exécutons maintenant splice en sélectionnant les lignes 8 à 9, 12 et 15 à 17.

    $ splice 8-9,12,15-17 textline 8line 9line 12line 15line 16line 17

    Comme on le voit, seules les lignes indiquées sont affichées. Quant à ceux qui voudraient maintenant sélec-tionner des tranches non plus en fonction des numéros de lignes, mais en fonction du texte (en quelque sorteun mélange des fonctionnalités de splice et de grep(1)), il y a moyen de faire quelque chose, mais c’est plusdélicat de trouver une manière générique de l’exprimer.

    21 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 lesfichiers 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

    8

  • -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 semaines dans l’année).

    Attention, rename(), tout comme son équivalent C (rename(2)) se contente de renommer le fichier ; ilne saura pas le déplacer physiquement d’un système de fichier à un autre si besoin est. Pour faire des copiesd’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.)

    22 Remplacer une ligne par une autre (les deux passées en para-mètre) dans un fichier :

    #!/bin/shif [ $# -ne 2 ] # si le nombre de paramètres n’est pas 2

    9

  • then # affiche l’usageecho Usage: $0 ligne1 ligne2 1>&2echo Ce programme lit l’entrée standard, remplace ligne1 par 1>&2echo ligne2 et écrit le résultat sur la sortie standard 1>&2exit 1

    fiIFS="\n" # la variable IFS (Internal Field Separator) est "ENTER"

    # (utilisée par la commande read)while read ligne # met dans la variable ligne une ligne de l’entrée standard

    # tant qu’on n’est pas arrivé a la findo

    if [ $ligne = $1 ] # si ligne est égale au premier paramètrethen

    echo $2 # affiche le deuxième paramètre à sa placeelse

    echo $ligne # sinon, il affiche la lignefi

    doneexit 0 # sortie normale (code de retour 0)

    Pour l’utiliser, exécutez (par exemple) : script un deuxfichierSortie.mk

    10

  • Deuxième partie

    Contenu

    23 Comment supprimer les doublons dans un fichier ?

    24 Comment convertir un a en un b en ligne de commande danstoto.c ?

    [A revoir]

    sed ’’s|a|b|g’’ toto.c;

    25 Tris en Perl

    @lettres = qw( a z e r t y );@out = sort @lettres;# @out vaut maintenant (a,e,r,t,y,z)

    25.1 Trier numériquement une liste

    sub par_num { return $a $b }@out = sort par_num @in

    25.2 La fonction sort

    La fonction sort accepte aussi directement un bloc anonyme à la place du nom de la fonction, qui jouerale rôle de la fonction de comparaison :

    @out = sort { $b $a } @in;# ici, on trie en ordre numérique inversé# (remarquez l’ordre de $a et $b)

    Le bloc anonyme étant totalement arbitraire, nous pouvons donc réaliser n’importe quel tri très facilementgrâce à ce modèle. Voici par exemple un tri sur la date de modification des fichiers :

    @out = sort { -M $a -M $b } @fichiers;

    25.3 La fonction sort 2

    @out = sort @in; # tri lexicographique@out = sort { $a cmp $b } @in; # même chose, explicite

    25.4 Tri avec référence

    Comme la liste à trier peut contenir n’importe quelles données, y compris des références, rien ne nousempêche d’interpréter les valeurs comme nous le voulons :

    @out = sort { $a->[0] $b->[0] } @in

    11

  • 25.5 Tris multiples

    out = sort { $a =~ y/e// $b =~ y/e// ||$a cmp $b } @mots

    25.6 Plus petit et le plus grand des éléments d’une liste :

    my ($min, $max) = (sort @tab)[0, -1

    ou

    my ($min,$max) = ($tab[0]) x 2; # init. sinon warningsforeach ( @tab ) {

    $min = $_ if $_ < $min;$max = $_ if $_ > $max;

    }

    25.7 Transformer deux mots

    Supposons que vous traduisez un document en anglais, et que vous voulez transformer tous les foo entoto et tous les bar en titi dans les exemples. Une fois que vous avez la nouvelle version, l’ancienne n’a plusd’intérêt pour vous.

    $ perl -i -pe ’s/\bfoo\b/toto/g;s/\bbar\b/titi/g’ monfichier

    26 Remplace ”machin” par ”bidule”

    perl -pe ’s/\bmachin\b/bidule/g’ fichier

    27 Supprime les lignes en doublon

    perl -ne ’print unless $doublon{$_}++’ fichier

    28 Calcule la somme du premier et dernier champ de chaqueligne :

    perl -lane ’print $F[0] + $F[-1]’ fichier

    29 Extrait, trie et imprime les mots d’un fichier

    perl -0nal012e ’@a{@F}++; print for sort keys %a’

    30 Affiche les lignes du fichier fichier (ou du flux reçu sur l’entréestandard) par ordre croissant d’occurrence

    perl -ne ’$c{$_}++;END{print sort { $c{$a}$c{$b} } keys%c}’ fichier

    12

  • 31 Pour convertir de ISO-Latin-1 vers UTF-8

    perl -MUnicode::String=latin1 -ne ’print latin1($_)->utf8’ fichier.txt > nouveau.txt

    32 Pour convertir de UTF-16 vers ISO-Latin-1

    $ perl -MUnicode::String=utf16 -ne ’print utf16($_)->latin1’ fichier.txt > nouveau.txt

    33 Mini-traducteur

    #!/usr/bin/perl -wuse strict;use WWW::Babelfish;

    my $fish = new WWW::Babelfish( agent => ’Translate/0.1’ );die ("Babelfish indisponible\n") unless defined($fish);my $prompt = "\n? ";print $prompt;

    while () {print $fish->translate(

    source => ’English’,destination => ’French’,text => $_,#delimiter => "\n\n",

    ),$prompt;

    }

    34 Affiche le premier paragraphe de la section Author de perl

    $ man perl | col -b | perl -ne ’/AUTHOR/../^$/ and print’AUTHOR

    Larry Wall , with the help of oodles ofother folks.

    En suivant la même route que pour splice, il est simple de transformer cet uniligne en petit script mgrep(comme multi-grep :

    #!/usr/bin/perlmy($first,$last) = (shift,shift);/$first/../$last/ and print while

    L’exemple précédent s’écrit alors :

    $ man perl | col -b | sgrep ’AUTHOR’ ’^$’AUTHOR

    Larry Wall , with the help of oodles ofother folks.

    13

  • 35 mgrep

    L’étape suivante, accepter plusieurs expressions régulières, est celle qu’il est plus difficile de rendre aussiélégante que pour splice. En effet, dans l’idéal nous voudrions pouvoir accepter n’importe quelle expressionrégulière, mais certains caractères sont nécessaires pour la syntaxe de délimitation de ces expressions àpasser en argument à mgrep (en reprenant celle de splice, on utilise le tiret pour délimiter les expressionsd’un couple et la virgule pour délimiter les couples). Ces caractères ne pourront donc pas être utilisés au seindes expressions régulières, à moins de vouloir coder un mécanisme d’échappement. Nous nous en tenons à lasyntaxe de splice, en connaissant et acceptant ses limitations.

    #!/usr/bin/perluse strict;sub usage { print STDERR "usage: mgrep PATTERNS [file ...]\n" and exit -1 }my $patterns = shift || usage();my(@first,@last,$i) = ();for my $block (split ’,’, $patterns) {

    my @l = split ’-’, $block;push @first, $l[0];push @last, $l[1] || $first[-1];

    }(/$first[$i]/||(/$first[$i+1]/&&++$i)) .. /$last[$i]/ and print while

    Un exemple d’exécution de mgrep ressemblera à ceci :

    $ man perl | col -b | mgrep AUTHOR-’^$’,motto,virtues-whyAUTHOR

    Larry Wall , with the help of oodles ofother folks.

    The Perl motto is "There’s more than one way to do it."The three principal virtues of a programmer are Laziness,Impatience, and Hubris. See the Camel Book for why.

    Les arguments signifient : afficher la ligne qui contient AUTHOR et le paragraphe qui suit (paramètreAUTHOR-’^$’), afficher la ligne qui contient motto (paramètre motto), afficher le texte de la ligne quicontient virtues à la ligne qui contient why (paramètre virtues-why).

    36 Supprimer des doublons

    Le webmestre de http ://www.fatrazie.com/ possède un fichier avec près de 50 000 noms de villes fran-çaises avec leurs coordonnées géographiques et leurs codes postaux. Ce fichier a été lui-même assemblélaborieusement à partir de diverses sources et à l’aide de programmes Perl (dont le module WWW : :Gazet-teer : :HeavensAbove).

    Le fichier courant contient une ville par ligne, avec dans l’ordre les champs nom, latitude, longitude,élévation et code postal, séparés par des tabulations. En voici un extrait :

    Montluel 45.850 5.050 195 01120Nièvroz 45.833 5.067 185 01120Pizay 45.883 5.083 284 01120Pizay 45.733 4.333 492 01120Thil 45.817 5.017 182 01120Sainte-Croix 44.767 5.283 425 01120Sainte-Croix 45.900 5.050 280 01120

    14

  • Sainte-Croix 44.767 5.283 425 01120La Léchere 45.200 6.467 1075 01121La Léchère 45.867 5.100 238 01121La Léchère 45.867 5.100 238 01121Léchère 45.583 6.333 1393 01121Belleydoux 46.250 5.767 754 01130Charix 46.183 5.683 758 01130

    Pour nettoyer son fichier, il souhaite maintenant supprimer les doublons de villes ayant le même nom etle même code postal (les coordonnées géographiques sont souvent très proches, voire identiques).

    L’objectif de cette perle n’est pas seulement de vous montrer l’uniligne qui a fait tout le travail, maissurtout de vous apprendre le réflexe presque pavlovien de tout perleur accompli : quand vous entendez lemot unique , vous devez immédiatement penser table de hachage . Ensuite, tout le problème est deconstruire la bonne clé pour ce hachage.

    Dans le cas qui nous occupe, c’est tout simple : on considère que deux villes sont identiques si elles ontle même nom et le même code postal. Notre clé sera donc la simple concaténation de ces deux champs.

    $ perl -lnaF\\t -e ’print unless $c{$F[0].$F[-1]}++’ FranceA-Z.txt > FranceA-unique.txt

    37 Supprimer les doublons 2

    Attention quand vous utilisez des clés composites : contrairement au cas ci-dessus, il est en généralpréférable d’utiliser un séparateur spécifique entre ces clés. Cela permet d’éviter des collisions fâcheuses, parexemple avec des cas où une clé serait la concaténation de ab, a et l’autre celle de a et ba.

    Le problème ne se posait pas dans notre cas, car il n’existe pas de ville dont le nom se termine par unnombre dans notre fichier.

    Pour nous simplifier la vie, nous allons utiliser une technique remontant à Perl 4 : l’émulation de tableauxmulti-dimensionnels (à l’époque, les références n’existaient pas et c’était la seule manière de faire des tableauxmulti-dimensionnels). Cela consiste à séparer les différents éléments de la clé par des virgules.

    Notre uniligne deviendrait (on a changé le . en ,) :

    $ perl -lnaF\\t -e ’print unless $c{$F[0],$F[-1]}++’ FranceA-Z.txt > FranceA-unique.txt

    Perl remplace alors $c{$F[0],$F[-1]} par $c{join $;, $F[0], $F[-1]},comme expliqué dans perlvar(1) à la section parlant de la variable$;. Par défaut, $; est le caractère \034, qui a tout de même peu dechances de se retrouver dans vos données.

    38 Calculer un handle de fichier

    J’ai récemment dû faire le tri entre les bonnes lignes et les mauvaises lignes d’un fichier. Le fichieren question était la sortie de comm(1). Il s’agissait de vérifier que toutes les lignes d’un fichier A étaientprésentes dans le fichier B (A et B étant triés).

    On utilise donc comm -2 A B pour obtenir les lignes de A absentes de B et les lignes de A présentes dansB. Ces dernières sont précédées d’une tabulation puisque comm(1) présente les résultats en colonnes.

    Pour distribuer les lignes dans les fichier A_ok et A_err, on utilise l’uniligne suivant :

    comm -2 A B | perl -nle ’print{s/^\t//?STDOUT:STDERR}$_’ > A_ok 2> A_err

    Explication : on utilise l’opérateur ternaire ? : pour choisir vers quel filehandle écrire la ligne courante :la sortie standard ou la sortie d’erreur. Le choix est conditionné par la présence d’une tabulation en début

    15

  • de ligne, que l’on enlève au passage (s/^\t//). Le filehandle donné à print doit être soit un mot simple(bareword), soit une variable scalaire (sinon l’analyseur syntaxique de Perl n’arrive pas à s’y retrouver).Toute chose plus compliquée que cela (comme un élément de tableau ou une expression) doit être placéeentre accolades :

    print { expression qui renvoie un filehandle } ...

    Ensuite, on utilise le shell pour rediriger la sortie standard et la sortie d’erreur vers deux fichiers différents.

    39 La fonction reduce()

    La fonction reduce() est une notion qui vient de la programmation fonctionnelle, comme map ou grep.L’idée est assez simple : soit une fonction f() prenant deux paramètres, il s’agit d’appliquer cette fonction

    à une liste de paramètres. On réduit la liste en appliquant successivement la fonction f() aux deux premierséléments de la liste et en les remplaçant par le résultat. On continue jusqu’à ce que la liste ne contienne plusqu’un seul élément, le résultat final.

    Un exemple concret est celui de la somme, qui généralise l’addition (opération appliquée à deux opérandes)à une liste de plusieurs opérandes.

    Dans le cas général, la réduction de la liste (a, b, c, d, e) par la fonction f() serait f( f( f( f( a, b ), c ), d), e ).

    Perl ne dispose pas d’une fonction reduce() en standard (contrairement à Python, par exemple). Heureu-sement, le module List : :Util en propose une, qui s’utilise en passant un bloc de code en premier paramètre,exactement comme la fonction standard sort().

    List : :Util fait partie de la distribution Scalar-List-Utils, qui contient également Scalar : :Util. Ces deuxmodules font partie de la distribution standard de Perl depuis la version 5.7.3.

    Comme List : :Util fournit déjà une fonction sum(), nous allons écrire une fonction mul() qui calcule leproduit des éléments d’une liste :

    use List::Util qw( reduce );sub mul { reduce { $a * $b } @_ }

    Tout l’intérêt de la fonction reduce() de List : :Util est de pouvoir utiliser les variables globales standardaetb, comme avec sort().

    En effet, on peut sinon écrire très facilement l’équivalent du code précédent :

    sub mul { my $res = shift; $res = $res * $_ for @_; $res }

    Ceci est bien sûr valable quelle que soit la fonction f() que l’on souhaite réduire.Il suffit d’écrire $res = f( $res, $_ ) for @_ dans l’exemple précédent.Attention tout de même aux effets de bords, en particulier avec l’utilisation de shift(), qui enlève le

    premier élément de la liste. Dans un contexte plus large qu’une simple fonction de quelques lignes où onmanipule @_, il faut faire attention à ne pas modifier le tableau en question (ou au moins savoir qu’on lefait). Ainsi, à la place de :

    my $res = shift @liste; # ATTENTION, modifie la liste !$res = f( $res, $_ ) for @liste;

    on préfèrera par exemple écrire :

    my $res = $liste[0];$res = f( $res, $_ ) for @liste[ 1 .. $#liste ];

    16

  • ou toute autre version adaptée à la fonction f() et à l’utilisation que l’on fait du tableau @liste.Pour information, le module List : :Util fournit également les fonctions suivantes :* min LISTE et max LISTELe minimum et le maximum d’une liste de nombres. * minstr LISTE et maxstr LISTELe minimum et le maximum d’une liste de châınes de caractères. * first BLOC LISTELe premier élément de la liste pour lequel le bloc renvoie une valeur vraie. * sum LISTELa somme des éléments de la liste, l’exemple classique. * shuffle LISTERenvoie les éléments de la liste dans un ordre aléatoire.

    40 Minimum et maximum d’une liste

    Perl ne dispose pas non plus des fonctions min() et max() pour obtenir le minimum et le maximum d’uneliste.

    Sans rentrer dans les détails, on peut dire que c’est probablement parce qu’il existe beaucoup de manièresde comparer plusieurs valeurs (en tant que nombres ou en tant que châınes de caractères, en tenant compteou non de la localisation, etc.). De plus, de telles fonctions sont finalement assez peu utilisées et en généralcourtes à coder (comme nous l’avons vu avec reduce()) ; il n’a probablement pas été jugé utile de gaspiller

    un mot-clé pour elles.C’est pourquoi le jour où on a besoin du maximum ou du minimum d’une liste (et pas de toute la liste

    triée, auquel cas on utilise sort(), bien sûr), il va nous falloir écrire la fonction nous-mêmes. Dans les exemplesqui suivent, nous prendrons pour simplifier le maximum numérique d’un tableau, mais c’est évidemment lamême chose quelle que soit la liste à traiter et la fonction de comparaison.

    Commençons par la fausse bonne idée :

    sub max { (sort { $a $b } @_)[-1] } # MAUVAIS

    Le résultat est juste : on prend le dernier élément d’une liste triée dans l’ordre croissant, c’est-à-dire lemaximum. C’est facile à écrire, ça utilise un idiome Perl (indice négatif d’une liste), mais c’est très mauvaisen performance : en effet, on trie la liste toute entière pour n’en garder qu’un seul élément.

    L’algorithme de tri utilisé par Perl dépend des versions (il y a eu pas mal d’ajouts pour Perl 5.8, enparticulier la possibilité avec la pragma sort de choisir l’algorithme de tri utilisé), mais il donne au mieux unrésultat en O(n log(n)).

    Pour obtenir le maximum d’une liste, on va plutôt utiliser la méthode classique, qui consiste à décréterque le maximum est le premier élément de la liste, puis à parcourir la liste pour mettre à jour sa valeur àchaque fois qu’on rencontre un élément plus grand que le maximum en cours.

    sub max { my $max = shift; $_ > $max and $max = $_ for @_; $max }

    Cette méthode est en O(n), c’est à dire que le nombre d’opérations est proportionnel au nombre d’élémentsde la liste. On ne peut pas faire mieux algorithmiquement. Plus le nombre n d’éléments de la liste crôıt,meilleur sera cet algorithme par rapport au précédent.

    Nous avons trouvé le meilleur algorithme, est-ce à dire qu’il n’est pas possible de faire mieux ? Bien sûrnous pouvons mieux faire, mais le gain obtenu ne pourra être que de l’ordre d’un facteur multiplicatif.

    Ainsi, le module List : :Util vu précédemment fournit une fonction max() écrite en C. Sur mon système,celle-ci est environ 3 fois plus rapide que la version Perl présentée ci-dessus. Certes, trouver le maximumd’une liste est d’autant plus long que la liste est grande, mais la fonction max() de List : :Util reste toujoursà peu près 3 fois plus rapide que la version précédente sur une liste de taille donnée.

    À propos de List : :Util, nous pourrions nous servir de la version Perl de reduce() présentée dans la perleprécédente. La fonction qui donne le maximum de deux éléments, tout le monde la connâıt : qui n’a pas vules sempiternelles macros min et max en C ?

    #define max(a,b) ((a)>(b)?(a):(b))

    17

  • On pourrait donc écrire une version un peu différente de max(), comme ceci :

    sub max { my $max = shift; $max = $_ > $max ? $_ : $max for @_; $max }

    Il va falloir comparer les temps d’exécution de ces fonctions pour estimer les performances des quatreversions de max() dont nous disposons désormais. Nous pouvons d’ores et déjà faire quelques prédictions :

    – Les versions utilisant l’algorithme en O(n) finiront toujours par être plus rapides que la version en O(nlog(n)).

    – La version C de List : :Util sera plus rapide que les versions Perl.– La version Perl utilisant la formule $_ > $max and $max = $_ sera plus rapide que celle utilisant$max = $_ > $max ? $_ : $max.

    – En effet, la première formule fait une comparaison et éventuellement une affection (une fois le maximumtrouvé, plus aucune affectation ne sera faite), tandis que la seconde fait à chaque fois une comparaisonet une affection, ce qui est nécessairement plus coûteux.

    41 Compter le nombre de lignes dans une châıne

    Un uniligne pour compter le nombre de lignes dans une châıne :

    $nr++ while "un\ndeux\ntrois\n" =~ m/\G.*?\n/gc;

    A chaque itération, on part de la fin du match précédent grâce à l’ancre \G, puis on saute un minimum decaractères grâce à .* ? avant de chercher un saut de ligne. On incrémente alors $nr. On sort de la bouclequand on ne trouve plus de match.

    Bien sûr, en Perl, on peut procéder de multiples autres manières pour arriver au même résultat :

    grep { $nr++ if $_ eq ’\n’} split ’’, "un\ndeux\ntrois\n";

    $nr = grep { $_ eq ’\n’ } split ’’, "un\ndeux\ntrois\n";

    $s = "un\ndeux\ntrois\n"$nr = grep { substr($s, $_, 1) eq ’\n’} for 0..length($s)-1

    42 La fonction pos()

    En dehors du match par une regex, la position courante dans une châıne est accessible par la fonctionpos(). Comme beaucoup de fonctions Perl, elle prend la variable $_ comme argument par défaut.

    Illustrons par un exemple :

    $s = "Les mongueurs de Perl connaissent bien le langage Perl";

    # Affiche 21, la position après la première occurrence de "Perl"$s =~ m/Perl/gc ; print pos($s),"\n" ;

    # Affiche toujours 21 car pas de match mais pas de remise à zéro# à cause de la présence de l’option /c$s =~ m/Python/gc ; print pos($s),"\n";

    # Affiche 54, la position après la seconde occurrence de "Perl"$s =~ m/Perl/gc ; print pos($s),"\n" ; # affiche "54\n"

    18

  • # Affiche 0. Pas de match et remise à zéro car absence de l’option /c.# pos($s) retourne undef qui, utilisé en contexte entier par# l’addition du 0, est converti en 0.

    $s =~ m/Python/g ; print pos($s)+0, "\n";

    Dans la suite nous nous passerons de =~, car nous effectuerons la recherche dans $_.Illustrons l’idiome m/\G.../gc par l’écriture d’un analyseur näıf de fichier de configuration qui permet

    de remplir le hash %config avec des couples clé/valeur de configuration.Ainsi un fichier .myconfig contenant :

    a = totob = titic = tutu

    reviendra à initialiser %config comme suit :

    $config{’a’} = "toto";$config{’b’} = "titi";$config{’c’} = "tutu";

    43 Découpage en tranches

    Puisque l’objet de ce collier de perles est de présenter des idiomes, rappellons que nous aurions puexprimer la même chose en terme de tranches de hash :

    @config{ ’a’, ’b’, ’c’ } = ( ’toto’, ’titi’, ’tutu’ )

    que nous pouvons aussi écrire en utilisant qw() pour créer les listes :

    @config{ qw( a b c ) } = qw( toto titi tutu );

    Voici le script de lecture du fichier de configuration :

    my %config; # hash qui contiendra la configurationopen I, ".myconfig" or die $!;while() {

    s/[\s;]+//g; # supprime blancs et éventuels points virgules$config{$1} = $2 if m/\G(\w+)=(\w+)/gc;last if m/\G$/gc; # équivalent à : last if pos == length

    }

    Troisième partie

    Annexes

    44 Extrait l’en-tête d’un mail

    perl -pe ’/^$/ && exit’ mail.txt

    19

  • 45 Extrait le corps d’un mail :

    perl -ne ’/^$/...do{print;0}’ mail.txt

    46 Supprime la plupart des commentaires d’un source C

    perl -0777 -pe ’s{/\*.*?\*/}{}gs’ source.c

    47 Trouve le premier UID non utilisé

    perl -le ’$i++ while getpwuid($i); print $i’

    48 Numérote les lignes d’un fichier

    perl -pe ’$_ = "$. $_"’ fichier

    49 Conversion de secondes

    Vous avez une durée exprimée en secondes, mais vous voudriez l’afficher en jours, heures, minutes, se-condes.

    $ perl -e ’$s=shift;print join"",map{$i=int($s/$_->[0]);$s-=$i*$_->[0];chop$_->[1]

    if$i==1;$i?($i,$_->[1]):()}[86400,"days"],[3600,"hours"],[60,"minutes"],[1,"seconds"]’ 120983

    Code déplié et commenté :

    $s = shift;print join " ", map { # concatène le résultat avec des espaces

    $i = int( $s / $_->[0] ); # combien de cette unité ?$s -= $i * $_->[0]; # secondes restanteschop $_->[1] if $i == 1; # supprime le ’s’ final au singulier$i ? ( $i, $_->[1] ) : () # retourne les éléments à afficher

    }# la liste des correspondances secondes/unité[ 86400, "jours" ], [ 3600, "heures" ], [ 60, "minutes" ],[ 1, "secondes"]

    50 Retrouvez votre adresse IP

    $ perl -MLWP::Simple -le ’print get("http://whatismyip.com/")=~/IP\s+is ([\d.]+)/i’

    51 Tester un compte POP

    #!/usr/bin/perluse Net::POP3;

    20

  • print STDERR "usage: pop3check server login [password]\n"and exit unless @ARGV;

    $| = 1;my ($server,$login,$passwd) = @ARGV;print "Password: " and chomp($passwd = ) unless defined $passwd;

    print "connecting to $server.. ";my $pop = Net::POP3->new($server);print STDERR "can’t connect to server\n" and exit unless defined $pop;print "ok\n";

    $pop->login($login, $passwd);print STDERR "error: wrong username or password\n" and exit unless $pop->ok;my ($undeleted, $size) = $pop->popstat;my $last = $pop->last;

    print "mail box size: $size\n","$undeleted unread mail(s).\n","last read mail was number $last\n\n";

    52 Générer toutes les adresses IP de plusieurs sous-réseaux

    #!/usr/bin/perluse NetAddr::IP;

    print STDERR "usage: subnets network/mask bits\n" and exit unless @ARGV;my($network,$bits) = @ARGV;my $mask = (split ’/’, $network)[1];print STDERR "bits undefined or smaller than mask\n" and exitunless $bits >= $mask;

    for my $net ( NetAddr::IP->new($network)->split($bits) ) {print join(" ", map { $net+$_ } 0..(1

  • 56 L’idiome substr() = ”toto”

    Il est peu connu que la fonction substr() peut être lhs. Ce sigle pour left hand side signifie qu’uneexpression peut apparâıtre dans la partie gauche d’une affectation.

    On sait que substr($str, $debut, $longueur) retourne la sous-châıne de $str de longueur $longueurcommençant à la position $debut. Mais, en mettant cette expression en lhs, cette sous-châıne est remplacéepar la partie droite de l’affectation. Exemple :

    $s = "groupe de mongers parisiens";print substr($s, 10, 7); # affiche "mongers"substr($s, 10, 7) = "mongueurs";print $s; # affiche "groupe de mongeurs parisiens";

    Notons que la fonction pos() est aussi lhs de sorte que vous pouvez modifier la position courante dansune châıne.

    57 Visualisation de la progression

    Revenons à notre script. Notre analyse syntaxique se bloque si le fichier de configuration n’a pas le formatattendu. Elle boucle alors indéfiniment. Corrigeons cela. En cas d’erreur, le script indiquera la position del’erreur, puis sortira. On le fait en insérant comme marqueur la châıne "" à la position courante de lachâıne analysée. On sort en affichant cette châıne modifiée si son analyse ne progresse plus. Adaptons notrescript pour afficher la position courante pour ce faire.

    Nous incluons aussi Data : :Dumper pour pouvoir afficher la valeur de %config à la fin du script.

    use strict;use Data::Dumpermy %config; # hash qui contiendra la configurationopen I, ".myconfig" or die $!;while() {

    my $pos = pos; # pos() mémorise la position courante

    s/[\s+;]+//g;$config{$1} = $2 if m/\G(\w+)=(\w+)/gc;last if m/\G$/gc;

    if ( $pos == pos ) { # la position courante a-t-elle avancé ?substr( $_, pos, 0 ) = "";die $_; # meurt si on n’a pas avancé dans la chaı̂ne

    }}print Dumper(\%config);

    58 Les parenthèses ne font pas les listes

    Notons que, dans notre script ci-dessus, nous appellons la fonction pos() sans utiliser de parenthèses. Enperl, dans l’écriture de l’appel d’une fonction, les parenthèses ne sont là que pour grouper les éléments d’uneliste, éventuellement vide, de paramètres. En d’autre termes, l’opérateur de création de liste est la virgule.

    22

  • Ce groupement par les parenthèses est souvent nécessaire car la précédence de l’opérateur d’affection est plusforte que celui de création de liste. Ainsi les parenthèses sont indispensables dans l’expression :

    substr( $_, pos, 0 ) = "";

    Car :

    substr $_, pos, 0 = "";

    est l’équivalent de :

    substr( $_, pos, (0 = "") );

    Cela n’a pas de sens car comme le compilateur le signalera alors, une constante ne peut pas être enposition lhs.

    59 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 enappliquer le résultat à l’arborescence d’origine.

    Si vous voulez récupérer les rustines individuelles (fichier source par fichier source), vous pouvez utiliserl’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 imprimerautomatiquement chaque ligne sur la sortie standard (STDOUT). L’astuce consiste à changer le fichiercorrespondant à 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 choseque 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 automatiquementfermés lors de l’association de STDOUT au fichier. Cela a été vérifié grâce à la commande lsof(1). Maintenantque nous connaissons le principe de base, imaginons que, en plein séance de compilation de RPM, nous mo-difions 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, noustenons à 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 vaaussi 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 à larescousse :

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

    23

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

    $ echo *.patchcheck_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 formem//, il nous aurait fallu échapper le / dans l’expression rationnelle, pour éviter que perl ne le confonde avecla fin de l’expression ; et comme le shell utilise le même caractère que perl pour les échappements (\), ilnous aurait fallu l’échapper deux fois (\\/). Les 47 rustines qui ne nous intéressent pas sont poubelliséesgrâ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 nouveaufichier qu’à la ligne commençant par /^diff/. Sinon, vos patches n’auront qu’une ligne, et leur contenu seraparti à la poubelle.

    Il ne nous reste plus qu’à concaténer nos trois fichiers pour avoir un joli patch à intégrer à notrepackage.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 induitpar le commutateur -p passé à perl. Vous trouverez plus d’informations en consultant les pages de manuelperlrun(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 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.

    60 Récupérer ses mails

    Avec un titre pareil, vous allez vous dire que ça part mal : pour récupérer ses mails, on utilise sonclient mail (quel qu’il soit), et ça marche très bien. Exact, je préfère ça aussi. Mais récemment, suite à undéménagement, je me suis retrouvé coupé de tout accès au net, et donc dans l’impossibilité de récupérer mesmails. Or je reçois environ 200 mails par jour et autant de spam. Et le quota sur Free n’est que de 25 Mo.

    Donc au bout de d’un mois, mon compte s’est dangereusement rapproché de la limite supérieure, et il mefallait récupérer mes mails avant que les suivants ne soient refusés. La réponse toute faite de la plupart despersonnes est d’utiliser Fetchmail. Sauf que Fetchmail tient absolument à renvoyer les mails sur un serveurqui se chargera de les délivrer (un MDA, Mail Delivery Agent). C’est une solution, mais je voulais simplementrécupérer mes mails, les stocker tous dans un simple fichier au classique format mbox. A priori, Fetchmailne permet pas de faire ça. Voici donc un petit script Perl pour récupérer les mails par POP3.

    #!/usr/bin/perluse strict;use Email::Simple;use Net::POP3;

    sub usage { die "usage: getmail file\n" }

    24

  • my $server = ’pop.free.fr’;my $login = ’maddingue’;my $passwd = ’5eckr3t’;

    my $mbox = shift or usage();

    $| = 1;

    print "connecting to $server.. ";my $pop = new Net::POP3 $serveror die "error: can’t connect to $server: $!\n";

    print "ok\n";

    $pop->login($login, $passwd);$pop->ok or die "error: wrong username or password\n";

    my ($undeleted, $size) = $pop->popstat;my $last = $pop->last;

    print "mail box size: $size\n","$undeleted unread mail(s).\n","last read mail was number $last\n\n";

    open(MBOX, ’>’, $mbox) or die "error: can’t write ’$mbox’: $!\n";my $fetched = 0;for my $num (1..$undeleted) {

    my $msg = $pop->get($num);next unless ref $msg;mbox_envelope($msg);print MBOX @$msg, $/;$fetched += $pop->list($num);printf "\rfetched %2.0f%%", $fetched*100/$size;$pop->delete($num);

    }close(MBOX);print $/;$pop->quit;

    sub mbox_envelope {my $text = $_[0];my $msg = new Email::Simple join ’’, @$text;my $date = $msg->header(’Date’);my $from = $msg->header(’Return-Path’);$from = $msg->header(’From’) unless $from;$from =~ s/[]//g;$from =~ /(\S+\@\S+)/ and $from = $1;unshift @$text, "From $from $date\n"

    }

    Vous reconnâıtrez dans le début du script celui présenté il y a un an et demi pour vérifier son compte POP3.Il est augmenté d’une boucle qui récupère les messages l’un après l’autre et les stocke dans le fichier dont le

    25

  • nom a été donné en argument du script. Détaillons son déroulement.Après s’être connecté ($pop = new Net::POP3 $server), authentifié ($pop->login($login, $passwd))

    et avoir récupéré le nombre de mails à lire ($pop->popstat), une boucle se charge de traiter chaque mes-sage. À noter qu’elle commence à 1 et non 0. On télécharge chaque message avec $pop->get($num), qui lerenvoie sous la forme d’une référence à tableau de lignes. On le passe à la fonction mbox_envelope() dontle rôle est d’ajouter une ligne au format From EXPEDITEUR DATE.

    Cette ligne, dite d’enveloppe, contient l’adresse de l’expéditeur telle qu’elle a été donnée au serveur maild’envoi avec la commande SMTP MAIL FROM :, suivie de la date d’envoi. On la reconstitue en prenant lavaleur du champ Return-Path :, s’il est présent, qui contient justement cette adresse, et sinon en prenant celledu champ From :. Cela peut sembler inutile mais cette ligne d’enveloppe, qui précède les entêtes RFC-822,est nécessaire pour que le fichier soit au format mbox et que les clients mails puissent ensuite le lire.

    Cette ligne est ensuite insérée en début du tableau qui contient le message. Puis celui-ci est stocké dansle fichier, et le message est marqué pour destruction sur le serveur POP3. À noter que les messages ne sonteffectivement détruits que lorsqu’on exécute $pop->quit(), donc jusqu’à ce moment-là, le script peut à toutmoment être interrompu sans que cela n’affecte vos mails sur le serveur.

    On peut noter que ce script utilise, en plus du module Net : :POP3, le module Email : :Simple du projetPEP[1] (Perl Email Project). Ce projet initié par Simon Cozens consiste à fournir des modules plus propreset plus simples que ceux qui existaient avant dans Mail : :* (y compris les siens). Il faut reconnâıtre qu’ici,son nom en : :Simple n’est pas abusif puisque l’interface est très naturelle : on passe le message en argumentde new(), et on peut récupérer chaque entête avec la méthode header(). La prochaine fois que vous avezbesoin d’un module Perl pour manipuler les mails, je vous recommande donc très chaudement de regarderd’abord les modules du projet PEP, qui sont véritablement simples à utiliser, même s’ils souffrent parfoisd’un certain manque de documentation.

    Enfin, pour ceux qui se demanderaient si j’ai vraiment utilisé ce script, je réponds oui, et même plusd’une fois. Au total, j’ai ainsi pu récupérer les quelques 6000 mails (hors spam) qui se sont accumulés endeux mois sur mon compte.

    61 Un (autre) robot de traduction

    Nous avons déjà présenté dans Linux Mag 61 un traducteur automatique, qui allait chercher les traduc-tions de Babelfish à l’aide d’un module CPAN. Voici aujourd’hui un rapide robot de traduction qui s’appuiecette fois sur le site FreeTranslation (http ://www.freetranslation.com/).

    Comme toujours, une fois trouvée la page contenant le formulaire adéquat, nous demandons à voir leformulaire dans ses moindres détails :

    $ mech-dump http://www.freetranslation.com/free/GET http://www.freetranslation.com/search/ [frmSearch]q=Search... (text)=Search (submit)

    POST http://ets.freetranslation.com/ [frmTranslator]sequence=core (hidden readonly)mode=html (hidden readonly)charset=UTF-8 (hidden readonly)template=results_en-us.htm (hidden readonly)language=English/Spanish (option) [*English/Spanish/English to Spanish|...|

    Russian/English/Russian to English]srctext=Type or paste some text here. (textarea)HumanTranslation= (button)Submit=FREE Translation (submit)

    26

  • C’est ici le second formulaire qui nous intéresse. Les noms des champs sont suffisamment parlants pourque nous identifiions rapidement les champs utiles : language et srctext. Un premier essai nous montre quela réponse est également dans un des champs du formulaire, le champ dsttext.

    Le script est constitué d’une boucle simple qui lit l’entrée standard ligne à ligne, envoie chaque ligne ausite de traduction et affiche le résultat, avant de re-présenter le prompt, pour recommencer :

    #!/usr/bin/perluse strict;use WWW::Mechanize;my $m = WWW::Mechanize->new();$|++; # autoflush

    # charge la première page$m->get(’http://www.freetranslation.com/free/’);die $m->res()->status_line() . "\n" unless $m->success();

    print "? ";while () {

    # sélectionne le second formulaire$m->form_number(2);

    # ou ’French/English’, ’English/German’, ’Italian/English’$m->field( language => ’English/French’ );

    $m->field( charset => ’iso-8859-1’ ); # voir ci-dessous$m->field( srctext => $_ );$m->click();

    print $m->current_form()->value(’dsttext’);print "\n? ";

    }

    Nos tests montrent rapidement qu’on peut également utiliser le champ charset si on préfère iso-8859-1plutôt que le défaut UTF-8 (d’où la ligne supplémentaire dans mon script).

    Et ça marche !

    ? programming languagelangage de programmation? the three virtues of a programmer are impatience, lazyness and hubrisles trois vertus d’un programmeur sont des impatiences, lazyness et la prétention

    Enfin, aussi bien que peut marcher la traduction automatique... ;-)Il s’agit d’un petit script rapide, mais c’est un bon point de départ pour écrire le module plus générique

    (par exemple Lingua : :Translate : :FreeTranslation).

    62 Mesurer son débit avec l’aide de Free

    Sur la page http ://tdebit.proxad.net/debit/ le fournisseur d’accès Free fournit un test de débit pourmesurer les débits montants et descendants disponibles sur votre connexion.

    Une fois la page téléchargée, on voit que le script est en fait chargé dans un :

    27

  • Nous utilisons mech-dump pour aller récupérer le formulaire directement et l’analyser :

    $ mech-dump http://tdebit.proxad.net/debit/index.plPOST http://tdebit.proxad.net/debit/debit.pl (multipart/form-data)ok=submit (image)up=010000001001000...100000010 (hidden readonly)dureeup=6.0342 (hidden readonly)sizeup=679209 (hidden readonly)

    Le contenu du champ up est énorme : 79521 caractères ! Cela fait partie de l’algorithme de calcul : ces donnéesvont être envoyées lors du POST effectué lorsque que nous cliquerons sur le bouton Lancer le test de débit, afin de calculer un débit à l’aide du temps mis par le script de Free pour recevoir ces données (calcul du

    débit montant).Les deux champs sizeup et dureeup, contrairement à ce que semblent indiquer leurs noms sont associés au

    calcul de débit descendant. Ils correspondent respectivement au volume de données reçues (cachées dans descommentaires HTML) lors de la réception du formulaire et au temps qu’il a fallu au script pour les envoyerà notre client.

    Construire un script qui valide le formulaire et récupère la page HTML générée prend quelques lignes :

    #!/usr/bin/perluse WWW::Mechanize;my $m = WWW::Mechanize->new( autocheck => 1 );$m->get(’http://tdebit.proxad.net/debit/index.pl’);$m->click(’ok’);print $m->content;

    Le contenu affiché contient toutes les informations souhaitées :

    Débit descendant(download)
    Taille du fichier 604,51 ko
    Durée 5.426 secondes
    Débit 891,25 kbit/s(111,41 ko/s)



    891,25 kbit/s

    Débit montant (upload)
    Taille du fichier 75,57 ko
    Durée 2.236 secondes
    Débit 270,4 kbit/s(33,8 ko/s)

    Et il ne nous reste plus qu’à les extraire.

    my @data = $m->content() =~ m{Taille\ du\ fichier\ (\d+(?:,\d+)?\ ko).*?Durée\ (\d+(?:\.\d+)?\ secondes).*?Débit\ (\d+(?:,\d+)?\ kbit/s).*?\((\d+(?:,\d+)?\ ko/s)\)

    }gsx;

    Avec cette expression régulière, nous récupérons les 8 valeurs d’un seul coup dans notre tableau. Nous devonsprotéger les espaces contenus dans le texte (ou les remplacer par \s) à cause de l’utilisation de l’option /xpour l’expression régulière.

    28

  • Nous avons également utilisé ( ? :...) ? pour rendre optionnels les chiffres après la virgule (ou le point).Une dernière remarque : à cause des accents dans l’expression régulière et de l’encodage des données

    reçues depuis le script de Free (iso-8859-1), il faut impérativement que le script soit encodé en iso-8859-1.Le tableau obtenu à l’aide de cette expression régulière correspond à :

    @data = (# débit descendant’604,51 ko’, # taille du fichier’5.426 secondes’, # durée de transfert’891,25 kbit/s’, # débit en kbit/s’111,41 ko/s’, # débit en ko/s# débit montant’75,57 ko’, # taille du fichier’2.236 secondes’, # durée de transfert’270,4 kbit/s’, # débit en kbit/s’33,8 ko/s’ # débit en ko/s

    );

    En ajoutant une petite boucle d’affichage, on obtient le script suivant :

    #!/usr/bin/perluse WWW::Mechanize;my $m = WWW::Mechanize->new( autocheck => 1 );

    # nécessaire pour éviter que Free filtre selon les navigateurs$m->agent_alias( ’Linux Mozilla’ );

    $m->get(’http://tdebit.proxad.net/debit/index.pl’);$m->click(’ok’);

    my @data = $m->content() =~ m{Taille\ du\ fichier\ (\d+(?:,\d+)?\ ko).*?Durée\ (\d+(?:\.\d+)?\ secondes).*?Débit\ (\d+(?:,\d+)?\ kbit/s).*?\((\d+(?:,\d+)?\ ko/s)\)

    }gsx;

    my $i = 0;for (qw( descendant montant )) {

    print "Débit $_ :\n"," $data[$i+3] ($data[$i+2])\n"," $data[$i] en $data[$i+1]\n";

    $i += 4;}

    Qui affiche chez moi (Télé2 1024) :

    Débit descendant :111,41 ko/s (891,25 kbit/s)604,51 ko en 5.426 secondes

    Débit montant :33,8 ko/s (270,4 kbit/s)75,57 ko en 2.236 secondes

    29

  • Merci à DomiX d’avoir demandé un coup de main sur le canal IRC des mongueurs (#perlfr sur le serveurirc.mongueurs.net) lors du débogage de son propre script.

    63 Fractionner une image

    Pour la conférence YAPC : :Europe 2005, les organisateurs avaient décidé de fournir un maximum d’in-formation et de matériel aux participants, quitte à ce qu’il en ait trop. ;-)

    Ainsi, ils ont mis à disposition sur le site de la conférence[1] une première carte sous la forme d’uneimage au format PNG[2], qui est un plan de Braga avec des points numérotés pour repérer les différents lieuxrelatifs à la conférence. Une autre personne décida alors de fournir l’équivalent Google Maps, ce qui rendit laprécédente carte moins utile. Néanmoins je décidais d’essayer de l’imprimer pour en avoir une version papiersous la main une fois à Braga.

    Après avoir récupéré cette image (qui pèse tout de même 5,1 Mo) je me suis demandé comment l’imprimer,car un premier essai me confirma qu’une impression directe en A4 était peu utile, le texte étant illisible. Lasolution était donc de fractionner l’image en plusieurs parties afin d’imprimer chacune sur une feuille A4.N’ayant pas envie de découper l’image à la main , je commençais à chercher un mécanisme pour s’encharger pour moi.

    Un rapide coup d’oeil dans The Gimp ne m’indiqua rien de flagrant pour réaliser cette opération. Je metournait ensuite vers les commandes en ligne de l’autre couteau suisse en matière d’images, ImageMagick.convert(1) ne permet que de convertir une image en un autre format (avec la possibilité d’appliquerlégion d’effets spéciaux). mogrify(1) permet de transformer des images, par exemple pour les redimensionneret montage(1) d’assembler plusieurs images en une seule, mais rien pour fractionner une image. Restaitconjure(1), qui exécute un script MSL (Magick Scripting Language), un machin un peu infâme en XML.Commençant à désespérer, je me mets à chercher sur Freshmeat puis le CPAN, où je suis tombé sur le moduleImage : :Magick : :Tiler[3] de Ron Savage.

    Celui-ci rend cette opération d’une simplicité déconcertante. Jugez plutôt, pour fractionner le plan deBraga en 6 carreaux , le petit script suivant suffit :

    #!/usr/bin/perluse strict;use Image::Magick::TilerImage::Magick::Tiler->new(

    input_file => shift || die("usage: $0 image [geometry [format]]"),geometry => shift || ’2x2’,output_type => shift || ’png’,write => 1, verbose => 1,

    )->tile()

    et il s’exécute ainsi :

    $ tiler braga.png 3x2

    Image : :Magick : :Tiler crée alors les fichiers correspondants :

    $ ls -ltotal 10384-rw-r--r-- 1 maddingue users 600908 nov 6 17:59 1-1.png-rw-r--r-- 1 maddingue users 930240 nov 6 17:59 1-2.png-rw-r--r-- 1 maddingue users 960254 nov 6 17:59 1-3.png-rw-r--r-- 1 maddingue users 852730 nov 6 17:59 2-1.png-rw-r--r-- 1 maddingue users 1171000 nov 6 17:59 2-2.png-rw-r--r-- 1 maddingue users 750563 nov 6 17:59 2-3.png-rw-r--r-- 1 maddingue users 5316276 jan 14 2005 braga.png

    30

  • Détaillons un peu le fonctionnement de ce script (même s’il est plutôt simple).On crée un objet (Image : :Magick : :Tiler->new(...)) et on exécute la méthode tile() qui effectue le travail

    proprement dit en fonction des paramètres passés à new().*input_file attend évidemment le nom du fichier à traiter. Ici, on utilise un court-circuit (l’opérateur

    ||) pour soit récupérer le premier argument du script, soit terminer le script en affichant son usage. *geometry indique comment découper l’image de départ. L’argument est de la forme NxM+x+y, où N

    est le nombre par défaut de carreaux en horizontal, et M le nombre par défaut de carreaux en vertical. Sil’image de départ a une largeur L et une hauteur H, les carreaux ont donc par défaut une largeur de L /N et une hauteur de H / M. +x et +y permettent ensuite d’ajuster respectivement la largeur et la hauteurdes carreaux, auquel cas Image : :Magick : :Tiler sera potentiellement amené à augmenter ou diminuer lenombre de carreaux à créer. *

    output_type permet d’indiquer le format de sortie, par défaut PNG. *write indique à la méthode tile() d’écrire les images sur disque au lieu de simplement créer les objets

    Image : :Magick correspondants. *verbose indique évidemment au module d’être verbeux.À noter qu’il existe aussi un paramètre output_dir pour indiquer le répertoire où créer les images (par

    défaut dans le répertoire courant).

    64 Découper des MP3 avec Perl

    Le script finalFinalement, notre script est assez simple puisqu’il ressemble à ce qui suit :

    #!/usr/bin/perl

    use strict;use warnings;use Getopt::Long;

    package My::MP3::Splitter;

    use MP3::Splitter;use Spreadsheet::Read qw( ReadData rows );use Carp;

    sub new {my $class = shift;my $self = bless {}, $class;$self->{input_file} = shift if scalar @_ >= 1; # on vérifie si

    # l’utilisateur a passé un# paramètre lors de la# création de l’objet

    }

    sub _process_input_file {my $self = shift;if ( -e $self->{input_file} ) {

    my $mp3_files = ReadData( $self->{input_file} );my @files = rows($mp3_files->[1]);

    31

  • shift @files; # par souci de documentation, la première ligne des# fichiers traités est ignorée, permettant ainsi# d’indiquer le type de données attendu

    foreach my $row (@files) {# on passe si...

    next if $row->[0] eq ""; # - cellule videnext if not -e $row->[0]; # - le fichier MP3 n’existe pasnext if scalar @{$row} < 4; # - pas assez d’information

    $self->_split_file(@{$row});}

    }else {

    croak "Le fichier $self->{input_file} n’existe pas...";}

    }

    sub _split_file {my ($self, $mp3_file, $new_file, $begin_part, $end_part) = @_;my $duration = $self->_compute_duration($begin_part, $end_part);mp3split($mp3_file, { name_callback => sub { $new_file } }, [ $begin_part, $duration ]);

    }

    sub _compute_duration {my ( $self, $begin, $end ) = @_;my ( $b_hour, $b_min, $b_sec )

    = $begin=~ /^(?:([\d.]+)(?:h|:(?=.*[m:])))?(?:([\d.]+)[m:])?(?:([\d.]+)s?)?$/;

    for ( $b_hour, $b_min, $b_sec ) {next unless defined $_;/^(\d+\.?|\d*\.\d+)$/;

    }my $begin_total

    = ( $b_hour || 0 ) * 3600 + ( $b_min || 0 ) * 60 + ( $b_sec || 0 );my ( $e_hour, $e_min, $e_sec )

    = $end=~ /^(?:([\d.]+)(?:h|:(?=.*[m:])))?(?:([\d.]+)[m:])?(?:([\d.]+)s?)?$/;

    for ( $e_hour, $e_min, $e_sec ) {next unless defined $_;/^(\d+\.?|\d*\.\d+)$/;

    }my $end_total

    = ( $e_hour || 0 ) * 3600 + ( $e_min || 0 ) * 60 + ( $e_sec || 0 );return $end_total > $begin_total ? $end_total - $begin_total : 0;

    }

    sub run {my ($self) = shift;if (scalar @_ >= 1) {$self->{input_file} = shift; # on vérifie si l’utilisateur a spécifié

    32

  • # un paramètre à la fonction, et le cas# échéant, on se prépare à traiter ce# fichier

    } else {if (not defined $self->{input_file}) {

    croak "No input file...\n"; # on gère le cas où aucun fichier à# traiter n’a été spécifié. Que ce# soit lors de la création de l’objet,# ou lors de l’appel de la méthode

    }}$self->_process_input_file();

    }

    package main;

    my %conf;GetOptions( \%conf, "input=s" );usage() if not exists $conf{input};My::MP3::Splitter->new( $conf{input} )->run();

    sub usage {die "$0 --input file, or $0 -i file\n";

    }

    ConclusionVoilà, j’ai maintenant la possibilité d’extraire des morceaux de mes fichiers MP3. Évidemment, je pourrais

    encore améliorer les services que peut me rendre ce script, par exemple, en ajoutant des champs dans le fichierCSV, je pourrais ajouter des informations ID3 aux fichiers MP3 créés, mais je laisse la réalisation de cetteidée au lecteur, ou à une soirée prochaine.

    Références

    [1] Sylvain Lhullier (2004) Introduction à la programmation en Perl, ou comment débuter en Perl.

    [2] http ://articles.mongueurs.net/

    33

    I Fichiers1 Comment convertir tous ces fichiers .toto en .tata ?2 Copie de fichiers3 Ajouter un préfixe aux fichiers traités4 Sauvegarder les originaux dans un répertoire5 Supprime les fichiers temporaires d'emacs6 Compte les paragraphes d'un fichier7 Imprime les lignes communes aux deux fichiers8 Imprime les lignes communes à 3 fichiers9 Détecte les fichiers texte10 Modifie des dates d'accès et de modification du fichier, pour affirmer qu'ils datent d'un mois dans le futur.11 Ajoute un COMMIT toutes les 500 lignes d'un gros fichier SQL d'insertion12 Décode et imprime un fichier encodé en base64 13 dos2unix 14 mac2unix15 Convertit tous les noms de fichiers du répertoire courant en minuscules, et meurt en cas de problème16 Effaceur de fichiers temporaires17 Découper un fichier en blocs de n lignes18 Découper un fichier en blocs de n lignes suite19 Sélectionner une tranche d'un fichier texte20 Sélectionner une tranche d'un fichier texte suite21 Classer ses fichiers par date22 Remplacer une ligne par une autre (les deux passées en paramètre) dans un fichier:

    II Contenu23 Comment supprimer les doublons dans un fichier ?24 Comment convertir un a en un b en ligne de commande dans toto.c ?25 Tris en Perl25.1 Trier numériquement une liste25.2 La fonction sort25.3 La fonction sort 225.4 Tri avec référence25.5 Tris multiples25.6 Plus petit et le plus grand des éléments d'une liste :25.7 Transformer deux mots

    26 Remplace "machin" par "bidule"27 Supprime les lignes en doublon28 Calcule la somme du premier et dernier champ de chaque ligne :29 Extrait, trie et imprime les mots d'un fichier 30 Affiche les lignes du fichier fichier (ou du flux reçu sur l'entrée standard) par ordre croissant d'occurrence 31 Pour convertir de ISO-Latin-1 vers UTF-832 Pour convertir de UTF-16 vers ISO-Latin-1 33 Mini-traducteur34 Affiche le premier paragraphe de la section Author de perl35 mgrep36 Supprimer des doublons37 Supprimer les doublons 238 Calculer un handle de fichier39 La fonction reduce()40 Minimum et maximum d'une liste41 Compter le nombre de lignes dans une chaîne42 La fonction pos()43 Découpage en tranches

    III Annexes44 Extrait l'en-tête d'un mail 45 Extrait le corps d'un mail :46 Supprime la plupart des commentaires d'un source C47 Trouve le premier UID non utilisé48 Numérote les lignes d'un fichier49 Conversion de secondes50 Retrouvez votre adresse IP51 Tester un compte POP52 Générer toutes les adresses IP de plusieurs sous-réseaux53 Générer une liste de nombres54 Valeurs hexadécimales des nombres de 27 à 3355 Générer une bête liste de nombres56 L'idiome substr() = "toto"57 Visualisation de la progression58 Les parenthèses ne font pas les listes59 Découper un fichier diff (une rustine, quoi)60 Récupérer ses mails61 Un (autre) robot de traduction62 Mesurer son débit avec l'aide de Free63 Fractionner une image64 Découper des MP3 avec Perl