🔝 Retour au Sommaire
La section précédente a présenté les bases de CLI11 avec un premier programme fonctionnel. Cette section approfondit les trois briques fondamentales du parsing d'arguments : les options (paramètres nommés avec valeur), les flags (commutateurs booléens) et les sous-commandes (arborescence de commandes). Maîtriser ces trois éléments permet de modéliser n'importe quelle interface CLI, de l'utilitaire le plus simple aux outils complexes à la git ou kubectl.
Une option est un paramètre nommé qui attend une valeur. CLI11 offre une grande flexibilité dans la façon de déclarer, typer et contraindre les options.
Le premier argument de add_option est une chaîne de spécification qui définit les noms reconnus pour l'option :
// Nom long uniquement
app.add_option("--output", output, "Fichier de sortie");
// Nom long + nom court (séparés par une virgule)
app.add_option("--output,-o", output, "Fichier de sortie");
// Nom court uniquement (rare, mais possible)
app.add_option("-o", output, "Fichier de sortie");
// Plusieurs alias (le premier nom long est le nom canonique)
app.add_option("--output,-o,--out", output, "Fichier de sortie");Le nom canonique — celui qui apparaît dans les messages d'erreur et l'aide — est le premier nom long spécifié. L'ordre de déclaration dans la chaîne détermine donc la présentation dans l'aide générée.
Sur la ligne de commande, l'utilisateur peut passer la valeur de plusieurs façons, et CLI11 accepte toutes les formes courantes :
# Formes équivalentes pour une option longue
--output result.txt
--output=result.txt
# Formes équivalentes pour une option courte
-o result.txt
-oresult.txt # Collé, sans espace (convention POSIX)CLI11 convertit automatiquement la valeur textuelle de la ligne de commande vers le type de la variable de destination. Les types suivants sont supportés nativement :
// Entiers
int count = 0;
app.add_option("--count,-c", count, "Nombre d'éléments");
// Flottants
double threshold = 0.95;
app.add_option("--threshold", threshold, "Seuil de confiance");
// Chaînes
std::string name;
app.add_option("--name,-n", name, "Nom de la ressource");
// Vecteurs (option répétable — chaque occurrence ajoute un élément)
std::vector<std::string> tags;
app.add_option("--tag,-t", tags, "Tags (répétable)");
// Usage : --tag web --tag prod --tag v2
// Paires et tuples
std::pair<std::string, int> mapping;
app.add_option("--map", mapping, "Mapping clé:valeur");
// Usage : --map hostname 8080
// Optional (C++17)
std::optional<int> port;
app.add_option("--port,-p", port, "Port (optionnel)");
// Après parsing : port.has_value() == true si l'option a été passéeLe support de std::optional est particulièrement utile : il permet de distinguer "l'utilisateur n'a pas passé l'option" (le optional est vide) de "l'utilisateur a passé la valeur par défaut" — une nuance impossible à capturer avec une simple valeur par défaut.
Un argument positionnel est une option sans nom : sa position sur la ligne de commande détermine à quelle variable il est assigné. C'est le pattern classique des commandes comme cp source destination :
std::string source;
std::string destination;
app.add_option("source", source, "Fichier source")
->required();
app.add_option("destination", destination, "Fichier destination")
->required();$ mon-outil fichier_a.txt fichier_b.txt
# source = "fichier_a.txt", destination = "fichier_b.txt"CLI11 identifie un argument positionnel par l'absence de tiret dans le nom. L'ordre de déclaration dans le code détermine l'ordre attendu sur la ligne de commande.
On peut combiner positionnels et options nommées librement :
std::string input;
std::string output = "out.txt";
bool verbose = false;
app.add_option("input", input, "Fichier d'entrée")->required();
app.add_option("--output,-o", output, "Fichier de sortie");
app.add_flag("--verbose,-v", verbose, "Mode verbeux"); # Toutes ces formes sont équivalentes
$ mon-outil data.csv --output result.txt -v
$ mon-outil -v --output result.txt data.csv
$ mon-outil --output=result.txt data.csv --verboseCLI11 n'impose pas d'ordre entre les options nommées et les positionnels — l'utilisateur est libre de les mélanger.
Pour accepter un nombre variable d'arguments positionnels, on utilise un std::vector :
std::vector<std::string> files;
app.add_option("files", files, "Fichiers à traiter")
->required()
->expected(1, -1); // Au moins 1, pas de maximum$ mon-outil a.txt b.txt c.txt
# files = {"a.txt", "b.txt", "c.txt"}La méthode expected(min, max) contrôle le nombre de valeurs attendues. La valeur -1 signifie "illimité". Quelques patterns courants :
->expected(1) // Exactement 1 valeur
->expected(2) // Exactement 2 valeurs
->expected(1, 5) // Entre 1 et 5 valeurs
->expected(0, -1) // 0 ou plus (optionnel variadique)Certaines options acceptent plusieurs valeurs en une seule occurrence. Par exemple, une paire de coordonnées :
std::vector<double> coords;
app.add_option("--coords", coords, "Coordonnées lat lon")
->expected(2); // Exactement 2 valeurs$ mon-outil --coords 48.8566 2.3522
# coords = {48.8566, 2.3522}Les méthodes chaînées sur le retour de add_option permettent de configurer finement chaque option :
app.add_option("--output,-o", output, "Fichier de sortie")
->required() // Obligatoire
// ->required() garantit déjà que l'option est fournie
->default_str("stdout") // Texte affiché dans l'aide
->envname("TOOL_OUTPUT") // Fallback sur variable d'environnement
->group("Output") // Groupe dans l'aide
->capture_default_str(); // Affiche la valeur par défaut dans l'aideLe modificateur capture_default_str() mérite une attention particulière. Sans lui, l'aide affiche le type attendu. Avec lui, l'aide affiche la valeur par défaut réelle :
# Sans capture_default_str()
--timeout,-t INT Timeout en millisecondes
# Avec capture_default_str()
--timeout,-t INT [5000] Timeout en millisecondes
C'est une petite touche d'ergonomie qui améliore sensiblement l'expérience utilisateur.
Un flag est un commutateur booléen : sa présence sur la ligne de commande active un comportement, son absence le laisse inactif. CLI11 propose plusieurs variantes pour couvrir les cas d'usage courants.
bool verbose = false;
app.add_flag("--verbose,-v", verbose, "Mode verbeux"); $ mon-outil # verbose = false
$ mon-outil --verbose # verbose = true
$ mon-outil -v # verbose = trueSi la variable de destination est un entier, chaque occurrence du flag incrémente la valeur. C'est le pattern classique des niveaux de verbosité :
int verbosity = 0;
app.add_flag("--verbose,-v", verbosity, "Niveau de verbosité (empilable)"); $ mon-outil # verbosity = 0
$ mon-outil -v # verbosity = 1
$ mon-outil -vv # verbosity = 2
$ mon-outil -vvv # verbosity = 3
$ mon-outil -v -v -v # verbosity = 3 (équivalent)Ce mécanisme est naturel pour l'utilisateur : -v pour un aperçu, -vvv pour le debug complet.
Parfois, il est utile d'avoir un flag qui désactive une fonctionnalité activée par défaut. CLI11 supporte la syntaxe {true_name} / {false_name} avec une notation spéciale sur le nom :
bool color = true; // Activé par défaut
app.add_flag("--color,!--no-color", color, "Activer/désactiver les couleurs"); $ mon-outil # color = true (défaut)
$ mon-outil --color # color = true
$ mon-outil --no-color # color = falseLe ! devant --no-color indique que ce nom inverse la valeur. C'est un pattern très courant dans les outils CLI (pensez à --color / --no-color dans grep ou git).
CLI11 supporte la convention POSIX de regroupement des flags courts en une seule chaîne :
bool a = false, b = false, c = false;
app.add_flag("-a", a, "Option A");
app.add_flag("-b", b, "Option B");
app.add_flag("-c", c, "Option C"); $ mon-outil -abc # a = true, b = true, c = true
$ mon-outil -a -b -c # ÉquivalentCe comportement est automatique dès lors que les flags courts sont définis avec une seule lettre.
Les sous-commandes permettent de structurer un outil CLI en plusieurs "modes" distincts, chacun avec ses propres options. C'est le pattern de git (commit, push, pull), de docker (run, build, ps) ou de kubectl (get, apply, delete).
CLI::App app{"mytool — Outil multi-fonctions"};
auto* cmd_build = app.add_subcommand("build", "Compiler le projet");
auto* cmd_test = app.add_subcommand("test", "Lancer les tests");
auto* cmd_deploy = app.add_subcommand("deploy", "Déployer en production"); Chaque sous-commande est un objet CLI::App* à part entière, sur lequel on enregistre des options indépendantes :
// Options spécifiques à "build"
std::string target = "release";
int jobs = 4;
cmd_build->add_option("--target,-t", target, "Cible de compilation")
->check(CLI::IsMember({"debug", "release", "profile"}));
cmd_build->add_option("--jobs,-j", jobs, "Nombre de jobs parallèles")
->check(CLI::Range(1, 64));
// Options spécifiques à "deploy"
std::string env = "staging";
bool dry_run = false;
cmd_deploy->add_option("--env,-e", env, "Environnement cible")
->check(CLI::IsMember({"staging", "production"}));
cmd_deploy->add_flag("--dry-run", dry_run, "Simuler le déploiement");$ mytool build --target debug -j 8
$ mytool deploy --env production
$ mytool deploy --dry-run --env stagingLes options déclarées sur l'application principale sont globales et disponibles quelle que soit la sous-commande. Les options déclarées sur une sous-commande sont locales :
// Option globale
bool verbose = false;
app.add_flag("--verbose,-v", verbose, "Mode verbeux");
// Option locale à "build"
int jobs = 4;
cmd_build->add_option("--jobs,-j", jobs, "Jobs parallèles"); $ mytool --verbose build --jobs 8 # verbose + build avec 8 jobs
$ mytool build --verbose --jobs 8 # Équivalent (ordre libre)
$ mytool test --jobs 8 # Erreur : --jobs n'existe pas pour "test"Par défaut, CLI11 n'exige pas de sous-commande : le programme peut être invoqué sans. On peut contraindre ce comportement :
app.require_subcommand(1); // Exactement 1 sous-commande requise
app.require_subcommand(1, 2); // Entre 1 et 2 sous-commandes
app.require_subcommand(0, 1); // 0 ou 1 (sous-commande optionnelle) require_subcommand(1) est le pattern le plus courant pour les outils structurés en commandes. Sans sous-commande, l'aide est affichée automatiquement.
Après le parsing, il faut déterminer quelle sous-commande a été invoquée pour exécuter la logique correspondante. CLI11 offre deux approches.
Approche par parsed() — vérification explicite :
CLI11_PARSE(app, argc, argv);
if (cmd_build->parsed()) {
run_build(target, jobs);
} else if (cmd_test->parsed()) {
run_tests();
} else if (cmd_deploy->parsed()) {
run_deploy(env, dry_run);
}Approche par callbacks — logique attachée à chaque sous-commande :
cmd_build->callback([&]() {
run_build(target, jobs);
});
cmd_test->callback([&]() {
run_tests();
});
cmd_deploy->callback([&]() {
run_deploy(env, dry_run);
});
CLI11_PARSE(app, argc, argv);
// Le callback de la sous-commande active est exécuté automatiquementL'approche par callbacks est plus élégante quand le programme est simple. L'approche par parsed() offre plus de contrôle quand on a besoin de logique partagée entre sous-commandes ou de traitement séquentiel.
CLI11 supporte l'imbrication de sous-commandes sur plusieurs niveaux, ce qui permet de modéliser des hiérarchies complexes :
CLI::App app{"kubectl-lite"};
// Niveau 1 : kubectl-lite get ...
auto* cmd_get = app.add_subcommand("get", "Afficher des ressources");
// Niveau 2 : kubectl-lite get pods / kubectl-lite get services
auto* get_pods = cmd_get->add_subcommand("pods", "Lister les pods");
auto* get_svc = cmd_get->add_subcommand("services", "Lister les services");
cmd_get->require_subcommand(1);
// Options sur la sous-commande de niveau 2
std::string ns = "default";
get_pods->add_option("--namespace,-n", ns, "Namespace Kubernetes");
bool wide = false;
get_pods->add_flag("--wide,-w", wide, "Affichage étendu"); $ kubectl-lite get pods --namespace kube-system --wide
$ kubectl-lite get services
$ kubectl-lite get --help
Afficher des ressources
Usage: kubectl-lite get [OPTIONS] SUBCOMMAND
Options:
-h,--help Print this help message and exit
Subcommands:
pods Lister les pods
services Lister les servicesEn pratique, deux niveaux de profondeur couvrent la grande majorité des besoins. Au-delà de trois niveaux, l'interface devient difficile à mémoriser pour l'utilisateur.
CLI11 permet de définir des alias pour les sous-commandes, ce qui améliore l'ergonomie :
auto* cmd_list = app.add_subcommand("list", "Lister les ressources");
cmd_list->alias("ls"); $ mytool list # Fonctionne
$ mytool ls # Fonctionne aussiPour illustrer l'interaction entre options, flags et sous-commandes, voici un exemple plus complet simulant un outil de gestion de conteneurs :
#include <CLI/CLI.hpp>
#include <print>
#include <string>
#include <vector>
int main(int argc, char* argv[]) {
CLI::App app{"cbox — Gestionnaire de conteneurs"};
app.require_subcommand(1);
// Option globale
bool verbose = false;
app.add_flag("--verbose,-v", verbose, "Mode verbeux");
std::string config_file;
app.add_option("--config,-c", config_file, "Fichier de configuration")
->check(CLI::ExistingFile)
->envname("CBOX_CONFIG");
// --- Sous-commande "run" ---
auto* cmd_run = app.add_subcommand("run", "Lancer un conteneur");
std::string image;
cmd_run->add_option("image", image, "Image à exécuter")->required();
std::string name;
cmd_run->add_option("--name,-n", name, "Nom du conteneur");
std::vector<std::string> env_vars;
cmd_run->add_option("--env,-e", env_vars, "Variables d'environnement (CLÉ=VAL)");
std::vector<std::string> ports;
cmd_run->add_option("--publish,-p", ports, "Mapping de ports (hôte:conteneur)");
bool detach = false;
cmd_run->add_flag("--detach,-d", detach, "Exécuter en arrière-plan");
bool rm = false;
cmd_run->add_flag("--rm", rm, "Supprimer le conteneur à l'arrêt");
int memory_mb = 0;
cmd_run->add_option("--memory,-m", memory_mb, "Limite mémoire en Mo")
->check(CLI::NonNegativeNumber);
std::vector<std::string> trailing_args;
cmd_run->add_option("args", trailing_args, "Commande à exécuter dans le conteneur");
// --- Sous-commande "ps" ---
auto* cmd_ps = app.add_subcommand("ps", "Lister les conteneurs");
cmd_ps->alias("list");
bool all = false;
cmd_ps->add_flag("--all,-a", all, "Inclure les conteneurs arrêtés");
std::string format = "table";
cmd_ps->add_option("--format,-f", format, "Format de sortie")
->check(CLI::IsMember({"table", "json", "wide"}))
->capture_default_str();
// --- Sous-commande "stop" ---
auto* cmd_stop = app.add_subcommand("stop", "Arrêter un ou plusieurs conteneurs");
std::vector<std::string> containers;
cmd_stop->add_option("containers", containers, "Conteneurs à arrêter")
->required()
->expected(1, -1);
int timeout_sec = 10;
cmd_stop->add_option("--timeout,-t", timeout_sec, "Timeout d'arrêt en secondes")
->check(CLI::Range(1, 300))
->capture_default_str();
bool force = false;
cmd_stop->add_flag("--force,-f", force, "Forcer l'arrêt (SIGKILL)");
// --- Parsing ---
CLI11_PARSE(app, argc, argv);
// --- Dispatch ---
if (cmd_run->parsed()) {
if (verbose) {
std::println(stderr, "[debug] Image: {}", image);
if (!name.empty())
std::println(stderr, "[debug] Nom: {}", name);
for (const auto& e : env_vars)
std::println(stderr, "[debug] Env: {}", e);
for (const auto& p : ports)
std::println(stderr, "[debug] Port: {}", p);
}
std::println("Lancement de {} {}{}{}", image,
detach ? "(détaché) " : "",
rm ? "(auto-suppression) " : "",
memory_mb > 0
? std::format("(mémoire: {} Mo)", memory_mb)
: "");
if (!trailing_args.empty()) {
std::print("Commande : ");
for (const auto& arg : trailing_args)
std::print("{} ", arg);
std::println("");
}
} else if (cmd_ps->parsed()) {
std::println("Liste des conteneurs{} (format: {})",
all ? " (tous)" : " (actifs)", format);
} else if (cmd_stop->parsed()) {
std::println("Arrêt de {} conteneur(s) (timeout: {}s{})",
containers.size(), timeout_sec,
force ? ", forcé" : "");
for (const auto& c : containers)
std::println(" → {}", c);
}
return 0;
}Testons les différentes sous-commandes :
# Lancer un conteneur avec options complètes
$ cbox run nginx:latest --name web -d --rm \
-e "PORT=8080" -e "ENV=prod" \
-p "80:8080" -p "443:8443" \
--memory 512
Lancement de nginx:latest (détaché) (auto-suppression) (mémoire: 512 Mo)
# Lister les conteneurs (avec alias "ls")
$ cbox ls --all --format json
Liste des conteneurs (tous) (format: json)
# Arrêter des conteneurs avec force
$ cbox stop web api worker --force --timeout 5
Arrêt de 3 conteneur(s) (timeout: 5s, forcé)
→ web
→ api
→ worker
# Option globale + sous-commande
$ cbox --verbose run alpine:3.19 -- echo "hello"
[debug] Image: alpine:3.19
Lancement de alpine:3.19
Commande : echo hello
# Aide contextuelle par sous-commande
$ cbox run --help
Lancer un conteneur
Usage: cbox run [OPTIONS] image [args...]
Positionals:
image TEXT REQUIRED Image à exécuter
args TEXT ... Commande à exécuter dans le conteneur
Options:
-h,--help Print this help message and exit
--name,-n TEXT Nom du conteneur
--env,-e TEXT ... Variables d'environnement (CLÉ=VAL)
--publish,-p TEXT ... Mapping de ports (hôte:conteneur)
--detach,-d Exécuter en arrière-plan
--rm Supprimer le conteneur à l'arrêt
--memory,-m INT:NONNEGATIVE Limite mémoire en MoCet exemple illustre les patterns fondamentaux que l'on retrouve dans les outils CLI professionnels :
- Options globales (
--verbose,--config) partagées entre toutes les sous-commandes. - Options répétables (
--env,--publish,--port) viastd::vector. - Arguments positionnels obligatoires (
image,containers) et optionnels variadiques (args). - Flags avec sémantique claire (
--detach,--rm,--force,--all). - Validation intégrée (
Range,IsMember,ExistingFile,NonNegativeNumber). - Alias (
ps/list). - Sortie diagnostique sur
stderrquand verbose est activé, données utiles surstdout.
Dans un vrai projet, le main() ne devrait contenir que la configuration CLI et le dispatch. La logique métier est déléguée à des fonctions ou des classes séparées :
// main.cpp — point d'entrée, déclaration CLI et dispatch
int main(int argc, char* argv[]) {
CLI::App app{"mytool"};
// ... déclaration des options et sous-commandes ...
CLI11_PARSE(app, argc, argv);
// ... dispatch vers des fonctions métier ...
}
// commands/build.hpp — logique de la commande "build"
struct BuildOptions {
std::string target;
int jobs;
bool verbose;
};
int run_build(const BuildOptions& opts);Ce pattern de structure — un struct d'options par sous-commande, une fonction par commande — est celui utilisé par les outils CLI majeurs. Il facilite les tests unitaires (on peut tester run_build indépendamment du parsing), la maintenance et l'ajout de nouvelles commandes. Nous approfondirons cette architecture en section 36.5.
La convention du double tiret (--) signale la fin des options : tout ce qui suit est traité comme un argument positionnel, même si ça commence par un tiret. CLI11 respecte cette convention nativement :
# Sans -- : CLI11 interprète "-la" comme un flag
$ cbox run alpine ls -la
Error: -l is not a valid flag
# Avec -- : tout après est positionnel
$ cbox run alpine -- ls -la
# trailing_args = {"ls", "-la"}C'est essentiel pour les outils qui passent des arguments à un sous-processus.
Utilisez systématiquement capture_default_str() sur les options qui ont une valeur par défaut significative. L'utilisateur ne devrait jamais avoir à lire le code source pour connaître le comportement par défaut :
int port = 8080;
app.add_option("--port,-p", port, "Port d'écoute")
->capture_default_str(); // Affiche [8080] dans l'aideLes descriptions d'options doivent être courtes (une ligne), en minuscule, sans point final, et décrire l'effet plutôt que le mécanisme :
// ✓ Bon : concis, décrit l'effet
"Port d'écoute"
"Nombre de jobs parallèles"
"Inclure les conteneurs arrêtés"
// ✗ Mauvais : verbeux, décrit le mécanisme
"Spécifie le numéro de port TCP sur lequel le serveur va écouter."
"Définit le nombre de threads utilisés pour la compilation parallèle."| Méthode | Usage | Exemple |
|---|---|---|
add_option("--nom,-n", var, desc) |
Option avec valeur | --port 8080 |
add_flag("--nom,-n", var, desc) |
Flag booléen/compteur | --verbose, -vvv |
add_flag("--x,!--no-x", var, desc) |
Flag inversable | --color / --no-color |
add_subcommand("nom", desc) |
Sous-commande | tool build |
->required() |
Rend obligatoire | |
->check(validator) |
Ajoute une validation | Range, IsMember, ExistingFile |
->expected(min, max) |
Nombre de valeurs | expected(1, -1) |
->envname("VAR") |
Fallback env variable | TOOL_PORT |
->capture_default_str() |
Affiche le défaut dans l'aide | [8080] |
->excludes(other) |
Exclusion mutuelle | --json exclut --table |
->needs(other) |
Dépendance | --pass requiert --user |
->group("Nom") |
Groupe dans l'aide | |
->alias("nom") |
Alias de sous-commande | list / ls |
require_subcommand(n) |
Exiger n sous-commandes | |
->callback(fn) |
Action post-parsing | Lambda |
->parsed() |
Sous-commande invoquée ? | Retourne true/false |
La section suivante (36.1.3) approfondit le système de validation et les callbacks, en montrant comment créer des validateurs personnalisés et orchestrer des logiques post-parsing complexes.