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

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
Chapitre précédent Phase 4 Terminée : Retour au sommaire