Article publié dans Linux Magazine 49, avril 2003. Repris dans Linux Dossiers 2, avril/mai/juin 2004.
Copyright © 2003 - Jérôme Quelin.
Un script peut se suffire à lui-même, et effectuer des actions codées en dur. Mais souvent, il est plus avantageux de créer un programme un peu plus générique, modifiant son comportement suivant les options passées en ligne de commande. Cet article vous montrera divers moyens de parvenir à vos fins.
Il n'est pas rare d'avoir besoin de récupérer le nom du script, pour
diverses raisons. Ce peut être aussi bien pour afficher des messages
d'erreur pertinents contenant le nom de l'application fautive, ou
alors pour adapter le comportement du programme selon le nom utilisé
pour son invocation. Par exemple, gzip
, gunzip
et zcat
sont
un seul et même programme, dont la fonction varie selon le nom par
lequel il a été appelé.
En Perl, le nom du script peut être récupéré grâce à la variable $0. En fait, cette variable contient le chemin par lequel il a été lancé. On saisit mieux la différence dans l'exemple suivant :
$ cat /tmp/foo.pl #!/usr/bin/perl print "$0\n"; $ cd /tmp; ./foo.pl ./foo.pl $ /tmp/foo.pl /tmp/foo.pl
Il n'est donc pas rare, dans un programme, de voir le morceau de code suivant :
my $prog = $0; $prog =~ s!.*/!!;
La variable $prog contiendra alors juste le nom du script. Cependant,
si cette approche suffit dans le cadre d'un environnement de type
Unix, cette solution n'est pas vraiment portable. Une alternative plus
sûre est d'utiliser le module File::Basename
, qui gère les
particularités du système d'exploitation pour vous :
use File::Basename; my $prog = basename $0;
La variable $prog contiendra alors, là encore, juste le nom du script.
La variable $0 est une variable prédéfinie globale, il ne faut donc
pas la déclarer, même lorsqu'on est sous use strict;
.
Pour la petite histoire, on peut aussi affecter une valeur à $0, ce
qui lui permettra d'apparaître sous la forme qu'on veut lorsque
quelqu'un récupèrera la liste des processus (c'est via un subterfuge
de ce genre que sendmail indique son état courant). Cependant, cette
fonctionnalité est dépendante de votre OS (attention à la
portabilité). En particulier, les systèmes BSD verront toujours
apparaître perl
dans la ligne de commande.
Voyons maintenant comment récupérer les paramètres de la ligne de
commande eux-mêmes. Attention à ne pas confondre les paramètres de
l'exécutable perl avec ceux de votre programme. Les paramètres de perl
se trouvent sur la ligne shebang (la première ligne du script, celle
commençant par #!
), et codés en dur dans votre script (il s'agit
par exemple de l'option -w
pour activer les warnings), alors que
les paramètres de votre script seront récupérés dans la ligne de
commande utilisée pour lancer votre script.
Les paramètres de votre script sont stockés dans la variable spéciale
@ARGV (il s'agit là encore d'une variable prédéfinie, qu'il ne faut donc
pas déclarer). Ainsi, le premier argument du script se trouve dans
$ARGV[0], le second dans $ARGV[1], etc. Le nombre d'arguments est donc
disponible via scalar(@ARGV)
.
$ cat foo.pl #!/usr/bin/perl print join("|", @ARGV), "\n"; $ ./foo.pl bar baz blah bar|baz|blah
Vous pouvez donc triturer le tableau @ARGV comme vous le voulez pour
récupérer ce qui vous intéresse. D'autant plus que, dans le programme
principal, les fonctions pop
et shift
s'appliquent par défaut à
@ARGV. On peut donc s'en servir ainsi :
my $action = shift; # Récupère le premier argument du script. foreach my $fic ( @ARGV ) { if ( $action =~ /COPY/ ) { ... } ... }
L'opérateur diamant <>
(permettant de récupérer une ligne
dans un fichier) utilisé sans paramètre, va quant à lui adapter son
comportement automagiquement, à la sed (ou à la awk, suivant votre
religion). Les données lues à partir de <>
proviendront
soit de l'entrée standard (STDIN), soit de chaque fichier listé sur la
ligne de commande. En fait, en interne, lorsque <>
est
évalué pour la première fois, $ARGV[0] est positionné à -
si @ARGV
est vide, qui donnera l'entrée standard lors d'un open
. La liste
@ARGV est alors traitée comme une liste de fichiers. Ce comportement
est très pratique pour faire fonctionner un script à la fois comme un
programme normal et comme un filtre (à emboîter dans une commande
Unix contenant des pipes).
Grâce au tableau @ARGV présenté auparavant, il est possible de tout faire. Mais Perl propose en interne un mécanisme d'analyse de la ligne de commande, qui peut vous simplifier la tâche.
Traditionnellement, beaucoup de paramètres sont passés sous forme d'options,
ou switchs, commençant par un tiret. Par exemple, un script lancé avec
le switch -h
affiche souvent son synopsis avant de se terminer,
sans rien faire d'autre. L'option -s
de perl (et donc positionnée
sur la ligne shebang) permet de vivifier des variables correspondant
aux arguments de la ligne de commande. Voyons tout de suite un
exemple, et considérons le script suivant :
$ cat options.pl #!/usr/bin/perl -s print "o = $o\n"; print "ARGV = @ARGV\n";
Lançons-le maintenant avec différents arguments :
$ ./options.pl o = ARGV = $ ./options.pl bar o = ARGV = bar $ ./options.pl -o o = 1 ARGV = $ ./options.pl -o bar o = 1 ARGV = bar
On voit que la variable $o est positionnée suivant la ligne de
commande, à la façon d'un booléen. En fait, elle est undef
si le
switch correspondant n'est pas positionné. Remarquez comme perl
supprime automatiquement l'option du reste de la ligne de
commande. Plus fort encore, on peut même passer des chaînes (ou des
nombres) à l'option :
$ ./options.pl -o=23 o = 23 ARGV = $ ./options.pl -o=bar baz o = bar ARGV = baz
Suivant ce qu'on voulait faire, le comportement ci-dessus est
acceptable ou non. Si on voulait en fait que l'option -o
vaille
bar baz
, alors il aurait fallu appeler le script ainsi :
$ ./options.pl -o="bar baz" o = bar baz ARGV =
D'un autre côté, il se peut qu'on veuille que -o
ne soit pas
interprété comme un switch. Pas de problème, il existe deux manières
de contourner le problème. La première consiste à mettre l'option qui
n'en est pas une après un paramètre qui ne peut pas être interprété
comme un switch :
$ ./options.pl bar -o o = ARGV = bar -o
D'autre part, perl arrête de traiter les options dès qu'il rencontre un double tiret, quelle que soit la suite de la ligne de commande :
$ ./options.pl -- -o o = ARGV = -o
L'option -s
reconnaît des switchs de longueur quelconque. Il faut
donc bien voir que si on lance la commande :
$ ./options -xyz
perl va positionner la variable $xyz, alors que :
$ ./options -x -y -z
positionnera les variables $x, $y et $z. Il n'existe aucun moyen de changer ce comportement.
Cependant, l'option -s
de perl comporte quelques
inconvénients. Tout d'abord, lorsqu'on utilise aussi le switch -w
(ce qui est quasi obligatoire lorsqu'on construit un programme non
jetable), il faut bien faire attention de tester si la valeur est
définie. Ainsi, si nous rajoutons un use warnings;
dans notre
script précédent, sans faire attention, nous obtiendrons :
$ ./options.pl bar Name "main::o" used only once: possible typo at ./options.pl line 3. Use of uninitialized value in concatenation (.) or string at ./options.pl line 3. o = ARGV = bar
Ceci est toutefois facilement contré avec un test if defined
(dans
notre exemple : print "o = $o\n" if defined $o;
).
Le deuxième problème est quant à lui plus préoccupant. Si le script
utilise use strict;
(comme il devrait le faire), Perl va se
plaindre que les variables d'options ne sont pas déclarées :
$ ./options.pl bar Global symbol "$o" requires explicit package name at ./options.pl line 4. Execution of ./options.pl aborted due to compilation errors.
Il faudra déclarer toutes les variables concernant les options. Mais
attention, les variables ainsi déclarées doivent être globales et non
lexicales. Une variable lexicale enlèvera les messages d'erreur, mais
la variable ne sera alors pas positionnée suivant la ligne de
commande. L'usage de my
est donc prohibé. Pour notre exemple, il
faudra utiliser l'une des deux formes suivantes :
use vars qw/ $o /; # notation classique our $o; # pour perl >= 5.6
Si on décide d'utiliser un grand nombre d'options, on se retrouve donc obligé de déclarer un grand nombre de variables globales, ce qui n'est pas très propre.
Mais le plus gros problème du switch -s
vient du fait qu'il
n'intercepte pas les options erronées. Ainsi, si quelqu'un passe le
switch -bar
au script, perl va positionner la variable $bar, sans
se préoccuper de savoir si cette option est valide. Plus gênant, il va
le supprimer complètement de la ligne de commande, comme s'il n'avait
jamais existé. Ce comportement peut être problématique, par exemple si
une nouvelle version du programme est distribuée, dont le jeu
d'options reconnues est différent. Un utilisateur de l'ancienne
version continuera d'utiliser des options qui ne sont plus valides, et
le programme ne lui signalera pas son erreur ! Malheureusement, le
switch -s
ne propose aucune parade à ce problème. Il faut vous
adresser ailleurs.
Ailleurs, cela veut dire Getopt::Std. Ce module (fourni dans la
distribution standard de Perl) propose deux
fonctions, getopt
et getopts
, qui vont analyser la ligne de
commande. Comme getopt
est moins puissante que getopts
, nous allons
nous concentrer sur cette dernière. L'appel à getopts
nécessite un
paramètre, qui est une suite de caractères, concaténés en une chaîne,
représentant les options acceptées par le script. Ainsi, si nous voulons
que notre script réagisse aux options -o
et -p
, il faut utiliser :
getopts( "op" );
Les options ainsi indiquées seront des valeurs booléennes, valant soit
undef
soit 1
suivant que l'option ait été indiquée en ligne de
commande ou pas. Pour indiquer une option attendant un argument, il suffit
d'accoler un :
après la lettre d'option correspondante. Ainsi, si
-p
est une option booléenne et -o
une option attendant un
argument, la syntaxe devient :
getopts( "o:p" );
La fonction récupèrera la valeur associée quelle que soit la syntaxe de la ligne de commande :
$ ./options -ofoo $ ./options -o foo $ ./options -o"foo bar" $ ./options -o "foo bar" $ ./options -o ""
Attention au dernier cas, c'est la seule manière de passer un argument
vide à l'option. En effet, si on ne met rien après, la fonction va
vouloir un argument pour remplir la variable. Et si l'option (sans sa
valeur associée) se trouve juste devant une autre option, getopts
va
prendre l'option qui suit pour remplir la valeur associée :
$ ./options.pl -o -p o = -p # erreur ! p = # erreur ! ARGV =
A l'instar du switch -s
de la ligne shebang, la fonction getopts
va
positionner la valeur des variables $opt_x, avec x
l'option
correspondante. Ainsi, dans notre exemple, les variables $opt_o et
$opt_p vont être positionnées. Ceci pose le même problème que
précédemment, à savoir qu'il faut les définir auparavant si on
utilise use strict;
. Mais Getopt::Std nous permet de passer un
paramètre supplémentaire à la fonction, qui est une référence à un
hash. getopts
remplira alors le hash suivant les options de la ligne
de commande, les clefs étant les options présentes, les valeurs
correspondantes :
getopts( "o:z", \%opts ); # Nous pouvons maintenant utiliser $opts{o} ou $opts{z}
Attention toutefois, une option non présente sur la ligne de commande n'aura pas de clef dans le hash. Le test doit donc se faire sur l'existence de la clef de hash plutôt que sur sa valeur associée.
Mais le grand intérêt de getopts
est la gestion des erreurs. Elle
s'effectue à deux niveaux : si l'utilisateur tente d'employer une
option non reconnue, getopts
va premièrement envoyer un message
d'erreur sur STDERR :
$ ./options.pl -f Unknown option: f
et de plus, elle renverra une valeur fausse. Ceci permettra de tester si l'utilisateur ne s'est pas trompé, pour imprimer un bref rappel des options supportées et arrêter le script. Par exemple, il n'est pas rare de rencontrer :
getopts( 'o:z', \%opts ) or print_usage();
getopts
suit les mêmes règles que le switch -s
, à savoir qu'il
arrêtera l'analyse de la ligne de commande dès qu'il rencontrera un
argument qui ne ressemble pas à une option (c'est à dire qui ne
commence pas par un tiret), ou quand il rencontrera l'argument --
(auquel cas il l'enlèvera de la ligne de commande avant de terminer
son analyse) :
$ ./options.pl -x foo -y x = 1 y = ARGV = foo -y $ ./options.pl -- -x -y foo x = y = ARGV = -x -y foo
La fonction getopts
est cependant limitée, car elle n'accepte que des
options à un caractère. Toute chaîne de caractères sur la ligne de
commande précédée d'un tiret sera considérée comme un amas d'options,
et seront découpées. Ainsi, l'option -xyz
sera analysée comme
-x
, -y
et -z
(sauf si l'une des options -x ou -y accepte un
paramètre, auquel cas il avalera le reste de la chaîne).
Bien que les solutions précédentes puissent vous convenir, il est des
cas où elles ne suffisent pas. Par exemple, les programmes GNU
utilisent souvent des options longues (précédées par un double tiret,
comme --help
), ou alors on peut vouloir ne pas tenir compte de la
casse, ou encore d'autres choses de cet acabit. Pas de problème, car
le module Getopt::Long est là pour vous aider. Getopt::Long est le
véritable couteau suisse des options, et vous y trouverez sûrement
votre bonheur.
Dans sa forme la plus simple, Getopt::Long s'emploie via sa fonction
GetOptions
. GetOptions
s'attend à recevoir une suite de noms
d'options avec une référence à la variable correspondante :
GetOptions( "verbose" => \$verbose, "all" => \$all );
GetOptions
va positionner les variables $verbose et $all. L'appel de
votre script se fait via la notation à double tiret (comme les options
longues) :
$ ./options --verbose
En plus des valeurs booléennes qu'on vient de voir, Getopt::Long
dispose de nouveaux types de variables. Il inclut ainsi les options
négatives et les options incrémentales. Une option négative est une
option booléenne particulière. Elle régira à une commande --option
mais aussi à une commande --nooption
. En initialisant correctement
la valeur correspondant à l'option, on peut imposer un comportement
par défaut facilement modifiable. Pour spécifier une option négative,
il suffit de concaténer un !
après le nom de l'option. Ainsi, en
reprenant notre exemple verbose
précédent, on peut écrire :
my $verbose = 1; GetOptions( "verbose!" => \$verbose );
Et l'utilisateur peut spécifier sur la ligne de commande :
$ ./options --noverbose
pour que le script ait un comportement discret.
Un autre nouveau type est le type incrémental. Ce type se distingue
via un +
concaténé après le nom de la variable (c'est le moyen
standard qu'utilise Getopt::Long pour spécifier un comportement
particulier). A chaque occurrence de l'option sur la ligne de commande,
la valeur sera incrémentée de 1. Ainsi, on peut facilement spécifier
le niveau de verbosité voulu (ou tout autre exemple) :
GetOptions( "verbose+" => \$verbose );
Si l'utilisateur appelle le programme ainsi :
$ ./options --verbose --verbose
alors la variable $verbose vaudra 2.
Les options peuvent aussi attendre un paramètre. Getopt::Std
permettait cela, mais sans faire de contrôle sur la valeur
récupérée. Getopt::Long est beaucoup plus fin, et permet qu'on lui
spécifie le type de paramètre attendu. On peut lui préciser qu'on
attend un argument de type entier, de type réel ou de type chaîne. De
plus, la valeur peut être spécifiée comme nécessaire ou
optionnelle. Pour cela, Getopt::Long utilise une syntaxe très simple :
les arguments optionnels ajoutent un :
après le nom de l'option
alors que les arguments obligatoires sont spécifiés par un =
. Pour
préciser le type, il suffit de rajouter après le =
(ou le :
) un
caractère représentant le type attendu : s
pour une chaîne
(string), i
pour un entier (integer) ou f
pour un réel
(float
). Voici ce que cela donne :
GetOptions( "nb=i" => \$nb, "foo:f" => \$foo );
GetOptions
va alors analyser la ligne de commande et rechercher une
option --nb
suivie d'un entier, et une option --foo
éventuellement
suivie d'un nombre réel. Si elle ne trouve rien qui ressemble à un
float après l'option --foo
, alors la variable $foo sera
positionnée à zéro. S'il n'y a pas de valeur suivant l'option --nb
,
ou si la valeur n'est pas un entier, alors GetOptions se plaindra :
$ ./options.pl --nb Option nb requires an argument $ ./options.pl --nb=12.2 Value "12.2" invalid for option nb (number expected)
Quelquefois, les options acceptent des valeurs multiples. Par exemple, un utilisateur peut spécifier plusieurs endroits où chercher un fichier :
--include include/std --include include/perl
Getopt::Long adaptera son comportement simplement si vous lui spécifiez une référence à un tableau au lieu d'une variable scalaire :
my @include; GetOptions( "include=s" => \@include );
Il est aussi possible de spécifier que la liste ne doit recevoir que des entiers (ou des nombres réels). Pour permettre de spécifier des options multiples séparées par des virgules (ou tout autre séparateur), il suffit de rajouter un post-traitement après l'analyse de la ligne de commande :
my @include = (); GetOptions( "include=s" => \@include ); @include = split /,/, join(',',@include);
Quant aux options avec des valeurs de hash, telles que :
--define os=linux --define arch=i386
vous l'avez deviné, il suffit de passer une référence de hash à GetOptions qui fera Ce Qu'Il Faut :
my %defines = (); GetOptions( "define=s" => \%defines );
Le hash contiendra dans le cas précédent deux clefs, os
et arch
dont les valeurs seront respectivement linux
et i386
. Comme
d'habitude, vous pouvez spécifier que vous attendez des entiers ou des
réels (les clefs seront toujours des chaînes).
Si tout cela ne vous suffit pas, vous pouvez spécifier comme moyen de
contrôle ultime une référence de fonction pour une option
donnée. GetOptions
appellera alors la fonction lorsqu'elle rencontrera
ladite option. La fonction appelée recevra deux paramètres : le nom
de l'option et la valeur correspondante. Voici un exemple, où les
options --verbose
et --quiet
contrôlent la même variable :
my $verbose = ''; # silencieux par défaut GetOptions( "verbose" => \$verbose, "quiet" => sub { $verbose = 0 } );
Ce n'est pas tout. Getopt::Long n'a pas livré tous ses secrets, loin
de là. Voyons tout d'abord comment spécifier des noms d'option
identiques : il suffit de les faire se suivre par un |
. Dans
l'exemple suivant, x
et long
sont une seule et même option (et
attendent un entier) :
GetOptions( "x|long=i" => \$x );
Jusqu'à présent, nous avons utilisé une variable par option. Mais,
tout comme Getopt::Std, il peut recevoir une référence de hash dans
laquelle il stockera les options. Attention, contrairement à
Getopt::Std, la référence au hash est la première valeur à passer à
GetOptions
. Voici un exemple reprenant les différentes syntaxes vues
jusqu'à présent :
GetOptions( \%opts, "verbose+", "x|long=i", "include=s@", "define=s%" );
Regardez comment il suffit d'ajouter un @
ou un %
pour retrouver
le comportement voulu, à savoir une option récupérant une liste ou un
hash.
Si malgré cela, vous n'êtes toujours pas satisfait, alors Getopt::Long
vous fournit une fonction Getopt::Long::Configure qui va vous
permettre d'adapter le comportement de GetOptions
à vos besoins.
Le premier comportement qu'on peut modifier concerne la casse. Par
défaut, Getopt::Long n'est pas sensible à la casse (et --VERBOSE
est
donc équivalent à --verbose
), mais on peut modifier ceci grâce à :
Getopt::Long::Configure( "no_ignorecase" );
Par défaut, les options peuvent être précédées d'un tiret -option
ou d'un double tiret --option
. Une option précédée d'un double
tiret sera toujours interprétée comme une seule option, alors qu'une
option précédée d'un tiret simple pourra être interprétée de deux
manières différentes : selon le comportement du switch -s
ou
selon celui de Getopt::Std. Avec :
Getopt::Long::Configure( "bundling" );
l'option -bar
(en supposant que b
, a
, r
et bar
sont des
options valides) positionnera les variables $b, $a et $r. Pour
positionner $bar, il faut utiliser --bar
. Mais si on avait
utilisé :
Getopt::Long::Configure( "bundling_override" );
alors l'option -bar
aurait positionné la variable $bar. Lorsque le
groupement des options est activé, alors on peut très bien grouper
aussi les valeurs, et se retrouver avec des lignes de commande du
genre :
-x80y24
qui correspondrait à :
-x 80 -y 24
Vous avez dit perturbant ?
La dernière option que nous allons étudier concerne l'abréviation des options. Si nous configurons Getopt::Long avec :
Getopt::Long::Configure( "no_auto_abbrev" );
alors il faudra taper le nom de l'option en entier. Par défaut,
l'abréviation des options est permise, et GetOptions
retrouvera ses
petits tant que l'utilisateur fournit un argument dont la longueur est
suffisante pour retrouver sans ambiguïté l'option correspondante. S'il
y a ambiguïté, alors Getopt::Long imprimera un message d'erreur.
$ ./options.pl -ba Option ba is ambiguous (bar, baz)
Rajoutez à cela la possibilité de fixer un ordre précis pour les
options, la possibilité de mélanger les options avec les non-options
(alors que Getopt::Std ou le switch -s
arrêtent l'analyse dès
qu'ils rencontrent une non-option), et vous aurez quasiment tout ce
qu'il vous faut pour vous emmêler les pinceaux.
Bien sûr, tout comme sa petite sœur getopts
du module Getopt::Std, la
fonction GetOptions renverra une valeur fausse si elle rencontre une
option non définie. Mais comme vous l'avez deviné, ce comportement est
aussi paramétrable, grâce à l'appel :
Getopt::Long::Configure( "pass_through" );
auquel cas GetOptions
analysera la ligne de commande, mais sans faire
d'erreur. Ceci vous permet par la suite d'analyser la ligne de
commande, voire même de refaire un appel à GetOptions
.
Par exemple, on utilisera cette méthode pour faire une première passe pour récupérer un fichier de configuration. On analysera ensuite le fichier de configuration passé en paramètre pour initialiser les valeurs des options, puis on lancera une deuxième analyse de la ligne de commande (après avoir réactivé le contrôle d'erreurs). Ceci permettra à l'utilisateur de surcharger les valeurs du fichier de configuration via la ligne de commande.
Quel que soit le degré de sophistication que vous souhaitez, Perl est
en mesure de vous aider. Ne réinventez pas la roue, et réutilisez du
code éprouvé. Les modules Getopt::Std et Getopt::Long (sans parler du
switch -s
bien entendu) sont fournis en standard avec Perl.
Cependant, vous avez sûrement entendu dire que le Diable se cache
dans les détails
, alors gardez à l'esprit qu'une interface (et donc
des options) standard et facile à appréhender (et à utiliser) est la
garantie d'un programme efficace qu'on aura plaisir à utiliser.
Jérôme Quelin <jquelin@mongueurs.net>
Jérôme Quelin est membre de l'association Mongueurs de Perl (http://www.mongueurs.net) dont l'objet est de promouvoir le langage Perl en France. Il tient à remercier les autres membres de l'association pour leur aide et les relectures effectuées.
Copyright © Les Mongueurs de Perl, 2001-2011
pour le site.
Les auteurs conservent le copyright de leurs articles.