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

Paramètres & options de script

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

Copyright © 2003 - Jérôme Quelin.

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

Chapeau

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.

Le nom du script

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.

Jouer avec les paramètres : la manière brute

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).

Un peu plus subtil

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.

Le module Getopt::Std

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).

Le module Getopt::Long

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.

Du plus simple...

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)

Au plus compliqué...

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).

Vous avez dit personnalisé ?

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 } );

Vous en voulez plus ?

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 );

Grouper les variables

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.

Ca ne vous suffit toujours pas...

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.

Le contrôle des options

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.

Conclusion

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.

Auteur

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.

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