Tom Daluzeau
← Parcours Python · fichiers
Python · fichiers · leçon 4 / 4Ch. 3 · Outil en ligne de commande13 minTerminée

Outil en ligne de commande : argparse, logging, codes de sortie

Lire les arguments de la ligne de commande avec argparse, ranger le programme dans main(), écrire des messages de suivi avec logging et renvoyer le bon code de sortie (0, 1 ou 2) au shell.

Mis à jour le

À recouper avec le cours. Cette leçon a été écrite à partir du plan du chapitre, avant relecture avec les supports. En cas de doute, le cours fait foi.

Pourquoi on voit ça

Jusqu’ici, le nom du fichier d’inventaire était écrit dans le code. Un vrai outil se pilote depuis le terminal :

python3 fleetcheck.py servers.json --env prod -v

Il doit aussi bien se comporter quand un autre programme le lance : un script shell, une tâche planifiée, une chaîne d’intégration continue. Pour ça, trois habitudes : lire ses arguments, séparer ses messages de suivi de ses résultats, et répondre par un code de sortie. C’est ce qui fait la différence entre un script et un outil.

argparse : lire la ligne de commande

import argparse

parser = argparse.ArgumentParser(description="Vérifie un parc de serveurs.")
parser.add_argument("file", help="fichier d'inventaire (JSON)")
parser.add_argument("--env", choices=["prod", "dev"], help="ne garder qu'un environnement")
parser.add_argument("-v", "--verbose", action="store_true", help="afficher plus de détails")
args = parser.parse_args()

print(args.file, args.env, args.verbose)

Chaque add_argument déclare un argument. parse_args() lit la ligne de commande et range les valeurs dans args, qu’on lit avec un point : args.file.

python3 fleetcheck.py servers.json
python3 fleetcheck.py servers.json --env prod -v
servers.json None False
servers.json prod True

Argument positionnel contre option

Positionnel Option
Déclaration add_argument("file") add_argument("--env")
Sur la ligne de commande la valeur seule : servers.json le nom puis la valeur : --env prod
Obligatoire ? oui non
Si on ne le donne pas erreur d’usage vaut None

Un nom sans tirets est un positionnel, repéré par sa place. Un nom avec tirets est une option. -v et --verbose sont la forme courte et la forme longue de la même option.

action="store_true" fait de -v un interrupteur : pas de valeur derrière, args.verbose vaut True si l’option est là, False sinon.

python3 fleetcheck.py servers.json --env prod -v le programme lancé positionnel args.file obligatoire option + valeur args.env = 'prod' facultative interrupteur args.verbose = True
Sans tirets : un positionnel, repéré par sa place. Avec tirets : une option, nommée, qu'on peut omettre.

choices

choices=["prod", "dev"] limite les valeurs acceptées. Tu n’as pas à vérifier toi-même :

python3 fleetcheck.py servers.json --env test
usage: fleetcheck.py [-h] [--env {prod,dev}] [-v] file
fleetcheck.py: error: argument --env: invalid choice: 'test' (choose from prod, dev)

argparse affiche l’erreur et arrête le script avec le code de sortie 2. Même chose si file manque.

–help

L’aide est fabriquée toute seule à partir de description et des help :

python3 fleetcheck.py --help
usage: fleetcheck.py [-h] [--env {prod,dev}] [-v] file

Vérifie un parc de serveurs.

positional arguments:
  file              fichier d'inventaire (JSON)

options:
  -h, --help        show this help message and exit
  --env {prod,dev}  ne garder qu'un environnement
  -v, --verbose     afficher plus de détails

Après --help, le script s’arrête avec le code 0.

main() et if name == “main”:

On range le programme dans une fonction main(), appelée tout en bas du fichier :

def main():
    print("vérification du parc")

if __name__ == "__main__":
    main()

__name__ est une variable que Python remplit tout seul :

  • dans le fichier lancé directement (python3 tools.py), elle vaut "__main__" ;
  • dans un fichier importé (import tools), elle vaut le nom du module : "tools".

La condition veut donc dire : « n’exécute main() que si on m’a lancé directement ». On peut ainsi importer le fichier, pour réutiliser ses fonctions ou les tester, sans déclencher tout le programme.

logging : les messages de suivi

print sert au résultat du programme. Pour raconter ce qui se passe (fichier lu, serveur lent, erreur), on utilise logging.

import logging

logging.basicConfig(level=logging.INFO)
log = logging.getLogger("fleetcheck")

log.debug("lecture de servers.json")
log.info("2 serveurs chargés")
log.warning("db-1 répond lentement")
log.error("cache-1 injoignable")
print("web-1 ok")
INFO:fleetcheck:2 serveurs chargés
WARNING:fleetcheck:db-1 répond lentement
ERROR:fleetcheck:cache-1 injoignable
web-1 ok

Chaque ligne indique le niveau, le nom donné à getLogger, puis le message.

Les niveaux

Niveau Sert à
DEBUG détails pour comprendre un problème
INFO déroulement normal
WARNING quelque chose d’anormal, mais on continue
ERROR une opération a échoué

Ils sont rangés du moins grave au plus grave. basicConfig(level=...) fixe un seuil : les messages de ce niveau et au-dessus s’affichent. Avec logging.INFO, le debug est ignoré. Sans basicConfig, le seuil est WARNING.

logging écrit sur stderr

Un programme a deux sorties : la sortie standard (stdout) où écrit print, et la sortie d’erreur (stderr) où écrit logging. Dans le terminal, on voit les deux mélangées. Mais le > du shell ne redirige que stdout :

python3 check.py > out.txt

Le fichier out.txt ne reçoit que web-1 ok. Les trois messages de log restent à l’écran. Le résultat reste donc propre, utilisable par un autre programme.

check.py print et logging stdout print, redirigé par > stderr logging, pas redirigé out.txt web-1 ok le terminal WARNING:fleetcheck:… $ python3 check.py > out.txt
Le résultat (stdout) part dans le fichier ; les messages de suivi (stderr) restent à l'écran.

L’option -v

On branche -v sur le seuil : sans elle on s’arrête à INFO, avec elle on descend à DEBUG.

level = logging.INFO
if args.verbose:
    level = logging.DEBUG
logging.basicConfig(level=level)

Codes de sortie : 0, 1, 2

Code Signification Qui le renvoie
0 tout s’est bien passé ton code
1 le script a tourné, mais il y a un échec (fichier inutilisable, serveur en panne) ton code
2 mauvaise utilisation (argument manquant, valeur refusée) argparse, tout seul
commande arguments OK ? non oui main() échec ? oui non $? vaut 2 argparse sort tout seul return 1 → $? vaut 1 fichier inutilisable… return 0 → $? vaut 0 tout va bien
2 vient d'argparse avant même que ton code tourne ; 0 et 1 sont renvoyés par main().

L’écriture habituelle : main() renvoie le code, et la dernière ligne le transmet au shell.

import argparse
import json
import logging
import sys

log = logging.getLogger("fleetcheck")


def main():
    parser = argparse.ArgumentParser(description="Vérifie un parc de serveurs.")
    parser.add_argument("file")
    parser.add_argument("-v", "--verbose", action="store_true")
    args = parser.parse_args()

    level = logging.INFO
    if args.verbose:
        level = logging.DEBUG
    logging.basicConfig(level=level)

    log.debug(f"lecture de {args.file}")
    try:
        with open(args.file, encoding="utf-8") as f:
            servers = json.load(f)
    except (OSError, json.JSONDecodeError) as e:
        log.error(f"fichier inutilisable : {e}")
        return 1

    log.info(f"{len(servers)} serveurs chargés")
    return 0


if __name__ == "__main__":
    sys.exit(main())
Commande Ce qui s’affiche echo $?
python3 fleetcheck.py servers.json INFO:fleetcheck:2 serveurs chargés 0
python3 fleetcheck.py absent.json ERROR:fleetcheck:fichier inutilisable : … 1
python3 fleetcheck.py usage: … et error: … 2

À retenir

  • add_argument("file") : positionnel, obligatoire. add_argument("--env") : option, facultative, None si absente.
  • choices=[...] limite les valeurs ; --help est fourni tout seul.
  • if __name__ == "__main__": est vrai seulement dans le fichier lancé directement.
  • logging écrit sur stderr, print sur stdout. Niveaux : DEBUG < INFO < WARNING < ERROR.
  • basicConfig(level=...) fixe le seuil ; -v le descend à DEBUG.
  • Codes de sortie : 0 succès, 1 échec, 2 mauvaise utilisation (argparse). sys.exit(main()).

Entraînement

12 questions, dont 6 à compléter. Tu réponds, on passe à la suivante. La note arrive à la fin.

Cartes · 8

Réviser les cartes ici

Espace : retourner