Skip to content

Latest commit

 

History

History
625 lines (470 loc) · 22.3 KB

File metadata and controls

625 lines (470 loc) · 22.3 KB

🔝 Retour au Sommaire

36.1.4 — Génération d'aide automatique

Section 36.1 : CLI11 — Parsing d'arguments professionnel


Introduction

L'aide en ligne de commande (--help) est souvent le premier contact qu'un utilisateur a avec votre outil. Une aide bien structurée, complète et lisible fait la différence entre un outil adopté par l'équipe et un outil que personne n'utilise parce que "personne ne sait comment ça marche". CLI11 génère cette aide automatiquement à partir des déclarations d'options, mais elle offre aussi un contrôle fin sur la présentation pour les projets qui ont besoin d'une finition professionnelle.


Aide par défaut : ce que CLI11 génère gratuitement

Sans aucune configuration supplémentaire, CLI11 produit une aide structurée à partir des informations déjà déclarées. Reprenons un exemple concret :

CLI::App app{"logwatch — Surveillance de fichiers de log en temps réel"};  
app.set_version_flag("--version,-V", "logwatch 2.1.0");  

std::string filepath;  
app.add_option("file", filepath, "Fichier de log à surveiller")  
    ->required()
    ->check(CLI::ExistingFile);

int lines = 20;  
app.add_option("--lines,-n", lines, "Nombre de lignes initiales à afficher")  
    ->check(CLI::Range(1, 10000))
    ->capture_default_str();

std::string pattern;  
app.add_option("--grep,-g", pattern, "Filtrer par expression régulière");  

std::string level = "info";  
app.add_option("--level,-l", level, "Niveau de log minimum")  
    ->check(CLI::IsMember({"trace", "debug", "info", "warn", "error", "fatal"}))
    ->capture_default_str();

bool follow = false;  
app.add_flag("--follow,-f", follow, "Suivre le fichier en temps réel (tail -f)");  

bool color = true;  
app.add_flag("--color,!--no-color", color, "Activer/désactiver la colorisation");  

std::string format = "text";  
app.add_option("--format", format, "Format de sortie")  
    ->check(CLI::IsMember({"text", "json", "compact"}))
    ->capture_default_str();

L'invocation logwatch --help produit :

logwatch — Surveillance de fichiers de log en temps réel  
Usage: logwatch [OPTIONS] file  

Positionals:
  file TEXT:FILE REQUIRED       Fichier de log à surveiller

Options:
  -h,--help                     Print this help message and exit
  --version,-V                  Display program version information and exit
  --lines,-n INT:INT in [1 - 10000] [20]
                                Nombre de lignes initiales à afficher
  --grep,-g TEXT                Filtrer par expression régulière
  --level,-l TEXT:{trace,debug,info,warn,error,fatal} [info]
                                Niveau de log minimum
  --follow,-f                   Suivre le fichier en temps réel (tail -f)
  --color,--no-color{false}     Activer/désactiver la colorisation
  --format TEXT:{text,json,compact} [text]
                                Format de sortie

Observons ce que CLI11 a généré automatiquement :

  • La description de l'application en titre.
  • La ligne d'usage synthétique (Usage: logwatch [OPTIONS] file).
  • La section Positionals avec le type attendu et le marqueur REQUIRED.
  • La section Options avec, pour chaque option : les noms long et court, le type, les contraintes (intervalle, membres), la valeur par défaut entre crochets, et la description.
  • Les flags --help et --version ajoutés automatiquement.

Tout cela provient des informations déjà déclarées dans le code — descriptions, validateurs, capture_default_str(). L'aide n'est jamais écrite manuellement, elle reste synchronisée avec le code par construction.


Groupes d'options

À mesure que le nombre d'options grandit, une liste plate devient difficile à parcourir. CLI11 permet de regrouper les options par thème via la méthode ->group() :

// Groupe "Filtrage"
app.add_option("--grep,-g", pattern, "Filtrer par expression régulière")
    ->group("Filtrage");
app.add_option("--level,-l", level, "Niveau de log minimum")
    ->check(CLI::IsMember({"trace", "debug", "info", "warn", "error", "fatal"}))
    ->capture_default_str()
    ->group("Filtrage");

// Groupe "Affichage"
app.add_option("--lines,-n", lines, "Nombre de lignes initiales")
    ->check(CLI::Range(1, 10000))
    ->capture_default_str()
    ->group("Affichage");
app.add_flag("--follow,-f", follow, "Suivre le fichier en temps réel")
    ->group("Affichage");
app.add_flag("--color,!--no-color", color, "Activer/désactiver la colorisation")
    ->group("Affichage");
app.add_option("--format", format, "Format de sortie")
    ->check(CLI::IsMember({"text", "json", "compact"}))
    ->capture_default_str()
    ->group("Affichage");

L'aide résultante est organisée par sections :

logwatch — Surveillance de fichiers de log en temps réel  
Usage: logwatch [OPTIONS] file  

Positionals:
  file TEXT:FILE REQUIRED       Fichier de log à surveiller

Filtrage:
  --grep,-g TEXT                Filtrer par expression régulière
  --level,-l TEXT:{trace,debug,info,warn,error,fatal} [info]
                                Niveau de log minimum

Affichage:
  --lines,-n INT:INT in [1 - 10000] [20]
                                Nombre de lignes initiales
  --follow,-f                   Suivre le fichier en temps réel
  --color,--no-color{false}     Activer/désactiver la colorisation
  --format TEXT:{text,json,compact} [text]
                                Format de sortie

Options:
  -h,--help                     Print this help message and exit
  --version,-V                  Display program version information and exit

Les groupes apparaissent dans l'ordre de leur première déclaration dans le code. Les options sans groupe explicite tombent dans le groupe par défaut ("Options"). Ce regroupement est une amélioration d'ergonomie significative dès que l'outil dépasse cinq ou six options.


Aide des sous-commandes

Chaque sous-commande a sa propre aide, accessible via <tool> <subcommand> --help. CLI11 génère aussi un résumé des sous-commandes dans l'aide principale.

CLI::App app{"dbcli — Outil de gestion de base de données"};  
app.require_subcommand(1);  

auto* cmd_migrate = app.add_subcommand("migrate", "Gérer les migrations de schéma");  
auto* cmd_seed = app.add_subcommand("seed", "Peupler avec des données de test");  
auto* cmd_backup = app.add_subcommand("backup", "Sauvegarder la base de données");  
auto* cmd_restore = app.add_subcommand("restore", "Restaurer depuis une sauvegarde");  

L'aide principale affiche la liste des sous-commandes :

$ dbcli --help
dbcli — Outil de gestion de base de données  
Usage: dbcli [OPTIONS] SUBCOMMAND  

Options:
  -h,--help    Print this help message and exit

Subcommands:
  migrate      Gérer les migrations de schéma
  seed         Peupler avec des données de test
  backup       Sauvegarder la base de données
  restore      Restaurer depuis une sauvegarde

Et chaque sous-commande affiche ses propres options :

$ dbcli backup --help
Sauvegarder la base de données  
Usage: dbcli backup [OPTIONS]  

Options:
  -h,--help                  Print this help message and exit
  --output,-o TEXT REQUIRED   Fichier de destination
  --compress,-c               Compresser la sauvegarde (gzip)
  --tables TEXT ...           Tables à sauvegarder (toutes par défaut)

Groupes de sous-commandes

Pour les outils avec de nombreuses sous-commandes, on peut les regrouper thématiquement :

auto* cmd_migrate = app.add_subcommand("migrate", "Gérer les migrations");  
cmd_migrate->group("Schéma");  

auto* cmd_seed = app.add_subcommand("seed", "Peupler avec des données de test");  
cmd_seed->group("Schéma");  

auto* cmd_backup = app.add_subcommand("backup", "Sauvegarder la base");  
cmd_backup->group("Opérations");  

auto* cmd_restore = app.add_subcommand("restore", "Restaurer une sauvegarde");  
cmd_restore->group("Opérations");  
$ dbcli --help
dbcli — Outil de gestion de base de données  
Usage: dbcli [OPTIONS] SUBCOMMAND  

Options:
  -h,--help    Print this help message and exit

Schéma:
  migrate      Gérer les migrations
  seed         Peupler avec des données de test

Opérations:
  backup       Sauvegarder la base
  restore      Restaurer une sauvegarde

Personnalisation du formatter

CLI11 utilise un objet Formatter pour produire le texte d'aide. Le formatter par défaut couvre la plupart des besoins, mais on peut le configurer ou le remplacer entièrement.

Ajuster la largeur des colonnes

Le formatter par défaut aligne les descriptions dans deux colonnes. On peut ajuster la largeur :

auto fmt = std::make_shared<CLI::Formatter>();  
fmt->column_width(40);  // Largeur de la colonne des noms d'options  
app.formatter(fmt);  

Une largeur de colonne plus grande est utile quand les noms d'options sont longs (options avec validateurs complexes). Une largeur plus petite convient aux outils avec des noms courts et concis.

Ajouter une description longue (footer)

CLI11 permet d'ajouter un texte libre en bas de l'aide, après la liste des options. C'est l'endroit idéal pour des exemples d'utilisation, des notes ou des liens :

app.footer(
    "Exemples:\n"
    "  logwatch /var/log/syslog --follow --level warn\n"
    "  logwatch app.log -n 100 --grep 'ERROR|FATAL' --format json\n"
    "  logwatch access.log -f --no-color | jq '.'\n"
    "\n"
    "Variables d'environnement:\n"
    "  LOGWATCH_CONFIG   Chemin du fichier de configuration\n"
    "  LOGWATCH_LEVEL    Niveau de log par défaut\n"
    "\n"
    "Documentation : https://example.com/logwatch"
);

Résultat à la fin de --help :

  ...

Exemples:
  logwatch /var/log/syslog --follow --level warn
  logwatch app.log -n 100 --grep 'ERROR|FATAL' --format json
  logwatch access.log -f --no-color | jq '.'

Variables d'environnement:
  LOGWATCH_CONFIG   Chemin du fichier de configuration
  LOGWATCH_LEVEL    Niveau de log par défaut

Documentation : https://example.com/logwatch

Le footer est un élément crucial d'ergonomie. Les utilisateurs de CLI lisent rarement la documentation complète — mais ils lisent le --help. Inclure deux ou trois exemples concrets couvrant les cas d'usage les plus courants accélère considérablement l'adoption.

Ajouter un en-tête descriptif

Pour une description plus longue que la ligne de titre, on peut modifier la description de l'application avec un texte multi-lignes :

app.description(
    "logwatch — Surveillance de fichiers de log en temps réel\n"
    "\n"
    "Surveille un fichier de log et affiche les nouvelles entrées\n"
    "avec filtrage par niveau, expression régulière et colorisation\n"
    "automatique. Supporte la sortie JSON pour l'intégration dans\n"
    "des pipelines de monitoring."
);

Masquer des options dans l'aide

Certaines options ne sont pas destinées à l'utilisateur final : options de débogage interne, flags expérimentaux, ou options de compatibilité ascendante qu'on souhaite maintenir sans les promouvoir. CLI11 permet de les masquer :

// Option masquée — fonctionnelle mais absente du --help
bool debug_internals = false;  
app.add_flag("--debug-internals", debug_internals,  
             "Activer le débogage interne")
    ->group("");  // Groupe vide = masqué dans l'aide

L'option reste pleinement fonctionnelle sur la ligne de commande, elle n'apparaît simplement pas dans le --help. C'est le pattern standard pour les options de débogage ou les fonctionnalités en cours de développement.

Pour regrouper explicitement les options avancées dans un groupe visible mais distinct :

// Options avancées, visibles mais clairement séparées
app.add_option("--buffer-size", buffer_sz, "Taille du buffer interne (octets)")
    ->group("Options avancées")
    ->capture_default_str();

app.add_option("--poll-interval", poll_ms, "Intervalle de polling (ms)")
    ->group("Options avancées")
    ->capture_default_str();

Aide programmatique

Au-delà de --help, il est parfois utile d'accéder au texte d'aide de manière programmatique — pour l'écrire dans un fichier, l'intégrer dans une documentation, ou l'afficher conditionnellement.

Récupérer l'aide sous forme de chaîne

std::string help_text = app.help();

app.help() retourne exactement le texte qui serait affiché par --help. On peut l'utiliser dans un callback pour afficher l'aide contextuelle :

app.callback([&]() {
    // Si aucune sous-commande n'a été invoquée, afficher l'aide
    if (app.get_subcommands().empty()) {
        std::println("{}", app.help());
    }
});

Aide d'une sous-commande spécifique

auto* cmd_build = app.add_subcommand("build", "Compiler le projet");
// ... options ...

// Plus tard, récupérer l'aide de "build" :
std::string build_help = cmd_build->help();

Énumérer les options déclarées

Pour des cas avancés (génération de documentation, complétion shell, introspection), CLI11 permet de parcourir les options enregistrées :

for (const auto* opt : app.get_options()) {
    std::println("Option : {} (requis: {}, type: {})",
                 opt->get_name(),
                 opt->get_required(),
                 opt->get_type_name());
}

Ce mécanisme d'introspection est la base sur laquelle sont construits les générateurs de complétion.


Génération de scripts de complétion

L'autocomplétion dans le shell est un confort majeur pour les utilisateurs réguliers d'un outil CLI. CLI11 ne génère pas directement les scripts de complétion, mais son API d'introspection rend l'exercice réalisable.

Approche manuelle pour Bash

Le pattern courant consiste à générer un script Bash à partir de la liste des options et sous-commandes :

auto* cmd_completion = app.add_subcommand("completion",
    "Générer un script de complétion shell");

std::string shell = "bash";  
cmd_completion->add_option("shell", shell, "Shell cible")  
    ->check(CLI::IsMember({"bash", "zsh", "fish"}))
    ->capture_default_str();

cmd_completion->callback([&]() {
    if (shell == "bash") {
        std::println("# Complétion Bash pour {}", app.get_name());
        std::println("# Ajouter à ~/.bashrc :");
        std::println("#   eval \"$({} completion bash)\"", app.get_name());
        std::println("");
        std::println("_{}_completions() {{", app.get_name());
        std::println("  local cur=\"${{COMP_WORDS[COMP_CWORD]}}\"");

        // Sous-commandes
        std::print("  local commands=\"");
        for (const auto* sub : app.get_subcommands({})) {
            std::print("{} ", sub->get_name());
        }
        std::println("\"");

        // Options globales
        std::print("  local global_opts=\"");
        for (const auto* opt : app.get_options()) {
            if (opt->get_name() != "--help" && opt->get_name() != "--version") {
                std::print("{} ", opt->get_name());
            }
        }
        std::println("\"");

        std::println("  COMPREPLY=( $(compgen -W "
                     "\"$commands $global_opts\" -- \"$cur\") )");
        std::println("}}");
        std::println("complete -F _{0}_completions {0}", app.get_name());
    }
    // ... zsh, fish ...
});
# Installer la complétion
$ eval "$(logwatch completion bash)"

# Utilisation : Tab-complétion active
$ logwatch --<TAB>
--color      --follow     --format     --grep       --help
--level      --lines      --no-color   --version

Ce script basique peut être enrichi pour gérer la complétion contextuelle par sous-commande, la complétion de valeurs pour IsMember, et la complétion de chemins de fichiers pour les options marquées ExistingFile. L'investissement en vaut la peine pour un outil distribué largement.


Conventions de l'aide : les standards à respecter

Les outils CLI les mieux conçus partagent des conventions dans leur aide. Les appliquer rend votre outil immédiatement familier :

Ligne d'usage

La ligne Usage: suit un format standardisé :

Usage: <tool> [OPTIONS] <required_args> [optional_args...]
  • [OPTIONS] entre crochets : optionnel.
  • <arg> entre chevrons : obligatoire.
  • [arg] entre crochets : optionnel.
  • arg... avec points de suspension : répétable.
  • SUBCOMMAND en majuscules : un choix parmi les sous-commandes.

CLI11 génère cette ligne automatiquement et la met à jour quand vous ajoutez ou retirez des options.

Ordre des sections dans l'aide

L'ordre conventionnel, suivi par la plupart des outils Unix, est :

1. Description (une phrase ou un paragraphe)
2. Usage (ligne synthétique)
3. Positionnels (arguments obligatoires)
4. Groupes d'options (par thème)
5. Options générales (--help, --version)
6. Sous-commandes (si applicable)
7. Footer (exemples, notes, liens)

CLI11 suit cet ordre par défaut. Les groupes d'options apparaissent dans l'ordre de première déclaration, ce qui vous donne un contrôle implicite sur le séquencement.

Descriptions d'options

Quelques conventions à suivre pour des descriptions cohérentes :

// ✓ Commencer par un verbe ou un nom, sans majuscule initiale,
//   sans point final, sur une seule ligne
"Filtrer par expression régulière"
"Nombre de lignes initiales à afficher"
"Suivre le fichier en temps réel (tail -f)"

// ✓ Mentionner l'unité quand c'est pertinent
"Timeout en millisecondes"
"Taille maximale en Mo"

// ✓ Mentionner le comportement par défaut quand il n'est pas évident
"Répertoire de sortie (défaut: répertoire courant)"

// ✗ Trop long — l'aide doit être scannable, pas lue en détail
"Spécifie le nombre maximal de lignes qui seront affichées au
 démarrage de l'outil, avant de passer en mode surveillance"

Formatter personnalisé

Pour les projets qui ont besoin d'un contrôle total sur le rendu de l'aide, CLI11 permet de remplacer le formatter par une implémentation personnalisée. On hérite de CLI::Formatter et on surcharge les méthodes souhaitées :

class CompactFormatter : public CLI::Formatter {  
public:  
    // Personnaliser le label de chaque option
    std::string make_option_opts(const CLI::Option* opt) const override {
        std::string out;
        if (opt->get_type_name() != "BOOLEAN") {
            out += " " + opt->get_type_name();
        }
        if (opt->get_required()) {
            out += " (requis)";
        }
        if (!opt->get_default_str().empty()) {
            out += " [=" + opt->get_default_str() + "]";
        }
        return out;
    }

    // Personnaliser le séparateur entre les groupes
    std::string make_group(std::string group,
                           bool is_positional,
                           std::vector<const CLI::Option*> opts) const override {
        std::string out = "\n── " + group + " ";
        out += std::string(50 - group.size(), '') + "\n";
        for (const auto* opt : opts) {
            out += make_option(opt, is_positional);
        }
        return out;
    }
};

// Application du formatter
app.formatter(std::make_shared<CompactFormatter>());

Les méthodes surchargeables les plus utiles :

Méthode Contrôle
make_help Le texte d'aide complet
make_description Le bloc de description
make_usage La ligne Usage:
make_group Un groupe d'options (titre + liste)
make_option Une ligne d'option individuelle
make_option_opts Les métadonnées d'une option (type, défaut)
make_option_desc La description d'une option
make_subcommands La section des sous-commandes
make_footer Le footer

En pratique, le formatter par défaut convient à la grande majorité des projets. La personnalisation est utile pour des outils destinés à un large public où l'image de marque et la cohérence visuelle avec d'autres outils de l'écosystème importent — par exemple, reproduire le style d'aide de kubectl ou de cargo.


Aide contextuelle : le pattern help comme sous-commande

Certains outils exposent l'aide non seulement via --help, mais aussi via une sous-commande help qui accepte un argument :

$ git help commit        # Équivalent de git commit --help
$ kubectl help get pods  # Aide pour "kubectl get pods"

CLI11 ne fournit pas ce pattern nativement, mais il s'implémente en quelques lignes :

auto* cmd_help = app.add_subcommand("help", "Afficher l'aide d'une commande");  
std::string help_topic;  
cmd_help->add_option("command", help_topic, "Sous-commande");  

cmd_help->callback([&]() {
    if (help_topic.empty()) {
        std::println("{}", app.help());
        return;
    }
    auto* sub = app.get_subcommand(help_topic);
    if (sub) {
        std::println("{}", sub->help());
    } else {
        std::println(stderr, "Commande inconnue : {}", help_topic);
        std::println("{}", app.help());
    }
});
$ mon-outil help
# Affiche l'aide générale

$ mon-outil help backup
# Affiche l'aide de la sous-commande "backup"

$ mon-outil help inexistant
Commande inconnue : inexistant
# Puis affiche l'aide générale

Récapitulatif

Technique Méthode CLI11 Effet
Description de l'app app.description(text) ou constructeur Titre/description en haut de l'aide
Version app.set_version_flag("--version,-V", str) Flag --version automatique
Grouper des options ->group("Nom") Sections thématiques dans l'aide
Masquer une option ->group("") Option fonctionnelle mais invisible
Valeur par défaut ->capture_default_str() Affiche [valeur] dans l'aide
Exemples et notes app.footer(text) Texte libre en bas de l'aide
Aide programmatique app.help() Retourne l'aide sous forme de chaîne
Introspection app.get_options() Liste des options déclarées
Largeur des colonnes formatter->column_width(n) Ajuste l'alignement
Formatter personnalisé app.formatter(shared_ptr) Contrôle total du rendu
Sous-commande help Implémentation manuelle Pattern tool help <cmd>

Cette section conclut la couverture de CLI11. Vous disposez maintenant de tous les éléments pour construire des interfaces CLI de qualité professionnelle : parsing typé (36.1.1), options et sous-commandes (36.1.2), validation et callbacks (36.1.3), et aide soignée (36.1.4). La section suivante (36.2) présente argparse, une alternative plus légère pour les outils à l'interface plus simple.

⏭️ argparse : Alternative légère