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.
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.
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 |
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,Nonesi absente.choices=[...]limite les valeurs ;--helpest fourni tout seul.if __name__ == "__main__":est vrai seulement dans le fichier lancé directement.loggingécrit sur stderr,printsur stdout. Niveaux : DEBUG < INFO < WARNING < ERROR.basicConfig(level=...)fixe le seuil ;-vle 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.
Ta note
Cartes · 8
Réviser les cartes ici
Pourquoi