Module 13 - Création d'Outils CLI
Créer des interfaces en ligne de commande professionnelles.
Durée estimée : 15 minutes
Objectifs du Module
- Maîtriser argparse pour les CLI basiques
- Utiliser Click pour des CLI avancées
- Formater les sorties avec Rich
- Créer des outils interactifs
1. argparse - La Bibliothèque Standard
CLI Simple
#!/usr/bin/env python3
import argparse
def main():
parser = argparse.ArgumentParser(
description="Outil de gestion de serveurs"
)
parser.add_argument(
"server",
help="Nom ou IP du serveur"
)
parser.add_argument(
"-p", "--port",
type=int,
default=22,
help="Port SSH (défaut: 22)"
)
parser.add_argument(
"-v", "--verbose",
action="store_true",
help="Mode verbeux"
)
args = parser.parse_args()
print(f"Connexion à {args.server}:{args.port}")
if args.verbose:
print("Mode verbeux activé")
if __name__ == "__main__":
main()
# Usage:
# python script.py web01 -p 2222 -v
# python script.py --help
Types d'Arguments
import argparse
parser = argparse.ArgumentParser()
# Argument positionnel obligatoire
parser.add_argument("filename")
# Argument optionnel avec valeur par défaut
parser.add_argument("-n", "--number", type=int, default=10)
# Flag booléen
parser.add_argument("-v", "--verbose", action="store_true")
parser.add_argument("-q", "--quiet", action="store_false", dest="verbose")
# Compteur (-vvv = 3)
parser.add_argument("-v", "--verbose", action="count", default=0)
# Choix limités
parser.add_argument(
"-f", "--format",
choices=["json", "yaml", "csv"],
default="json"
)
# Liste de valeurs
parser.add_argument(
"-H", "--host",
action="append",
help="Ajouter un host (peut être répété)"
)
# Usage: -H host1 -H host2
# Plusieurs valeurs en une fois
parser.add_argument(
"files",
nargs="+", # Au moins un
help="Fichiers à traiter"
)
# Usage: script.py file1.txt file2.txt
# Valeur optionnelle (0 ou 1)
parser.add_argument(
"-c", "--config",
nargs="?",
const="config.yml",
default=None
)
# Argument requis
parser.add_argument("-t", "--token", required=True)
Sous-commandes
import argparse
def cmd_start(args):
print(f"Starting {args.service}")
def cmd_stop(args):
print(f"Stopping {args.service}")
def cmd_status(args):
print(f"Status of {args.service}")
parser = argparse.ArgumentParser(prog="sysctl")
subparsers = parser.add_subparsers(dest="command", help="Commandes disponibles")
# Sous-commande start
start_parser = subparsers.add_parser("start", help="Démarrer un service")
start_parser.add_argument("service", help="Nom du service")
start_parser.set_defaults(func=cmd_start)
# Sous-commande stop
stop_parser = subparsers.add_parser("stop", help="Arrêter un service")
stop_parser.add_argument("service")
stop_parser.add_argument("-f", "--force", action="store_true")
stop_parser.set_defaults(func=cmd_stop)
# Sous-commande status
status_parser = subparsers.add_parser("status", help="Afficher le statut")
status_parser.add_argument("service", nargs="?", default="all")
status_parser.set_defaults(func=cmd_status)
args = parser.parse_args()
if hasattr(args, "func"):
args.func(args)
else:
parser.print_help()
# Usage:
# python sysctl.py start nginx
# python sysctl.py stop nginx --force
# python sysctl.py status
2. Click - CLI Moderne
Installation
CLI Basique avec Click
import click
@click.command()
@click.argument("name")
@click.option("-c", "--count", default=1, help="Nombre de salutations")
@click.option("-v", "--verbose", is_flag=True, help="Mode verbeux")
def hello(name, count, verbose):
"""Programme de salutation simple."""
for _ in range(count):
if verbose:
click.echo(f"Bonjour très cher {name}!")
else:
click.echo(f"Bonjour {name}!")
if __name__ == "__main__":
hello()
Options et Arguments
import click
@click.command()
# Arguments positionnels
@click.argument("source", type=click.Path(exists=True))
@click.argument("dest", type=click.Path())
# Options avec types
@click.option("-p", "--port", type=int, default=8080)
@click.option("-H", "--host", default="localhost")
# Choix
@click.option(
"-f", "--format",
type=click.Choice(["json", "yaml", "xml"]),
default="json"
)
# Flags
@click.option("-v", "--verbose", is_flag=True)
@click.option("-q", "--quiet", is_flag=True)
# Multiple valeurs
@click.option("-t", "--tag", multiple=True)
# Prompt pour entrée
@click.option("--password", prompt=True, hide_input=True)
# Confirmation
@click.option("--yes", is_flag=True, expose_value=False, callback=confirm_callback)
# Fichier
@click.option("-o", "--output", type=click.File("w"), default="-")
def deploy(source, dest, port, host, format, verbose, quiet, tag, password, output):
"""Déploie une application."""
click.echo(f"Deploying {source} to {dest}")
click.echo(f"Tags: {tag}")
Groupes de Commandes
import click
@click.group()
@click.option("-v", "--verbose", is_flag=True)
@click.pass_context
def cli(ctx, verbose):
"""Outil de gestion de serveurs."""
ctx.ensure_object(dict)
ctx.obj["VERBOSE"] = verbose
@cli.command()
@click.argument("name")
@click.option("-t", "--type", default="web")
@click.pass_context
def create(ctx, name, type):
"""Crée un nouveau serveur."""
if ctx.obj["VERBOSE"]:
click.echo(f"Création du serveur {name} de type {type}...")
click.echo(f"Serveur {name} créé!")
@cli.command()
@click.argument("name")
@click.option("-f", "--force", is_flag=True)
@click.pass_context
def delete(ctx, name, force):
"""Supprime un serveur."""
if not force:
click.confirm(f"Vraiment supprimer {name}?", abort=True)
click.echo(f"Serveur {name} supprimé!")
@cli.command("list")
@click.option("--format", type=click.Choice(["table", "json"]), default="table")
def list_servers(format):
"""Liste les serveurs."""
servers = ["web01", "web02", "db01"]
if format == "json":
import json
click.echo(json.dumps(servers))
else:
for s in servers:
click.echo(f" - {s}")
if __name__ == "__main__":
cli()
# Usage:
# python server.py --verbose create myserver -t database
# python server.py delete myserver --force
# python server.py list --format json
Validation et Callbacks
import click
def validate_port(ctx, param, value):
"""Valide que le port est dans la plage autorisée."""
if value < 1 or value > 65535:
raise click.BadParameter("Port doit être entre 1 et 65535")
return value
def validate_hostname(ctx, param, value):
"""Valide le format du hostname."""
import re
if not re.match(r'^[a-zA-Z0-9][a-zA-Z0-9\-\.]*$', value):
raise click.BadParameter("Hostname invalide")
return value
@click.command()
@click.option("-p", "--port", type=int, callback=validate_port, default=8080)
@click.option("-H", "--host", callback=validate_hostname, required=True)
def connect(port, host):
click.echo(f"Connexion à {host}:{port}")
3. Rich - Formatage Avancé
Installation
Sortie Colorée
from rich.console import Console
from rich.panel import Panel
from rich.text import Text
console = Console()
# Couleurs et styles
console.print("Texte normal")
console.print("[bold red]Erreur![/bold red] Quelque chose s'est mal passé")
console.print("[green]Succès[/green] - Opération terminée")
console.print("[bold blue]Info:[/bold blue] Version 1.0.0")
# Styles combinés
console.print("[bold italic yellow on red]Attention![/bold italic yellow on red]")
# Emojis
console.print(":rocket: Déploiement en cours...")
console.print(":white_check_mark: Terminé!")
console.print(":x: Échec!")
# Panel
console.print(Panel("Contenu important", title="Titre", border_style="green"))
# Règles
console.rule("[bold red]Section")
Tableaux
from rich.console import Console
from rich.table import Table
console = Console()
# Tableau simple
table = Table(title="Serveurs")
table.add_column("Nom", style="cyan", no_wrap=True)
table.add_column("IP", style="magenta")
table.add_column("Status", justify="center")
table.add_column("CPU", justify="right")
table.add_row("web01", "192.168.1.10", "[green]Running[/green]", "23%")
table.add_row("web02", "192.168.1.11", "[green]Running[/green]", "45%")
table.add_row("db01", "192.168.1.20", "[red]Stopped[/red]", "0%")
console.print(table)
# Tableau depuis données
def show_processes(processes):
table = Table(title="Processus")
table.add_column("PID")
table.add_column("Nom")
table.add_column("CPU %")
table.add_column("Mémoire")
for proc in processes:
table.add_row(
str(proc["pid"]),
proc["name"],
f"{proc['cpu']:.1f}%",
f"{proc['memory']} MB"
)
console.print(table)
Barre de Progression
from rich.console import Console
from rich.progress import (
Progress,
SpinnerColumn,
BarColumn,
TextColumn,
TimeRemainingColumn,
TaskProgressColumn
)
import time
console = Console()
# Progress bar simple
from rich.progress import track
for item in track(range(100), description="Processing..."):
time.sleep(0.02)
# Progress bar personnalisée
with Progress(
SpinnerColumn(),
TextColumn("[bold blue]{task.description}"),
BarColumn(),
TaskProgressColumn(),
TimeRemainingColumn(),
) as progress:
task1 = progress.add_task("Téléchargement...", total=100)
task2 = progress.add_task("Installation...", total=100)
while not progress.finished:
progress.update(task1, advance=0.9)
progress.update(task2, advance=0.5)
time.sleep(0.02)
# Multiple tasks
with Progress() as progress:
tasks = {
"web01": progress.add_task("[cyan]web01", total=100),
"web02": progress.add_task("[cyan]web02", total=100),
"db01": progress.add_task("[cyan]db01", total=100),
}
for server, task_id in tasks.items():
for i in range(100):
progress.update(task_id, advance=1)
time.sleep(0.01)
Logging avec Rich
import logging
from rich.logging import RichHandler
# Configuration du logging avec Rich
logging.basicConfig(
level=logging.DEBUG,
format="%(message)s",
datefmt="[%X]",
handlers=[RichHandler(rich_tracebacks=True)]
)
log = logging.getLogger("rich")
log.debug("Message de debug")
log.info("Information")
log.warning("Attention")
log.error("Erreur")
try:
1/0
except Exception:
log.exception("Exception capturée")
4. CLI Complète
Structure de Projet
myapp/
├── myapp/
│ ├── __init__.py
│ ├── cli.py
│ ├── commands/
│ │ ├── __init__.py
│ │ ├── server.py
│ │ └── deploy.py
│ └── utils/
│ ├── __init__.py
│ └── config.py
├── setup.py
└── pyproject.toml
CLI Professionnelle
# myapp/cli.py
import click
from rich.console import Console
from rich.table import Table
import logging
console = Console()
# Configuration globale
class Config:
def __init__(self):
self.verbose = False
self.config_file = None
pass_config = click.make_pass_decorator(Config, ensure=True)
@click.group()
@click.option("-v", "--verbose", is_flag=True, help="Mode verbeux")
@click.option("-c", "--config", type=click.Path(), help="Fichier de configuration")
@click.version_option(version="1.0.0")
@pass_config
def cli(config, verbose, config_file):
"""
MyApp - Outil de gestion d'infrastructure
Exemple d'utilisation:
myapp server list
myapp deploy --env production
"""
config.verbose = verbose
config.config_file = config_file
if verbose:
logging.basicConfig(level=logging.DEBUG)
# Commandes serveur
@cli.group()
def server():
"""Gestion des serveurs."""
pass
@server.command("list")
@click.option("--format", type=click.Choice(["table", "json"]), default="table")
@pass_config
def server_list(config, format):
"""Liste tous les serveurs."""
servers = [
{"name": "web01", "ip": "192.168.1.10", "status": "running"},
{"name": "web02", "ip": "192.168.1.11", "status": "running"},
{"name": "db01", "ip": "192.168.1.20", "status": "stopped"},
]
if format == "json":
import json
console.print_json(json.dumps(servers))
else:
table = Table(title="Serveurs")
table.add_column("Nom", style="cyan")
table.add_column("IP")
table.add_column("Status")
for s in servers:
status_style = "green" if s["status"] == "running" else "red"
table.add_row(
s["name"],
s["ip"],
f"[{status_style}]{s['status']}[/{status_style}]"
)
console.print(table)
@server.command("start")
@click.argument("name")
@click.option("-w", "--wait", is_flag=True, help="Attendre le démarrage")
@pass_config
def server_start(config, name, wait):
"""Démarre un serveur."""
with console.status(f"[bold green]Démarrage de {name}..."):
import time
time.sleep(2) # Simulation
console.print(f":white_check_mark: Serveur [cyan]{name}[/cyan] démarré!")
@server.command("stop")
@click.argument("name")
@click.option("-f", "--force", is_flag=True, help="Forcer l'arrêt")
@click.confirmation_option(prompt="Êtes-vous sûr de vouloir arrêter ce serveur?")
@pass_config
def server_stop(config, name, force):
"""Arrête un serveur."""
console.print(f":stop_sign: Serveur [cyan]{name}[/cyan] arrêté!")
# Commande de déploiement
@cli.command()
@click.option("-e", "--env", type=click.Choice(["dev", "staging", "prod"]), required=True)
@click.option("-t", "--tag", help="Tag de la release")
@click.option("--dry-run", is_flag=True, help="Simulation sans déploiement")
@pass_config
def deploy(config, env, tag, dry_run):
"""Déploie l'application."""
if dry_run:
console.print("[yellow]Mode dry-run activé[/yellow]")
from rich.progress import Progress
with Progress() as progress:
task = progress.add_task(f"[green]Déploiement {env}...", total=100)
for i in range(100):
import time
time.sleep(0.02)
progress.update(task, advance=1)
console.print(f":rocket: Déployé sur [bold]{env}[/bold]!")
# Point d'entrée
def main():
cli()
if __name__ == "__main__":
main()
Configuration setup.py
# setup.py
from setuptools import setup, find_packages
setup(
name="myapp",
version="1.0.0",
packages=find_packages(),
include_package_data=True,
install_requires=[
"click>=8.0",
"rich>=10.0",
],
entry_points={
"console_scripts": [
"myapp=myapp.cli:main",
],
},
)
5. Interactivité
Prompts
import click
from rich.console import Console
from rich.prompt import Prompt, Confirm, IntPrompt
console = Console()
# Click prompts
name = click.prompt("Votre nom")
password = click.prompt("Mot de passe", hide_input=True)
age = click.prompt("Âge", type=int, default=25)
if click.confirm("Continuer?"):
click.echo("OK!")
# Rich prompts
name = Prompt.ask("Entrez votre nom")
name = Prompt.ask("Nom", default="Anonymous")
age = IntPrompt.ask("Âge", default=25)
if Confirm.ask("Voulez-vous continuer?"):
console.print("Continuing...")
# Choix
choice = Prompt.ask(
"Choisir un environnement",
choices=["dev", "staging", "prod"],
default="dev"
)
Menu Interactif
from rich.console import Console
from rich.prompt import Prompt
from rich.panel import Panel
console = Console()
def show_menu():
console.clear()
console.print(Panel.fit(
"[bold cyan]Menu Principal[/bold cyan]\n\n"
"[1] Lister les serveurs\n"
"[2] Créer un serveur\n"
"[3] Supprimer un serveur\n"
"[4] Configuration\n"
"[q] Quitter",
title="MyApp v1.0"
))
choice = Prompt.ask("Choix", choices=["1", "2", "3", "4", "q"])
return choice
def main():
while True:
choice = show_menu()
if choice == "q":
console.print("Au revoir!")
break
elif choice == "1":
list_servers()
elif choice == "2":
create_server()
# etc.
Prompt.ask("\nAppuyez sur Entrée pour continuer...")
if __name__ == "__main__":
main()
6. Bonnes Pratiques
Gestion des Erreurs
import click
import sys
class CLIError(Exception):
"""Erreur CLI personnalisée."""
pass
def handle_error(func):
"""Décorateur pour gérer les erreurs."""
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except CLIError as e:
click.echo(f"Erreur: {e}", err=True)
sys.exit(1)
except Exception as e:
click.echo(f"Erreur inattendue: {e}", err=True)
sys.exit(2)
return wrapper
@click.command()
@handle_error
def main():
raise CLIError("Quelque chose s'est mal passé")
Configuration par Fichier
import click
import yaml
from pathlib import Path
def load_config(config_file=None):
"""Charge la configuration depuis un fichier."""
paths = [
config_file,
Path.home() / ".myapprc",
Path("/etc/myapp/config.yml"),
]
for path in paths:
if path and Path(path).exists():
with open(path) as f:
return yaml.safe_load(f)
return {}
@click.command()
@click.option("-c", "--config", type=click.Path(exists=True))
def main(config):
cfg = load_config(config)
click.echo(f"Loaded config: {cfg}")
Exercices Pratiques
Exercice 1 : Outil de Backup
# Créer un CLI qui :
# - Accepte source et destination
# - Option pour compresser
# - Barre de progression
# - Log des opérations
Exercice 2 : Gestionnaire de Services
# Créer un CLI avec sous-commandes :
# - start/stop/restart/status
# - Liste des services avec statut coloré
# - Confirmation pour les actions destructives
Exercice 3 : Dashboard Interactif
# Créer un outil qui :
# - Affiche un dashboard de monitoring
# - Rafraîchissement automatique
# - Navigation au clavier
Points Clés à Retenir
Bonnes Pratiques
- Utiliser des noms de commandes clairs
- Fournir une aide complète (--help)
- Valider les entrées utilisateur
- Gérer les erreurs proprement
- Utiliser des codes de sortie appropriés
Pièges Courants
- Oublier le shebang (
#!/usr/bin/env python3) - Ne pas gérer Ctrl+C proprement
- Sorties non formatées pour les scripts
- Absence de mode verbose/quiet
Voir Aussi
Navigation
| ← Module 12 - SSH & Automatisation Dist... | Module 14 - Cloud & AWS avec Python → |