[couverture de Linux Dossiers 2]
[couverture de Linux Magazine 50]

Ecrire un programme d'une ligne en Perl

Article publié dans Linux Magazine 50, mai 2003. Repris dans Linux Dossiers 2 (avril/mai/juin 2004).

Copyright © 2003 - Philippe Bruhat.

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

Chapeau de l'article

You want it in one line? Does it have to fit in 80 columns? :-)

-- Larry Wall, comp.lang.perl, 8 mars 1990

Perl est né dans le monde Unix, et a commencé sa vie en tant qu'outil d'administration système. Il permet d'écrire aussi bien des applications de dix mille lignes que de petits filtres, sous forme de courts programmes utilisables très rapidement et jetables aussitôt.

Bien sûr, ces deux types de programmes n'utilisent pas le même style de programmation (c'est pourquoi les langages qui imposent un style ne peuvent pas couvrir un terrain aussi large). Dans cet article, nous allons vous faire découvrir le style concis de Perl.

Introduction en forme d'hommage

Les lecteurs d'Hebdogiciel se souviennent certainement de la fameuse rubrique des deulignes. Il s'agissait de faire tenir son programme dans un fichier de deux lignes (la plupart des systèmes concernés utilisaient des lignes de 255 caractères maximum). Mais on peut écrire des programmes très courts autrement que pour le plaisir de l'assombrissement, simplement parce que des choses conceptuellement simples devraient être simples à programmer. C'est le cas en Perl.

Depuis longtemps, grâce à la notion de filtre et avec l'aide du | (pipe ou tube), les systèmes Unix permettent de construire une longue commande qui traitera votre flux de données. Voici un exemple de one-liner (dans les exemples qui suivent, $ représentera le prompt utilisateur, et # un commentaire explicatif, ou bien le prompt du root) :

    $ awk '{print $3}' access.log | sort | uniq -c | sort -nr

Celui-ci permet de savoir qui fait le plus de hits dans le proxy à partir d'un log de Squid.

En général, un uniligne Perl est le remplacement d'un ensemble de tubes avec des commandes comme sed, cut, sort et consorts. Mais il n'y a pas de surcoût (fork + exec) ni de dépendance vis-à-vis d'une version particulière de ces commandes, sans parler de la difficulté de gérer des emboîtements complexes de commandes en shell. Par exemple, le find de GNU doit se réécrire find . -print pour être portable. En revanche, pour certaines tâches spécifiques, un programme spécialisé pourra se révéler plus efficace qu'un script Perl.

L'avantage d'un uniligne Perl est qu'on n'a plus à se soucier du système d'exploitation ; cela permettra de le copier plus tard dans un programme plus grand, faute de temps pour une écriture plus propre.

Cet article vous présente l'ensemble des outils que Perl met à votre disposition pour l'écriture d'unilignes.

(En hommage à l'HHHHebdo, j'aurais bien utilisé le mot uneligne, mais je cède à l'amicale pression de mes relecteurs mongueurs de Perl, et utiliserai donc le terme uniligne. Une autre suggestion intéressante était monoligne...)

Les options de la ligne de commande

Nous nous intéresserons ici seulement aux options (en anglais switches) utiles lors de la création de mini-scripts. D'autres paramètres existent, mais n'ont pas la même utilisation.

Exécution d'un script : -e ligne de code

-e est le paramètre principal pour l'écriture d'un uniligne. Son argument est une ligne de code. Pour inclure plusieurs lignes de code, il suffit d'utiliser plusieurs fois -e (cela ne vous dispense pas d'utiliser les points-virgules).

Note : Si -e est fourni, perl ne va pas chercher le nom du script à exécuter parmi les arguments en ligne de commande. Classiquement, la liste des options passées à l'interpréteur perl s'arrête avec la dernière option, ou avec l'option spéciale --. La liste des options est suivie par la liste des paramètres. Elle est accessible depuis l'interpréteur par la variable @ARGV.

Exemple :

    $ perl -e 'print "Hello, world!\n"'
    Hello, world!

Évidemment, la notion de ligne en Perl étant toute relative, on peut réaliser des scripts assez complets qui tiennent sur une ligne ! (Mais celle-ci pourra faire beaucoup plus de 80 caractères...)

Comme je l'ai indiqué précédemment,

    $ perl -e 'print "Hello, world!\n" ;' -e 'print "Bonjour, monde !\n"'

est équivalent à :

    $ perl -e 'print "Hello, world!\n" ; print "Bonjour, monde !\n"'

L'utilisation de l'option -e peut poser quelques problèmes mineurs, selon votre shell. Il suffit d'être prévenu pour les éviter facilement, et les quelques explications qui suivent devraient vous faciliter la tâche.

En règle générale, si vous ne savez pas comment réagit votre shell, souvenez-vous qu'utiliser qq(), q() et qx() au lieu de "", '' et `` peut vous simplifier l'écriture d'unilignes.

Vous pouvez consulter la page de manuel perlop(1), à la section Quote and Quote-like Operators, pour plus d'informations sur le fonctionnement de qq et de ses compères.

Les filtres sur des fichiers : -n et -p

Ces deux options sont probablement les plus utilisées pour l'écriture d'unilignes en Perl.

-n ajoute la boucle suivante autour de votre code :

    LINE:
    while (<>) {
        ...    # votre programme ici
    }

-p ajoute la boucle suivante, qui imprime automatiquement les lignes, autour de votre code :

    LINE:
    while (<>) {
        ...    # votre programme ici
    } continue {
        print or die "-p destination: $!\n";
    }

[ Note : Dans une boucle while, le bloc continue est exécuté juste avant que la condition soit à nouveau évaluée. Même si on sort prématurément du corps du while avec next ou last, le bloc continue est exécuté. Ce n'est cependant pas le cas avec un redo. Voir la page de manuel perlsyn(1) pour plus de détails. ]

Avec -n et -p, si Perl n'arrive pas à ouvrir l'un des fichiers dont le nom a été passé en ligne de commande, il affiche un avertissement et passe au fichier suivant.

Des blocs BEGIN et END peuvent être utilisés pour prendre le contrôle avant et après la boucle implicite.

Avec ces options, on peut construire (grâce à la magie de l'opérateur diamant <>) aussi bien des filtres que des scripts traitant toute une liste de fichiers. On trouve ici un exemple de la puissance et de la concision de Perl.

Notez également que -n et -p peuvent bien sûr être utilisés sur la ligne shebang (#!/usr/bin/perl au début de votre script), et feront ainsi une boucle implicite autour de l'intégralité de votre script.

Quelques exemples d'utilisation de -n :

    # grep dopé à l'EPO
    $ perl -ne 'print if /regex perl/' fichier

    # Compte le nombre de "e" dans un fichier
    $ perl -ne 'END{print "$n lettres e\n"} $n += y/e//' fichier

Quelques exemples d'utilisation de -p :

    # Rechercher-remplacer (voir plus loin l'option -i)
    $ perl -pe 's/\bfoo\b/toto/g' fichier > fichier2

    # grep -v dopé à l'EPO
    $ perl -pe '$_ = '' if /regex perl/' fichier

    # Filtre de conversion au vol
    $ perl -pe 's/\btiti\b/toto/' fichier

    # Affiche les 10 première lignes d'un fichier
    $ perl -pe 'exit if $. > 10' fichier

    # Affiche les 10 premières lignes d'un fichier (variante)
    $ perl -pe '11..exit' fichier

    # Affiche les 10 premières lignes d'un fichier (autre variante)
    $ perl -pe '10...exit' fichier

Note : Ces deux derniers exemples sont plus compliqués : il s'agit d'une utilisation particulière des opérateurs d'intervalle (range operators) .. et ....

En contexte de liste, ces opérateurs renvoient la liste des éléments entre les deux opérandes (par exemple, 1 .. 5 renvoie la liste (1, 2, 3, 4, 5)).

En contexte scalaire, .. et ... fonctionnent comme des bascules qui renvoient l'opérande de gauche ou de droite après évaluation. Quand les opérandes sont des nombres, une comparaison est faite avec $., le numéro de ligne courante.

Tous les détails sont dans perlop(1), section Range operators et dans Programmation en Perl, 3ème édition, pages 90-91. ]

L'édition sur place : -i[extension]

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

    $ perl -pe 's/\bfoo\b/toto/g;s/\bbar\b/titi/g' monfichier > monfichier.new
    $ mv -f monfichier.new monfichier

C'était bien la peine de faire un uniligne...

L'option -i vous permet de faire de l'édition sur place, c'est-à-dire de modifier directement le fichier que vous êtes en train de traiter. Ou plus exactement, le fichier traité par la construction <> (qui est justement la construction utilisée implicitement par -p et -n).

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

(En réalité, Perl renomme le fichier original et redirige la sortie du script vers un fichier portant le même nom que l'original. Le fichier renommé peut être conservé comme fichier de sauvegarde, comme nous le voyons au paragraphe suivant.)

Si vous avez peur de vous tromper, vous pouvez fournir une extension à l'option -i, qui sera utilisée pour créer un fichier de sauvegarde identique au fichier traité original. L'extension est ajoutée à la fin du nom du fichier traité :

    $ ls
    monfichier
    $ perl -i.bak -pe 's/\bfoo\b/toto/g;s/\bbar\b/titi/g' monfichier
    $ ls
    monfichier
    monfichier.bak

En fait, l'option -i vous permet de faire plus que d'ajouter une extension à l'ancienne version du fichier. Si l'extension contient un ou plusieurs caractères *, chaque * est remplacé par le nom du fichier courant. En Perl, cela s'écrirait ainsi :

    ($backup = $extension) =~ s/\*/$file_name/g;

Le code ci-dessus fonctionne de la manière suivante : $backup se voit affecter la valeur de $extension, et la substitution porte sur la valeur (la lvalue en fait) retournée par l'expression entre parenthèses, c'est-à-dire la variable $backup.

Cela vous permet d'ajouter un préfixe aux fichiers traités :

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

Ou de sauvegarder les originaux dans un répertoire :

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

Les séries qui suivent présentent des unilignes équivalents :

    # Édition sur place
    $ perl -pi -e 's/\bfoo\b/toto/g;s/\bbar\b/titi/g' fichier1 fichier2
    $ perl -pi '*' -e 's/\bfoo\b/toto/g;s/\bbar\b/titi/g' fichier1 fichier2

    # Copie de sauvegarde dans fichier1.bak
    $ perl -pi.bak -e 's/\bfoo\b/toto/g;s/\bbar\b/titi/g' fichier1 fichier2
    $ perl -pi '*.bak' -e 's/\bfoo\b/toto/g;s/\bbar\b/titi/g' fichier1 fichier2

Quelques remarques supplémentaires :

Définition du caractère de fin d'enregistrement : -0[octal]

-0 (c'est un zéro, pas la lettre O) permet de définir le caractère séparateur d'enregistrement ($/) en octal.

Il existe plusieurs valeurs particulières :

Attention, avec cette notation, vous limitez $/ à un seul caractère, alors que vous pouvez choisir un séparateur plus long si vous affectez explicitement $/ dans votre code.

Exemples :

    # Supprime les fichiers temporaires d'emacs
    $ find $HOME -name '*~' -print0 | perl -n0e unlink

    # Compte les paragraphes d'un fichier
    $ perl -n000e 'END{print "$. paragraphes\n"}' fichier

    # Comme expliqué précédemment, le script suivant pourra
    # donner des résultats différents selon les fichiers
    $ perl -ne 'BEGIN{$/="\n\n"}END{print "$. paragraphes\n"}' fichier

Le traitement automatique des fins de ligne : -l[octal]

-l permet le traitement automatique des fins de ligne. Cette option a deux effets :

  1. Utilisée avec -n ou -p, elle fait automatiquement un chomp au début de la boucle implicite.

  2. Elle affecte à $\ (le séparateur d'enregistrements en sortie) le caractère dont la valeur octale a été passée. Si aucune valeur octale n'est donnée, l'option met $\ à la valeur courante de $/.

    Cette affectation $\ = $/ est faite au moment où l'option est rencontrée. Le séparateur d'enregistrements en entrée peut donc être différent du séparateur en sortie si l'option -l est suivie par l'option -0.

Quelques exemples :

L'autosplit : -a et -Fmotif

L'option -a ajoute un split() au début de la boucle implicite créée par -n ou -p. Le résultat du split est stocké dans le tableau @F.

Notez bien que split sans argument est exactement équivalent à split(/\s+/, $_, 0).

L'option -F permet de définir un autre motif quand elle est utilisée avec -a.

Imprime le premier mot de chaque ligne :

    $ perl -lane 'print shift @F' fichier

En explicitant les options utilisées, ce script s'écrirait :

    #!/usr/bin/perl
    $\ = $/;               # option -l
    while (<>) {           # option -n
        chomp $_;          # options -l et -n
        @F = split;        # option -a
        print shift @F;    # option -e
    }

Dénonce les utilisateurs de ksh :

    $ perl -laF: -ne 'print $F[0] if $F[6] =~ m!/ksh!' /etc/passwd

En déroulant les options, on obtient :

    $\ = $/;                             # option -l
    while (<>) {                         # option -n
        chomp $_;                        # options -l et -n
        @F = split /:/;                  # options -a et -F
        print $F[0] if $F[6] =~ m!/ksh!; # option -e
    }

L'utilisation de modules : -M et -m

Ces options vous permettent d'utiliser des modules pour construire votre uniligne.

-m fait un use module (); avant d'exécuter votre programme. Cela signifie qu'aucun symbole ne sera exporté par le module utilisé.

Vous pouvez utiliser des guillemets pour ajouter du code à la suite du nom du module :

    $ perl -M'POSIX qw/strftime/' -e 'print strftime("%d %B %Y", localtime)'

Vous pouvez également utiliser le signe = pour importer des symboles, ce qui vous évite d'utiliser des guillemets. Ceci supprime également la différence entre -m et -M (autrement dit, vous pouvez utiliser -m pour importer explicitement des symboles). Ainsi :

    $ perl -MPOSIX=strftime -e 'print strftime("%d %B %Y", localtime)'

est identique à :

    $ perl -mPOSIX=strftime -e 'print strftime("%d %B %Y", localtime)'

Si le premier caractère qui suit le -m ou -M est un tiret -, alors le use est remplacé par un no.

Voici quelques exemples. Selon les modules utilisés, cette option peut donner lieu à des constructions très puissantes :

Pour finir avec les modules, voici l'uniligne généralement utilisé pour installer un nouveau module depuis CPAN :

    # perl -MCPAN -e 'install Mon::Module'

ou, depuis la sortie de CPANPLUS :

    # perl -MCPANPLUS -e 'install Mon::Module'

Conclusion

L'écriture d'unilignes fait partie de la culture Unix, et donc de la culture Perl. Mais si l'un de vos unilignes vous sert très souvent, cela vaut le coup d'en faire un script. Et vous pourrez commencer alors à lui rajouter des fonctionnalités (et des options, comme vous avez appris à le faire avec Jérôme Quelin dans Linux Mag 49)...

Références

L'auteur

Philippe 'BooK' Bruhat, <book@mongueurs.net>.

Philippe Bruhat est vice-président de l'association les Mongueurs de Perl (http://www.mongueurs.net/), membre du groupe Paris.pm et fait partie de l'équipe organisatrice de la conférence YAPC::Europe à Paris en juillet 2003 (http://yapc.mongueurs.net/). Il est consultant en sécurité et l'auteur des modules Log::Procmail, HTTP::Proxy et Regexp::Log.

BooK tient à remercier les Mongueurs de Perl pour quelques-uns des unilignes cités ici, et en particulier Stéphane Payrard, pour la mise en perspective de Perl et Unix en début d'article. Merci à Estelle pour sa relecture de dernière minute ! ;-*


Une boîte à outils en unilignes :

La liste qui suit présente un ensemble d'unilignes qui pourront éventuellement vous être utiles. L'explication du code est laissée en exercice au lecteur, qui aura besoin pour certains de perlop(1), perlvar(1) et du Camel book...

Attention, certains de ces unilignes sont extrêmement astucieux... Merci à leurs auteurs !

Filtres

Remplace "machin" par "bidule" (souvent utilisé avec l'option -i) :

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

Extrait l'en-tête d'un mail :

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

Extrait le corps d'un mail :

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

Imprime les lignes communes aux deux fichiers (posté par Randal Schwartz sur perlmonks) :

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

Le même, pour trois fichiers (et pour vous aider à comprendre) :

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

Supprime les lignes en doublon (attention, c'est plus fort qu'uniq) :

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

Extrait de la FAQ de Perl

Les unilignes suivants sont extraits de perlfaq3(1), Programming tools.

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

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

Détecte les fichiers texte (un fichier est considéré comme un fichier texte par l'opérateur -T s'il contient plus de 30% de caractères "bizarres" ou un caractère nul (\0) dans le premier bloc) :

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

Supprime la plupart des commentaires d'un source C :

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

Modifie des dates d'accès et de modification du fichier, pour affirmer qu'ils datent d'un mois dans le futur.

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

Trouve le premier UID non utilisé :

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

Divers

Numérote les lignes d'un fichier :

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

Ajoute un COMMIT toutes les 500 lignes d'un gros fichier SQL d'insertion (Cédric Bouvier) :

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

Extrait, trie et imprime les mots d'un fichier (par Peter J. Kernan, publié dans The Perl Journal 13) :

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

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

dos2unix :

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

mac2unix, qui a obtenu la deuxième place de la catégorie le programme le plus puissant de l'OPC-0 (Obfuscated Perl Contest, concours de Perl assombri), en 1996 :

    perl -w015l12pi.bak -e1 fichier_mac.txt

La fortune du pauvre (les textes sont séparés par la ligne "\n%\n") :

    perl -nle 'BEGIN{$/="\n%\n"}END{print$f} $f = $_ if rand $. < 1' fortune.txt

Affiche les répertoires du chemin d'inclusion des librairies de Perl :

    perl -le 'print for @INC'

Convertit tous les noms de fichiers du répertoire courant en minuscules, et meurt en cas de problème (attention, ce script n'a pas été testé sous Windows) :

    perl -e 'rename $_, lc or die $! for <*>'

rename permet également de faire un retour arrière, si un uniligne n'a pas fonctionné comme prévu, mais qu'heureusement -i a été utilisé pour garder une copie de sauvegarde (ici -i.bak) :

    perl -e '/(.+)\.bak$/ && rename $_, $1 for @ARGV' *.bak

Génère un mot de passe aléatoire :

    perl -e 'print chr(32 + rand 95) for 1..8'

Affiche les lignes du fichier fichier (ou du flux reçu sur l'entrée standard) par ordre croissant d'occurrence :

    perl -ne '$c{$_}++;END{print sort { $c{$a}<=>$c{$b} } keys%c}' fichier

Pathologique

Comment les Mongueurs de Perl de Lyon déterminent la date de la réunion du mois en cours (Jérôme Quelin) :

    $ LC_ALL=C cal | perl -aple '$x=$F[4]||$x}{$_=$x'

Et en plus, ils se réunissent au café Perl !

Ré-inventions de la roue

Quand on s'est pris au jeu des unilignes, on a vite tendance à écrire un petit script Perl, au lieu de se servir des outils existants, qui sont souvent plus adaptés (ou plus économiques pour le clavier)...

Les exemples qui suivent sont fournis pour vous inciter à utiliser les commandes classiques, au lieu de faire les malins !

wc -l

    perl -ne 'END{print $.}' fichier

mkdir -p

    perl -MExtUtils::Command -e mkpath un/nouveau/repertoire

col -b

    perl -pi -e 's/.\010//g'

Dans le cas où votre plate-forme de prédilection aurait des utilitaires un peu différents (il y a parfois beaucoup de différences entre deux versions de find ou de tar...), sachez qu'en février 1999 Tom Christiansen (gourou Perl s'il en est) a lancé le projet Perl Power Tools, qui consiste à reconstruire les outils de base d'Unix en pur Perl (http://ppt.perl.org/).

Puisqu'on vous dit d'arrêter de ré-inventer la roue !

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