Construire une CLI avec Click
Utiliser import('click') dans un script Catnip pour construire une vraie CLI avec arguments, options, help et
validation.
Click est un framework CLI Python. Catnip peut l'utiliser directement via
import('click').
1. Le pattern de base
#!/usr/bin/env catnip
click = import('click')
sys = import('sys')
mantra = (name) => {
click.echo(f"BORN TO SEGFAULT, {name}!")
}
cmd = click.Command("mantra", callback=mantra, params=list(click.Argument(list("name"))))
# sys.argv[0] est le nom du script — Click attend uniquement les arguments
ctx = cmd.make_context("mantra", sys.argv[1:])
p = ctx.params
mantra(p["name"])
Trois étapes :
- Définir la callback (fonction Catnip normale)
- Construire la commande avec
click.Command+ paramètres - Résoudre les arguments via
make_context, puis appeler la callback
2. Arguments et options
Click distingue les arguments positionnels (click.Argument) et les options nommées (click.Option).
#!/usr/bin/env catnip
click = import('click')
sys = import('sys')
io = import('io', wild=True)
transform = (input, output, scale, verbose) => {
if (verbose) { print(f"Processing {input} -> {output} (scale={scale})") }
# ... logique de transformation
print("done")
}
cmd = click.Command("transform", callback=transform, params=list(
click.Argument(list("input")),
click.Argument(list("output")),
click.Option(list("--scale", "-s"), default=1.0, type=click.FLOAT, help="Scale factor"),
click.Option(list("--verbose", "-v"), is_flag=True, help="Show details")
))
ctx = cmd.make_context("transform", sys.argv[1:])
p = ctx.params
transform(p["input"], p["output"], p["scale"], p["verbose"])
Les types Click (click.INT, click.FLOAT, click.STRING, click.BOOL, click.Choice(...)) fonctionnent pour la
validation et la conversion.
3. Help automatique
Click génère le help à partir des paramètres déclarés :
print(cmd.get_help(cmd.make_context("transform", list("_", "_"))))
Usage: transform [OPTIONS] INPUT OUTPUT
Options:
-s, --scale FLOAT Scale factor
-v, --verbose Show details
--help Show this message and exit.
Les arguments requis nécessitent des valeurs dummy dans make_context pour que le help se génère sans erreur.
4. Validation et choix
#!/usr/bin/env catnip
click = import('click')
sys = import('sys')
io = import('io', wild=True)
deploy = (env, replicas) => {
print(f"Deploying to {env} with {replicas} replicas")
}
cmd = click.Command("deploy", callback=deploy, params=list(
click.Option(list("--env", "-e"), required=True,
type=click.Choice(list("dev", "staging", "prod")),
help="Target environment"),
click.Option(list("--replicas", "-r"), default=1,
type=click.IntRange(1, 10),
help="Number of replicas (1-10)")
))
ctx = cmd.make_context("deploy", sys.argv[1:])
p = ctx.params
deploy(p["env"], p["replicas"])
Click valide les entrées avant que la callback soit appelée. Un --env=invalid produit une erreur claire sans code
supplémentaire.
5. --help et la sortie de Click
Click affiche le help puis lève click.exceptions.Exit quand --help est dans les arguments. Ce n'est pas un
SystemExit : la classe dérive de RuntimeError, donc rien ne la distingue d'une erreur ordinaire. Le help s'affiche,
et RuntimeError: 0 suit.
Un statut de sortie qui hérite de la classe des pannes. Le code est correct, le canal ne l'est pas.
Un vrai sys.exit() sort proprement, lui, avec le statut demandé -- celui de la stdlib Catnip (import('sys')) comme
celui de Python (import('sys', protocol='py')). Seule la sortie propre à Click passe par RuntimeError.
Pour générer le help sans déclencher cette sortie, appeler get_help directement :
if ("--help" in sys.argv) {
# make_context avec des dummies pour satisfaire les arguments requis
dummies = list("_") + sys.argv[1:]
print(cmd.get_help(cmd.make_context(cmd.name, dummies, resilient_parsing=True)))
} else {
ctx = cmd.make_context(cmd.name, sys.argv[1:])
p = ctx.params
callback(p["arg1"], p["opt1"])
}
6. Pourquoi l'API programmatique
Les décorateurs Click (@click.command(), @click.option()) ne fonctionnent pas sur les fonctions Catnip : ils posent
des attributs (__click_params__, __name__) sur l'objet fonction, et les fonctions Catnip n'ont pas de __dict__.
De même, cmd.invoke(ctx) introspecte la signature de la callback via inspect.signature(). Les fonctions Catnip
exposent (*args, **kwargs), ce qui empêche Click de dispatcher les paramètres par nom.
L'API programmatique (click.Command, click.Argument, click.Option) contourne ces deux limites : pas de
décorateurs, pas de dispatch par introspection.
7. Quand préférer Python comme entrypoint
Le pattern pur Catnip convient aux scripts utilitaires autonomes. Si la CLI grossit (sous-commandes, middlewares, plugins, gestion d'erreurs fine), Python comme entrypoint donne plus de contrôle :
import click
from catnip import Catnip, Context
from catnip.exc import CatnipError
@click.command()
@click.argument("script", type=click.Path(exists=True))
@click.option("--name", default="world")
def main(script, name):
ctx = Context(globals={"name": name})
cat = Catnip(context=ctx)
try:
cat.parse(open(script).read())
result = cat.execute()
except CatnipError as exc:
raise click.ClickException(str(exc)) from exc
if result is not None:
click.echo(result)
L'avantage : les décorateurs Click fonctionnent normalement, les exceptions Catnip sont converties en erreurs CLI propres, et le contexte est contrôlé explicitement.
Voir HOST_INTEGRATION et EXTENDING_CONTEXT pour les patterns d'embedding.