# Tutoriel argparse

author: | Tshepang Lekhonkhobe

This tutorial is intended to be a gentle introduction to `argparse`, the recommended command-line parsing module in the Python standard library. This was written for argparse in Python 3. A few details are different in 2.x, especially some exception messages, which were improved in 3.x.

Note

Il y a deux autres modules qui remplissent le même rôle : `getopt` (un équivalent de `getopt()` du langage C) et `optparse` qui est obsolète. Il faut noter que `argparse` est basé sur `optparse` et donc s’utilise de manière très similaire.

## Concepts

Commençons par l’utilisation de la commande ls pour voir le type de fonctionnalité que nous allons étudier dans ce tutoriel d’introduction :

    $ ls
    cpython  devguide  prog.py  pypy  rm-unused-function.patch
    $ ls pypy
    ctypes_configure  demo  dotviewer  include  lib_pypy  lib-python ...
    $ ls -l
    total 20
    drwxr-xr-x 19 wena wena 4096 Feb 18 18:51 cpython
    drwxr-xr-x  4 wena wena 4096 Feb  8 12:04 devguide
    -rwxr-xr-x  1 wena wena  535 Feb 19 00:05 prog.py
    drwxr-xr-x 14 wena wena 4096 Feb  7 00:59 pypy
    -rw-r--r--  1 wena wena  741 Feb 18 01:01 rm-unused-function.patch
    $ ls --help
    Usage: ls [OPTION]... [FILE]...
    List information about the FILEs (the current directory by default).
    Sort entries alphabetically if none of -cftuvSUX nor --sort is specified.
    ...

Quelques concepts que l’on peut apprendre avec les quatre commandes :

- La commande ls est utile quand elle est exécutée sans aucun paramètre. Par défaut cela affiche le contenu du dossier courant.
- Si l’on veut plus que ce qui est proposé par défaut, il faut l’indiquer. Dans le cas présent, on veut afficher un dossier différent : `pypy`. Ce que l’on a fait c’est spécifier un argument positionnel. C’est appelé ainsi car cela permet au programme de savoir quoi faire avec la valeur seulement en se basant sur sa position dans la ligne de commande. Ce concept est plus pertinent pour une commande comme cp dont l’usage de base est `cp SRC DEST`. Le premier argument est ce que vous voulez copier et le second est où vous voulez le copier.
- Maintenant, supposons que l’on veut changer la façon dont le programme agit. Dans notre exemple, on affiche plus d’information pour chaque ficher que simplement leur nom. Dans ce cas, `-l` est un argument facultatif.
- C’est un fragment du texte d’aide. Cela peut être très utile quand on tombe sur un programme que l’on à jamais utilisé auparavant car on peut comprendre son fonctionnement simplement en lisant l’aide associée.

## Les bases

Commençons par un exemple très simple qui ne fait (quasiment) rien :

    import argparse
    parser = argparse.ArgumentParser()
    parser.parse_args()

Ce qui suit est le résultat de l’exécution du code :

    $ python prog.py
    $ python prog.py --help
    usage: prog.py [-h]

    optional arguments:
      -h, --help  show this help message and exit
    $ python prog.py --verbose
    usage: prog.py [-h]
    prog.py: error: unrecognized arguments: --verbose
    $ python prog.py foo
    usage: prog.py [-h]
    prog.py: error: unrecognized arguments: foo

Voilà ce qu’il ce passe :

- Exécuter le script sans aucun paramètre à pour effet que rien est affiché sur la sortie d’erreur. Ce n’est pas très utile.
- Le deuxième commence à montrer l’intérêt du module `argparse`. On a quasiment rien fait mais on a déjà un beau message d’aide.
- L’option `--help`, que l’on peut aussi raccourcir en `-h`, est la seule option que l’on a gratuitement (i.e. pas besoin de la préciser). Préciser quoi que ce soit d’autre entrainera une erreur. Mais même dans ce cas, on reçoit aussi un message utile, toujours gratuitement.

## Introduction aux arguments positionnels

Un exemple :

    import argparse
    parser = argparse.ArgumentParser()
    parser.add_argument("echo")
    args = parser.parse_args()
    print args.echo

On exécute le code :

    $ python prog.py
    usage: prog.py [-h] echo
    prog.py: error: the following arguments are required: echo
    $ python prog.py --help
    usage: prog.py [-h] echo

    positional arguments:
      echo

    optional arguments:
      -h, --help  show this help message and exit
    $ python prog.py foo
    foo

Voilà ce qu’il ce passe :

- On a ajouté la méthode `add_argument()` que l’on utilise pour préciser quels paramètre de lignes de commandes le programme peut accepter. Dans le cas présent, je l’ai appelé `echo` pour que cela corresponde à sa fonction.
- Utiliser le programme nécessite maintenant que l’on précise un paramètre.
- La méthode `parse_args()` renvoie en réalité certaines données des paramètres précisées, dans le cas présent : `echo`.
- La variable est comme une forme de “magie” que `argparse` effectue gratuitement (i.e. pas besoin de préciser dans quelle variable la valeur est stockée). Vous aurez aussi remarqué que le nom est le même que l’argument en chaîne de caractère donné à la méthode : `echo`.

Notez cependant que, même si l’affichage d’aide paraît bien , il n’est pas aussi utile qu’il pourrait l’être. Par exemple, on peut lire que `echo` est un argument positionnel mais on ne peut pas savoir ce que cela fait autrement qu’en le devinant ou en lisant le code source. Donc, rendons le un peu plus utile :

    import argparse
    parser = argparse.ArgumentParser()
    parser.add_argument("echo", help="echo the string you use here")
    args = parser.parse_args()
    print args.echo

Et on obtient :

    $ python prog.py -h
    usage: prog.py [-h] echo

    positional arguments:
      echo        echo the string you use here

    optional arguments:
      -h, --help  show this help message and exit

Maintenant, qu’en dîtes vous s’il on fait quelque chose d’encore plus utile :

    import argparse
    parser = argparse.ArgumentParser()
    parser.add_argument("square", help="display a square of a given number")
    args = parser.parse_args()
    print args.square**2

Ce qui suit est le résultat de l’exécution du code :

    $ python prog.py 4
    Traceback (most recent call last):
      File "prog.py", line 5, in <module>
        print args.square**2
    TypeError: unsupported operand type(s) for ** or pow(): 'str' and 'int'

Cela n’a pas très bien fonctionné. C’est parce que `argparse` traite les paramètres que l’on donne comme des chaînes de caractères à moins qu’on ne lui indique de faire autrement. Donc, disons à `argparse` de traiter cette entrée comme un entier :

    import argparse
    parser = argparse.ArgumentParser()
    parser.add_argument("square", help="display a square of a given number",
                        type=int)
    args = parser.parse_args()
    print args.square**2

Ce qui suit est le résultat de l’exécution du code :

    $ python prog.py 4
    16
    $ python prog.py four
    usage: prog.py [-h] square
    prog.py: error: argument square: invalid int value: 'four'

Cela a bien fonctionné. Maintenant le programme va même s’arrêter si l’entrée n’est pas légale avant de procéder à l’exécution.

## Introduction aux arguments optionnels

So far we have been playing with positional arguments. Let us have a look on how to add optional ones:

    import argparse
    parser = argparse.ArgumentParser()
    parser.add_argument("--verbosity", help="increase output verbosity")
    args = parser.parse_args()
    if args.verbosity:
        print "verbosity turned on"

Et le résultat :

    $ python prog.py --verbosity 1
    verbosity turned on
    $ python prog.py
    $ python prog.py --help
    usage: prog.py [-h] [--verbosity VERBOSITY]

    optional arguments:
      -h, --help            show this help message and exit
      --verbosity VERBOSITY
                            increase output verbosity
    $ python prog.py --verbosity
    usage: prog.py [-h] [--verbosity VERBOSITY]
    prog.py: error: argument --verbosity: expected one argument

Voilà ce qu’il ce passe :

- Le programme est écrit de sorte qu’il n’affiche rien sauf si l’option `--verbosity` est précisée.
- Pour montrer que l’option est bien optionnelle il n’y aura pas d’erreur s’il on exécute le programme sans celle-ci. Notez que par défaut, si une option n’est pas utilisée, la variable associée, dans le cas présent : `args.verbosity`, prend la valeur `None` c’est pourquoi elle échoue le test de vérité de l’assertion `if`.
- Le message d’aide est quelque peu différent.
- Quand on utilise l’option `--verbosity` on doit aussi préciser une valeur, n’importe laquelle.

L’exemple ci-dessus accepte des valeurs entières arbitraires pour `--verbosity` mais pour notre programme simple seule deux valeurs sont réellement utiles : `True` et `False`. Modifions le code en accord avec cela :

    import argparse
    parser = argparse.ArgumentParser()
    parser.add_argument("--verbose", help="increase output verbosity",
                        action="store_true")
    args = parser.parse_args()
    if args.verbose:
       print "verbosity turned on"

Et le résultat :

    $ python prog.py --verbose
    verbosity turned on
    $ python prog.py --verbose 1
    usage: prog.py [-h] [--verbose]
    prog.py: error: unrecognized arguments: 1
    $ python prog.py --help
    usage: prog.py [-h] [--verbose]

    optional arguments:
      -h, --help  show this help message and exit
      --verbose   increase output verbosity

Voilà ce qu’il ce passe :

- Maintenant le paramètre est plus une option que quelque chose qui nécessite une valeur. On a même changé le nom du paramètre pour qu’il corresponde à cette idée. Notez que maintenant on précise une nouvelle `action` clavier et qu’on lui donne la valeur `"store_true"`. Cela signifie que si l’option est précisée la valeur `True` est assignée à `args.verbose`. Ne rien préciser implique la valeur `False`.
- Dans l’esprit de ce que sont vraiment les options, pas des paramètres, il se plaint quand vous tentez de préciser une valeur.
- Notez que l’aide est différente.

### Les paramètres raccourcis

Si vous êtes familier avec l’utilisation des ligne de commande vous avez dû remarqué que je n’ai pour l’instant rien dit au sujet des versions raccourcies des paramètres. C’est très simple :

    import argparse
    parser = argparse.ArgumentParser()
    parser.add_argument("-v", "--verbose", help="increase output verbosity",
                        action="store_true")
    args = parser.parse_args()
    if args.verbose:
        print "verbosity turned on"

Et voilà :

    $ python prog.py -v
    verbosity turned on
    $ python prog.py --help
    usage: prog.py [-h] [-v]

    optional arguments:
      -h, --help     show this help message and exit
      -v, --verbose  increase output verbosity

Notez que la nouvelle option est aussi indiquée dans l’aide.

## Combinaison d’arguments positionnels et optionnels

Notre programme continue de grandir en complexité :

    import argparse
    parser = argparse.ArgumentParser()
    parser.add_argument("square", type=int,
                        help="display a square of a given number")
    parser.add_argument("-v", "--verbose", action="store_true",
                        help="increase output verbosity")
    args = parser.parse_args()
    answer = args.square**2
    if args.verbose:
        print "the square of {} equals {}".format(args.square, answer)
    else:
        print answer

Et voilà le résultat :

    $ python prog.py
    usage: prog.py [-h] [-v] square
    prog.py: error: the following arguments are required: square
    $ python prog.py 4
    16
    $ python prog.py 4 --verbose
    the square of 4 equals 16
    $ python prog.py --verbose 4
    the square of 4 equals 16

- Nous avons ajouté un argument nommé, d’où le message d’erreur.
- Notez que l’ordre importe peu.

Qu’en est il si nous donnons à ce programme la possibilité d’avoir plusieurs niveaux de verbosité, et que celui-ci les prend en compte :

    import argparse
    parser = argparse.ArgumentParser()
    parser.add_argument("square", type=int,
                        help="display a square of a given number")
    parser.add_argument("-v", "--verbosity", type=int,
                        help="increase output verbosity")
    args = parser.parse_args()
    answer = args.square**2
    if args.verbosity == 2:
        print "the square of {} equals {}".format(args.square, answer)
    elif args.verbosity == 1:
        print "{}^2 == {}".format(args.square, answer)
    else:
        print answer

Et le résultat :

    $ python prog.py 4
    16
    $ python prog.py 4 -v
    usage: prog.py [-h] [-v VERBOSITY] square
    prog.py: error: argument -v/--verbosity: expected one argument
    $ python prog.py 4 -v 1
    4^2 == 16
    $ python prog.py 4 -v 2
    the square of 4 equals 16
    $ python prog.py 4 -v 3
    16

Tout semble bon sauf le dernier, qui montre que notre programme contient un bogue. Corrigeons cela en restreignant les valeurs que `--verbosity` accepte :

    import argparse
    parser = argparse.ArgumentParser()
    parser.add_argument("square", type=int,
                        help="display a square of a given number")
    parser.add_argument("-v", "--verbosity", type=int, choices=[0, 1, 2],
                        help="increase output verbosity")
    args = parser.parse_args()
    answer = args.square**2
    if args.verbosity == 2:
        print "the square of {} equals {}".format(args.square, answer)
    elif args.verbosity == 1:
        print "{}^2 == {}".format(args.square, answer)
    else:
        print answer

Et le résultat :

    $ python prog.py 4 -v 3
    usage: prog.py [-h] [-v {0,1,2}] square
    prog.py: error: argument -v/--verbosity: invalid choice: 3 (choose from 0, 1, 2)
    $ python prog.py 4 -h
    usage: prog.py [-h] [-v {0,1,2}] square

    positional arguments:
      square                display a square of a given number

    optional arguments:
      -h, --help            show this help message and exit
      -v {0,1,2}, --verbosity {0,1,2}
                            increase output verbosity

Notez que ce changement est pris en compte à la fois dans le message d’erreur et dans le texte d’aide.

Essayons maintenant une approche différente pour jouer sur la verbosité, ce qui arrive fréquemment. Cela correspond également à comment le programme CPython gère ses propres paramètres de verbosité (jetez un œil sur la sortie de la commande `python --help`) :

    import argparse
    parser = argparse.ArgumentParser()
    parser.add_argument("square", type=int,
                        help="display the square of a given number")
    parser.add_argument("-v", "--verbosity", action="count",
                        help="increase output verbosity")
    args = parser.parse_args()
    answer = args.square**2
    if args.verbosity == 2:
        print "the square of {} equals {}".format(args.square, answer)
    elif args.verbosity == 1:
        print "{}^2 == {}".format(args.square, answer)
    else:
        print answer

Nous avons introduit une autre action, `"count"`, pour compter le nombre d’occurrences d’une argument optionnel en particulier :

    $ python prog.py 4
    16
    $ python prog.py 4 -v
    4^2 == 16
    $ python prog.py 4 -vv
    the square of 4 equals 16
    $ python prog.py 4 --verbosity --verbosity
    the square of 4 equals 16
    $ python prog.py 4 -v 1
    usage: prog.py [-h] [-v] square
    prog.py: error: unrecognized arguments: 1
    $ python prog.py 4 -h
    usage: prog.py [-h] [-v] square

    positional arguments:
      square           display a square of a given number

    optional arguments:
      -h, --help       show this help message and exit
      -v, --verbosity  increase output verbosity
    $ python prog.py 4 -vvv
    16

- Oui, c’est maintenant d’avantage une option (similaire à `action="store_true"`) de la version précédente de notre script. Cela devrait expliquer le message d’erreur.
- Cela se comporte de la même manière que l’action `"store_true"`.
- Maintenant voici une démonstration de ce que l’action `"count"` fait. Vous avez sûrement vu ce genre d’utilisation auparavant.
- And, just like the « store_true » action, if you don’t specify the `-v` flag, that flag is considered to have `None` value.
- Comme on s’y attend, en spécifiant l’option dans sa forme longue, on devrait obtenir la même sortie.
- Malheureusement, notre sortie d’aide n’est pas très informative à propos des nouvelles possibilités de notre programme, mais cela peut toujours être corrigé en améliorant sa documentation. (e.g. en utilisant le l’argument `help`).
- La dernière sortie du programme montre que celui-ci contient un bogue.

Corrigeons :

    import argparse
    parser = argparse.ArgumentParser()
    parser.add_argument("square", type=int,
                        help="display a square of a given number")
    parser.add_argument("-v", "--verbosity", action="count",
                        help="increase output verbosity")
    args = parser.parse_args()
    answer = args.square**2

    # bugfix: replace == with >=
    if args.verbosity >= 2:
        print "the square of {} equals {}".format(args.square, answer)
    elif args.verbosity >= 1:
        print "{}^2 == {}".format(args.square, answer)
    else:
        print answer

Et c’est ce que ça donne :

    $ python prog.py 4 -vvv
    the square of 4 equals 16
    $ python prog.py 4 -vvvv
    the square of 4 equals 16
    $ python prog.py 4
    Traceback (most recent call last):
      File "prog.py", line 11, in <module>
        if args.verbosity >= 2:
    TypeError: unorderable types: NoneType() >= int()

- Les premières exécutions du programme sont correctes, et le bogue que nous avons eu est corrigé. Cela dit, nous voulons que n’importe quelle valeur >= 2 rende le programme aussi verbeux que possible.
- La troisième sortie de programme n’est pas si bien que ça.

Corrigeons ce bogue :

    import argparse
    parser = argparse.ArgumentParser()
    parser.add_argument("square", type=int,
                        help="display a square of a given number")
    parser.add_argument("-v", "--verbosity", action="count", default=0,
                        help="increase output verbosity")
    args = parser.parse_args()
    answer = args.square**2
    if args.verbosity >= 2:
        print "the square of {} equals {}".format(args.square, answer)
    elif args.verbosity >= 1:
        print "{}^2 == {}".format(args.square, answer)
    else:
        print answer

Nous venons juste d’introduire un nouveau mot clef, `default`. Nous l’avons définit à `0` pour le rendre comparable aux autres valeurs. Rappelez-vous que par défaut, si un argument optionnel n’est pas spécifié, il sera définit à `None`, et ne pourra pas être comparé à une valeur de type entier (Une erreur `TypeError` serait alors levée).

Et :

    $ python prog.py 4
    16

Vous pouvez aller assez loin seulement avec ce que nous avons appris jusqu’à maintenant, et nous n’avons qu’aperçu la surface. Le module `argparse` est très puissant, et nous allons l’explorer un peu plus avant la fin de ce tutoriel.

## Aller un peu plus loin

Qu’en est il si nous souhaitons étendre notre mini programme pour le rendre capable de calculer d’autres puissances, et pas seulement des carrés :

    import argparse
    parser = argparse.ArgumentParser()
    parser.add_argument("x", type=int, help="the base")
    parser.add_argument("y", type=int, help="the exponent")
    parser.add_argument("-v", "--verbosity", action="count", default=0)
    args = parser.parse_args()
    answer = args.x**args.y
    if args.verbosity >= 2:
        print "{} to the power {} equals {}".format(args.x, args.y, answer)
    elif args.verbosity >= 1:
        print "{}^{} == {}".format(args.x, args.y, answer)
    else:
        print answer

Sortie :

    $ python prog.py
    usage: prog.py [-h] [-v] x y
    prog.py: error: the following arguments are required: x, y
    $ python prog.py -h
    usage: prog.py [-h] [-v] x y

    positional arguments:
      x                the base
      y                the exponent

    optional arguments:
      -h, --help       show this help message and exit
      -v, --verbosity
    $ python prog.py 4 2 -v
    4^2 == 16

Il est à noter que jusqu’à présent nous avons utilisé le niveau de verbosité pour changer le texte qui est affiché. L’exemple suivant au contraire utilise le niveau de verbosité pour afficher plus de texte à la place :

    import argparse
    parser = argparse.ArgumentParser()
    parser.add_argument("x", type=int, help="the base")
    parser.add_argument("y", type=int, help="the exponent")
    parser.add_argument("-v", "--verbosity", action="count", default=0)
    args = parser.parse_args()
    answer = args.x**args.y
    if args.verbosity >= 2:
        print "Running '{}'".format(__file__)
    if args.verbosity >= 1:
        print "{}^{} ==".format(args.x, args.y),
    print answer

Sortie :

    $ python prog.py 4 2
    16
    $ python prog.py 4 2 -v
    4^2 == 16
    $ python prog.py 4 2 -vv
    Running 'prog.py'
    4^2 == 16

### Paramètres en conflit

Jusque là, nous avons travaillé avec deux méthodes d’une instance de `argparse.ArgumentParser`. En voici une troisième, `add_mutually_exclusive_group()`. Elle nous permet de spécifier des paramètres qui sont en conflit entre eux. Changeons aussi le reste du programme de telle sorte que la nouvelle fonctionnalité fasse sens : nous allons introduire l’option `--quiet`, qui va avoir l’effet opposé de l’option `--verbose` :

    import argparse

    parser = argparse.ArgumentParser()
    group = parser.add_mutually_exclusive_group()
    group.add_argument("-v", "--verbose", action="store_true")
    group.add_argument("-q", "--quiet", action="store_true")
    parser.add_argument("x", type=int, help="the base")
    parser.add_argument("y", type=int, help="the exponent")
    args = parser.parse_args()
    answer = args.x**args.y

    if args.quiet:
        print answer
    elif args.verbose:
        print "{} to the power {} equals {}".format(args.x, args.y, answer)
    else:
        print "{}^{} == {}".format(args.x, args.y, answer)

Notre programme est maintenant plus simple, et nous avons perdu des fonctionnalités pour faire cette démonstration. Peu importe, voici la sortie du programme :

    $ python prog.py 4 2
    4^2 == 16
    $ python prog.py 4 2 -q
    16
    $ python prog.py 4 2 -v
    4 to the power 2 equals 16
    $ python prog.py 4 2 -vq
    usage: prog.py [-h] [-v | -q] x y
    prog.py: error: argument -q/--quiet: not allowed with argument -v/--verbose
    $ python prog.py 4 2 -v --quiet
    usage: prog.py [-h] [-v | -q] x y
    prog.py: error: argument -q/--quiet: not allowed with argument -v/--verbose

Cela devrait être facile à suivre. J’ai ajouté cette dernière sortie pour que vous puissiez voir le genre de flexibilité que vous pouvez avoir, i.e. faire un mixe entre des paramètres courts et longs.

Avant d’en finir, vous voudrez certainement dire à vos utilisateurs quel est le but principal de votre programme, juste dans le cas ou ils ne savent pas :

    import argparse

    parser = argparse.ArgumentParser(description="calculate X to the power of Y")
    group = parser.add_mutually_exclusive_group()
    group.add_argument("-v", "--verbose", action="store_true")
    group.add_argument("-q", "--quiet", action="store_true")
    parser.add_argument("x", type=int, help="the base")
    parser.add_argument("y", type=int, help="the exponent")
    args = parser.parse_args()
    answer = args.x**args.y

    if args.quiet:
        print answer
    elif args.verbose:
        print "{} to the power {} equals {}".format(args.x, args.y, answer)
    else:
        print "{}^{} == {}".format(args.x, args.y, answer)

Notez cette nuance dans le texte d’utilisation. Les options [-v | -q]`, qui nous disent que nous pouvons utiliser au choix `-v` ou `-q`, mais pas les deux ensemble :

    $ python prog.py --help
    usage: prog.py [-h] [-v | -q] x y

    calculate X to the power of Y

    positional arguments:
      x              the base
      y              the exponent

    optional arguments:
      -h, --help     show this help message and exit
      -v, --verbose
      -q, --quiet

## Conclusion

Le module `argparse` offre bien plus que ce qui est montré ici. Sa documentation est assez détaillée complète et pleine d’exemples. En ayant accompli ce tutoriel, vous pourriez facilement comprendre cette documentation sans vous sentir dépassé.