PERL: on ne nous dit pas tout ...

Retour à la page Systèmes

Motivation

L'abondante documentation de PERL pêche par son manque de concision.
Cette page rassemble des informations que l'auteur aurait aimé découvrir en premier:
c'est un résumé des pièges et de la syntaxe pour des programmeurs C/C++ qui souhaiteraient découvrir PERL.
Pour plus de précision, voir les liens en fin de page.

Pièges de la syntaxe

Préfixes et types

### Noms: $v @v %v sont trois variables distinctes de types distincts ###

$v = "123";                      #Variable scalaire (entier, réel, string, référence)
@v = ( "a", "1", "b", "2" );     #Variable tableau
%v = ( "a" => "1", "b" => "2" ); #Variable hachage

Remarque: tableaux et hachages ne peuvent contenir que des types scalaires.
La création de structures imbriquées se fait alors à l'aide de références. Certaines variables n'ont aucun préfixe: les descripteurs de fichiers sont les plus connues.

Accès aux variables

Utiliser des crochets, des parenthèses ou des accollades n'est pas un choix innocent, puisque par exemple les index [] et {} ne concernent pas les mêmes variables.

$a = $v[1];   #"1" lu dans le tableau @v (accepté mais obsolète: @v[1])
$a = $v{"a"}; #"1" lu dans le hachage %v (accepté mais obsolète: %v{"a"})

$ra = \@v; # Référence de la variable 'array' @v
$rh = \%v; # Référence de la variable 'hachage' %v

$ra->[1];  # Accès à un élément du tableau référencé
$rh->{"a"};# Accès à un élément du hachage

Pourquoi utiliser le préfixe $ pour lire dans des variables de type @ ou %?
Sans doute parce que le résultat est de type scalaire ...

De plus, "" interprète les noms de variable dans les chaînes de caractères, alors que '' ne le fait pas.

print "moi@hebergeur.com\n"; #Erreur: @hebergeur est une variable vide...
print 'toi@hebergeur.com\n'; #Erreur: @hebergeur tel quel, mais \n aussi.
print "lui\@hebergeur.com\n";#Correct: @ non-interprété, \n interprété.

Contexte

Perl essaye de "deviner" ce que l'on veut faire. A cette fin, il prend en compte un "contexte" lors de l'évaluation des expressions.
En somme, le type de variable cible impose un "contexte" qui peut influer sur le résultat.

$vs = @va;                          #Contexte scalaire: taille du tableau @va
@vb = @va;                          #Contexte array: copie du tableau @va
@v  = ( "a", "1", "b", "2" );       #Contexte tableau: $v[0]="a", $v[1]="1", ...
%v  = ( "a"=>"1", "b"=>"2" );       #Contexte hachage: $v{"a"}="1", $v{"b"}="2"
%v  = ( "a", "1", "b", "2" );       #Contexte hachage: idem!

Les fonctions et opérateurs utilisés sont également à choisir en fonction du type des données que l'on manipule (man perlop)
En particulier:

$s1="123";
$s2="123d";
if ( $s1 == $s2) { print "$s1 == $s2"; } #Compare des nombres: TRUE.
if ( $s1 eq $s2) { print "$s1 eq $s2"; } #Compare des chaînes: FALSE.

Bind et variables par défaut

Un bon nombre de fonctions de Perl ne demandent pas obligatoirement d'argument: par défaut, elles manipuleront la variable par défaut $_.

Pour en utiliser une autre, on recourt parfois aux opérateurs =~ et !~, dits de "bind".

### Variables par défaut et opérateurs de bind =~ !~ ###

$s = "abcdef";   #Variable normale.
$_ = "abcdef";   #$_ est la variable que PERL utilise par défaut.

s/abc/ABC/;      #Substitue "ABC" à "abc" dans $_
$s =~ s/abc/ABC; #Idem, mais dans la variable $s

print;           #Sans argument, affiche $_;

if (m/abc/) { print; }              #Affiche $_ si $_ contient "abc"
if ($s !~ m/abc/) { print "KO\n"; } #Affiche KO si $s ne contient PAS "abc"

Flexibilité de la syntaxe

Pour faciliter la création des scripts, la syntaxe de PERL se veut très libre ("free form") en permettant de multiples façons d'exprimer un même test.

<si_vrai> if <condition>;      #Ex: perl -e 'print "ok" if 1;'
if (<condition>) { <si_vrai> } #Ex: perl -e 'if (1) {print "ok";}'
<si_faux> unless <condition>;
unless (<condition) { <si_faux> }

Expressions régulières

Les expressions régulières sont irremplaçables pour analyser du texte, et leur excellente intégration dans PERL est l'une des plus grandes forces du langage.
A quelques subtilités près, voici comment former une expression régulière (man perlre):

Les cas les plus courants concernent la reconnaissance (opérateur match m/expr/) et la substitution s/expr/remplacement/

#Reconnaissance grossière d'une adresse e-mail nom@hebergeur
$sregex = '^([a-zA-Z0-9_.]+)@([a-zA-Z0-9_.]+)$'; #Remarque: ^...$ : début-fin

#Reconnu: entre '', @domaine reste tel quel
$chaine = 'utilisateur@domaine.extension';
if ( $chaine =~ m/$sregex/ ) { print "nom=$1; hebergeur=$2\n"; }

#Non-reconnu: entre "", @domaine est interprété comme un tableau vide
$chaine = "utilisateur2@domaine.extension";
if ( $chaine =~ m/$sregex/ ) { print "nom=$1; hebergeur=$2\n"; }

Syntaxe plus évoluée

Prototypes de fonctions

Les fonctions sont déclarées très simplement par sub:

sub f_double1 { my $d = 2*$_[0] ; return $d; }   #Renvoie le double d'un nombre.
sub f_double2 { my $d = 2*$_[0] ; }              #Idem.
sub f_add1 { my $x=$_[0]; my $y=$_[1]; $x+$y; }  #Arguments lus dans @_: méthode 1.
sub f_add2 { my $x=shift; my $y=shift; $x+$y; }  #Arguments lus dans @_: méthode 2.

Par défaut, le nombre des arguments d'une fonction est libre.
Pour spécifier un prototype, mettre un $ par argument attendu.
En voici quelques exemples.

sub fArgX;                  #Function avec nombre indéterminé d'args;
sub fArg1($);               #Fonction annonçant 1 argument formel exactement.
sub fArg2($$);              #Fonction annonçant 2 arguments formels exactement.
 
fArgX("chose","machin");    #Prototypée: accepte tous arguments
fArg1("x","y");             #Refusé: trop d'arguments pour $$
fArg2("x","y");             #Accepté: nombre d'args OK.
 
sub fArgX($$) { print $_[0]+$_[1]; }   #Accepté car proto indéterminé jusqu'ici.
sub fArg1     { print $_[0]+$_[1]; }   #Refusé: doit correspondre au prototype.
sub fArg2($$) { print $_[0]+$_[1]; }   #Accepté.
 
fArgX("chose");             #Refusé car l'implémentation a fixé le prototype.

Structures complexes

Une référence n'est guère autre chose qu'un pointeur typé.
A l'instar du langage C, copier une référence ne copie pas l'objet pointé.
Là encore, le choix des délimiteurs [] ou {}, et celui des préfixes $, @ ou %, influe sur les objets créés ou consultés.
Voici la syntaxe:

  #Création: \variable

  $rs = \$vs;  # référence un scalaire.
  $ra = \@va;  # référence un tableau (array)
  $rh = \%vh;  # référence un hachage (hash)
  $rf = \&f;   # référence une fonction
  $rg = \*glob;# référence un symbole global (à approfondir)
 
  #Consultation: préfixe adapté au type, et {} autour de la référence.

  ${$rs};  # contenu de $vs, abrégeable en $$rs
  @{$ra};  # contenu de $va, abrégeable en @$ra
  %{$rh};  # contenu de $vh, abrégeable en %$rh
  &{$rf}(@args); # appel de la fonction f depuis sa référence $rf.
 
  #Consultation: autre syntaxe ->

  $ra->[0];     # Synonyme de @{$ra}[0] et de $$ra[0];
  $rh->{"a"};   # Synonyme de %{$rh}[0] et de %$rh[0];
  $rf->(@args); # Synonyme de &{$rf}(@args)
 
  #Création de références vers tableaux et hachages anonymes

  $ra = [ 1,2,3 ];         # référence au tableau (1,2,3)
  $rh = { k1 => v1, ... }; # référence au hachage ( k1 => v1, .... )

L'exemple qui suit utilise presque toutes ces syntaxes pour décrire les séances de cinéma dans une ville:

  %programme = (); #Programme à remplir

  # -----Cinema "Rex" -----
  $titre = "Avatar";
  $raSeances = [ "20h30", "22h30" ];
  %hRex = ( $titre => $raSeances );

  # ----- Cinema "Louxor" -----
  $raSeances1  = [ "19h30" ];          #Avatar
  $raSeances2  = [ "20h15", "22h30" ]; #RED
  $rhLouxor = { "Avatar" => $raSeances1, "RED" => $raSeances2 };

  # ----- Séances dans toute la ville -----
  $programme{"Rex"} = \%hRex;       #Les () créaient un objet hash
  $programme{"Louxor"} = $rhLouxor; #Les {} créaient une référence hash

  # ----- Consultation des séances -----
  for $cine ( keys %programme ) {
    print "cine=$cine\n";
    $rhFilms = $programme{$cine};
    for $film (keys %$rhFilms) {
      print "  film=$film\n";
      $raSeances = $rhFilms->{$film};
      for $seance (@$raSeances) {
        print "    seance=$seance\n";
  } } }

Programmation objet

Le support dit objet de PERL est efficace, bien qu'il repose au fond sur un bricolage peu élégant auquel il n'est heureusement pas nécessaire de penser:

Avant d'oublier ce qui vient d'être dit, voici comment cett «cuisine» fonctionne pour un exemple simple récolté ailleurs:

###### Package article ######

package article;                  # Nom du package: utilisé comme un nom de classe
  
  sub new {                       # Notre "constructeur" pourrait s'appeler n'importe comment.
    my($class) = shift;           # La syntaxe article->new() met "article" en premier arg.
    my $rObjet = {                # Créer d'abord une référence vers les données membres
      "description" => 0,
      "quantite"    => 1
    };
    bless $rObjet, $class;        # Ainsi 'ref $rObjet' renverra $class.
    #La fonction renvoie $rObjet
  }
 
  sub get_num {
    my $self = shift;   # $objet->get_num() met $objet dans $_[0]
    return $self->{"quantite"};
  }

  sub set_num {
    my $self = shift;   # $objet->set_num() met $objet dans $_[0]
    my $qtt = shift;    # Argument passé par l'utilisateur
    $self->{"quantite"} = $qtt;
  }
  
##### Utilisation de la 'classe' article #####

package main;
 
  $item = article->new(); #Référence de hash. Le référent est initialisé par new()
  print ref $item, "\n";     #affiche "article"
  $item->set_num(3);
  print $item->get_num(), "\n"; #Appelle get_num() dans 'ref $item', avec $_0=$item

Utilisation pratique

Cette section propose de brefs extraits de code réutilisables:

Un peu de rigueur

Deux lignes pour éviter bien des pertes de temps en débogage:

use strict;
use warnings;

$a = 1;    #Refusé par 'strict': il faut déclarer $a avant
my $b = 2; #Accepté: 'my' rend $b visible de tout le bloc courant.

Tester un type

Tester un type est rarement nécessaire. La fonction ref fera l'affaire (man perlref) pour dire vers quoi pointe une référence:

$s = "chaine";
$rs = \$s;     # SCALAR
$ra = [];      # ARRAY
$rh = {};      # HASH
print ref $rs, "\n", ref $ra, "\n", ref $rh, "\n";

Pour une variable non-référence, le préfixe suffit en général, mais on peut utiliser encore ref en lui demandant vers quoi pointe la référence à cette variable:

$s = "";
@a = ();
%h = {};
$rh = \%h;
print ref \$s, "\n", ref \@a, "\n", ref \%h, "\n", ref \$rh, "\n";

Perl se refusant à distinguer chaînes et nombres, on calcule directemment avec, ou on teste sommairement si ce n'est pas nul, ou on appelle une fonction dédiée plus solide commne looks_like_number():

use Scalar::Util qw(looks_like_number);
for my $s (qw(0 0.0 1.0 1e-9 nan AbC -inf -nan -nem INF naN)) {
 $v = $s+0; #Pour convertir une chaîne en nombre
 ($s != 0 || $s eq "0") or print "a) PAS un nombre?:", $s, "\n";
 looks_like_number($s) or print "b) PAS un nombre?:", $s, "\n";
}

Le test a) utilise le fait que Perl compte tout nombre non-reconnu comme 0, mais il échoue pour 0.0;
Le test b) repose sur une fonction dédiée qui ne fait pas cette boulette;
Les deux tests comptent visiblement 'nan' et 'inf' comme des nombres, et semblent indifférents à la casse.

Tableaux

TODO

Hachages

Un exemple minimal de XML::Parser, pour afficher des tags XML et leurs attributs. L'initialisation du parseur et le parcours des attributs d'un élément utilisent des hachages.

#!/bin/env perl
use warnings; use strict; use XML::Parser;

#Exemple minimal inspiré de http://docstore.mik.ua/orelly/xml/pxml/ch04_06.htm
#Voir perldoc XML::Parser
my $p1 = XML::Parser->new(
  #Voir perldoc XML::Parser pour tous les handlers possibles
  Handlers => { #référence vers un hachage de fonctions
    Start => \&handle_elem_start,
    End => \&handle_elem_end,
});

#Analyse le fichier donné en argument, sinon stdin
my $fichier = shift @ARGV;
if ($fichier) { $p1->parsefile($fichier); } else {
  my $input = "";
  while( <STDIN> ) { $input .= $_; }
  $p1->parse($input);
}
exit;

#Implémentation des handlers annoncés à XML::Parser->new()

sub handle_elem_start {
  my ($expat, $name, %atts) = @_;
  print $name,"\n";
  for my $attname (keys %atts) { #Hachage nom=>valeur
  	print "  ",$attname," = ",$atts{$attname},";\n";
} }

sub handle_elem_end { }

Fichiers

Fonctions de base: open, select, print, close

open fh,">","truc.txt";            #Création/ouverture
print fh "Ligne 1 dans fh\n";      #Ecrit dans fh
print    "Ligne 1 dans STDOUT\n";  #Ecrit dans STDOUT par defaut
select fh;                         #Change le filehandle par défaut
print    "Ligne 2 dans fh\n";      #Ecrit donc dans fh par défaut
print STDOUT "Ligne 2 dans STDOUT\n"; #... et demande pour écrire STDOUT
close fh;                  #Fermeture

Exemple un peu plus évolué qui lit un fichier nom/prénom puis le trie.

#Lit des lignes prenom <TAB> nom, puis écrit nom <TAB> prenom, trié par nom.

#cf "Un peu de rigueur"
use strict;
use warnings;

#cf "Variables prédéfinies" (man perlvar)
my $nom_entree = $ARGV[0];       #Premier argument du script
my $nom_sortie = $ARGV[1];       #Second argument du script

print "Entree: $nom_entree\n";
print "Sortie: $nom_sortie\n";

#cf "Fichiers"
open fh_entree,"<",$nom_entree || die "Impossible de lire $nom_entree\n";

my %hNomPrenom = ();             #Hachage nom=>prenom, vide.

while (<fh_entree>) {
  #cf "Variable par défaut"
  my $ligne = $_;                #Ligne lue
  $ligne =~ s/[\n\r]*$//g;       #Enlever saut et retour de ligne
  if ($ligne =~ m/(.*)\t(.*)/) { #Regex: nom, tabulation, prénom
    my $prenom = $1;             #Premier groupe de parenthèses
    my $nom = $2;                #Second groupe de parenthèses
    $hNomPrenom{$nom} = $prenom; #Associer nom/prenom
  } else {
    die "Ligne non-reconnue: $ligne\n";
  }
}
close fh_entree;                   #Fini de lire: on peut fermer l'entrée

my @aNoms = sort keys %hNomPrenom; #Trier les noms

#Ouvrir/créer le fichier de sortie
open fh_sortie,">",$nom_sortie || die "Impossible d'ouvrir $nom_sortie\n";

#Parcourir les noms dans l'ordre
for my $nom (@aNoms) {
  my $prenom = $hNomPrenom{$nom}; #Prénom associé au nom
  my $chaine = $nom."\t".$prenom; #Ligne à écrire
  print fh_sortie $chaine, "\n";  #Ecriture dans le fichier
  print "STDOUT: $chaine\n";      #Vérification visuelle
}

close fh_sortie;                  #Fini! on peut fermer

Pour aller plus loin

Documents disponibles sur la plupart des systèmes Linux:

Documents de l’internet qui ont inspiré cet article:

Formations et ressources de référence:

Dernière modification: 1er janvier 2014