4.5 CLI avec argparse
En PHP CLI, on utilise $argv et getopt() â fonctionnel mais peu pratique : pas de --help automatique, pas de validation de types. Python a argparse dans la stdlib qui gĂ©nĂšre le --help, valide les types, gĂšre les valeurs par dĂ©faut et documente chaque argument, tout seul.
1. Structure de base
import argparse
parser = argparse.ArgumentParser(
prog="monoutil",
description="Description affichée dans --help"
)
# Argument positionnel (obligatoire, sans --)
parser.add_argument("fichier", help="Le fichier Ă traiter")
# Argument optionnel
parser.add_argument("--format", default="json", help="Format de sortie (défaut: json)")
# Flag booléen
parser.add_argument("--verbose", action="store_true", help="Mode verbeux")
args = parser.parse_args()
print(args.fichier) # valeur du positionnel
print(args.format) # "json" si non fourni
print(args.verbose) # True ou False
python monoutil.py --help
usage: monoutil [-h] [--format FORMAT] [--verbose] fichier
Description affichée dans --help
positional arguments:
fichier Le fichier Ă traiter
options:
-h, --help show this help message and exit
--format FORMAT Format de sortie (défaut: json)
--verbose Mode verbeux
2. Types, validation, raccourcis
argparse valide automatiquement les types et rejette les valeurs invalides avec un message propre.
parser.add_argument("--port", type=int, default=8080, help="Port d'écoute")
parser.add_argument("--timeout", type=float, default=30.0)
parser.add_argument("-o", "--output", # -o est le raccourci de --output
default="sbom.json",
metavar="FILE", # nom affiché dans --help (au lieu de OUTPUT)
help="Fichier de sortie"
)
parser.add_argument("--format",
choices=["json", "xml"], # valeurs autorisées
default="json"
)
# Si l'utilisateur passe --port abc :
# error: argument --port: invalid int value: 'abc'
# Le programme s'arrĂȘte avec un message clair â sans try/except Ă Ă©crire
type=intâ conversion automatique + message d'erreur si invalidemetavarâ contrĂŽle le placeholder dans le--helpchoicesâ liste blanche de valeurs acceptĂ©esrequired=Trueâ argument optionnel rendu obligatoire
3. Application projet : CLI pyCycloneFlow finale
On remplace le main() codé en dur par une vraie CLI. L'utilisateur passe les fichiers à scanner et le fichier de sortie.
$ python main.py --pip requirements.txt -o sbom.json
$ python main.py --pip scanner.txt --verbose
$ python main.py --help
# main.py
import argparse
import pathlib
import logging
from models import SBOM
from parsers.pip import parse_pip
def load_file(path: str) -> str:
try:
return pathlib.Path(path).read_text(encoding="utf-8")
except FileNotFoundError:
logging.error(f"Fichier introuvable : {path}")
return ""
except PermissionError:
logging.error(f"Permission refusée : {path}")
return ""
except Exception as e:
logging.error(f"Erreur inattendue : {e}")
return ""
def build_parser() -> argparse.ArgumentParser:
p = argparse.ArgumentParser(
prog="pycycloneflow",
description="GénÚre un SBOM CycloneDX v1.7 depuis plusieurs écosystÚmes"
)
p.add_argument(
"--pip",
metavar="FILE",
help="Fichier pip list (sortie de 'pip list')"
)
p.add_argument(
"-o", "--output",
default="sbom.json",
metavar="FILE",
help="Fichier SBOM de sortie (défaut : sbom.json)"
)
p.add_argument(
"-v", "--verbose",
action="store_true",
help="Affiche les détails de chaque composant"
)
return p
def main():
args = build_parser().parse_args()
level = logging.DEBUG if args.verbose else logging.WARNING
logging.basicConfig(level=level, format='%(levelname)s: %(message)s')
components = []
if args.pip:
raw = load_file(args.pip)
components.extend(parse_pip(raw))
if not components:
print("Aucun fichier fourni. Utilisez --pip FILE")
build_parser().print_usage()
return
sbom = SBOM(components)
sbom.to_json_file(args.output)
print(f"â {len(components)} composants â {args.output}")
if args.verbose:
for c in components:
print(f" â {c} â {c.purl}")
if __name__ == "__main__":
main()
Exemples d'utilisation :
$ python main.py --pip scanner.txt
[timer] parse_pip 0.0004s
â 3 composants â sbom.json
$ python main.py --pip scanner.txt -v
[timer] parse_pip 0.0004s
â 3 composants â sbom.json
â requests == 2.28.1 â pkg:pypi/requests@2.28.1
â flask == 2.2.2 â pkg:pypi/flask@2.2.2
â cryptography == 38.0.1 â pkg:pypi/cryptography@38.0.1
$ python main.py --pip scanner.txt -o rapport.json
[timer] parse_pip 0.0004s
â 3 composants â rapport.json
$ python main.py --help
usage: pycycloneflow [-h] [--pip FILE] [-o FILE] [-v]
GénÚre un SBOM CycloneDX v1.7 depuis plusieurs écosystÚmes
options:
-h, --help show this help message and exit
--pip FILE Fichier pip list (sortie de 'pip list')
-o FILE, --output FILE
Fichier SBOM de sortie (défaut : sbom.json)
-v, --verbose Affiche les détails de chaque composant
Structure finale du projet Ă l'issue de la Phase 4 :
pyCycloneFlow/
âââ models/
â âââ __init__.py # from .component import Component; from .sbom import SBOM
â âââ component.py # Component + @property version + purl
â âââ sbom.py # SBOM + to_cyclonedx_json + to_json_file
âââ parsers/
â âââ __init__.py # from .pip import parse_pip
â âââ pip.py # _pip_components() gĂ©nĂ©rateur + @timer parse_pip()
âââ utils/
â âââ __init__.py
â âââ decorators.py # @timer
âââ tests/
â âââ fixtures/
â â âââ pip_list.txt
â âââ test_opener.py
âââ main.py # build_parser() + main() argparse