Skip to content

Latest commit

 

History

History
354 lines (249 loc) · 15.1 KB

File metadata and controls

354 lines (249 loc) · 15.1 KB

🔝 Retour au Sommaire

36.1 — CLI11 : Parsing d'arguments professionnel ⭐

Module 12 : Création d'Outils CLI — Niveau Avancé


Introduction

CLI11 est une librairie header-only de parsing d'arguments en ligne de commande pour C++11 et versions ultérieures. Créée et maintenue par Henry Schreiner, elle s'est imposée depuis plusieurs années comme la référence de l'écosystème C++ pour la construction d'interfaces CLI professionnelles. Son nom — "CLI11" — fait référence à la compatibilité minimale C++11, mais la librairie tire pleinement parti des fonctionnalités de C++17 et au-delà lorsqu'elles sont disponibles.

CLI11 se distingue par une philosophie claire : déclarer l'interface, pas l'implémenter. Plutôt que d'écrire des boucles de parsing manuelles, vous décrivez ce que votre programme accepte — options, flags, arguments positionnels, sous-commandes — et CLI11 se charge du parsing, de la validation, de la génération d'aide et des messages d'erreur.


Pourquoi CLI11 ?

Le problème avec les approches traditionnelles

Avant CLI11, les options disponibles en C++ pour parser des arguments en ligne de commande présentaient chacune des limitations significatives :

getopt / getopt_long (C POSIX) — L'approche historique. Fonctionnelle mais verbeuse : chaque option nécessite un case dans un switch, la validation est entièrement manuelle, et il n'y a aucun support natif pour les sous-commandes. Le code de parsing finit par éclipser la logique métier du programme.

Boost.Program_options — Puissante mais lourde. Elle impose une dépendance à Boost (compilation nécessaire, pas header-only), sa syntaxe à base de variables_map et de notify() est peu intuitive, et la gestion des sous-commandes est laborieuse.

Parsing manuel de argv — La tentation du "je fais tout moi-même". Cela fonctionne pour --help et un ou deux flags, mais devient rapidement ingérable dès qu'on dépasse une poignée d'options. La gestion des erreurs est invariablement incomplète.

Ce que CLI11 apporte

CLI11 résout ces problèmes avec une API moderne et expressive :

#include <CLI/CLI.hpp>
#include <string>
#include <print>

int main(int argc, char* argv[]) {
    CLI::App app{"deptool — Gestionnaire de dépendances C++"};
    app.require_subcommand(1);  // Exactement une sous-commande requise

    // Sous-commande "install"
    auto* install_cmd = app.add_subcommand("install", "Installer une dépendance");
    std::string package_name;
    std::string version = "latest";
    int jobs = 4;
    bool system_wide = false;

    install_cmd->add_option("package", package_name, "Nom du paquet")
        ->required();
    install_cmd->add_option("--version,-v", version, "Version à installer");
    install_cmd->add_option("--jobs,-j", jobs, "Nombre de jobs parallèles")
        ->check(CLI::Range(1, 32));
    install_cmd->add_flag("--system-wide,-s", system_wide, "Installation système");

    // Sous-commande "list"
    auto* list_cmd = app.add_subcommand("list", "Lister les dépendances");
    bool outdated = false;
    list_cmd->add_flag("--outdated", outdated, "Afficher uniquement les obsolètes");

    CLI11_PARSE(app, argc, argv);

    if (install_cmd->parsed()) {
        std::println("Installation de {} v{} ({} jobs, système: {})",
                     package_name, version, jobs, system_wide);
    } else if (list_cmd->parsed()) {
        std::println("Liste des dépendances{}", outdated ? " obsolètes" : "");
    }

    return 0;
}

Ce programme gère automatiquement :

$ deptool install boost --version 1.87 -j 8 --system-wide
Installation de boost v1.87 (8 jobs, système: true)

$ deptool install
Error: package is required  
Run with --help for more information.  

$ deptool install boost --jobs 99
Error: --jobs: Value 99 not in range [1, 32]

$ deptool --help
deptool — Gestionnaire de dépendances C++  
Usage: deptool [OPTIONS] SUBCOMMAND  

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

Subcommands:
  install      Installer une dépendance
  list         Lister les dépendances

$ deptool install --help
Installer une dépendance  
Usage: deptool install [OPTIONS] package  

Positionals:
  package TEXT REQUIRED    Nom du paquet

Options:
  -h,--help               Print this help message and exit
  --version,-v TEXT        Version à installer
  --jobs,-j INT:INT in [1 - 32]
                           Nombre de jobs parallèles
  --system-wide,-s        Installation système

En une trentaine de lignes déclaratives, on obtient un parsing complet avec validation, aide contextuelle par sous-commande, et messages d'erreur explicites — sans écrire une seule ligne de logique de parsing.


Caractéristiques principales

Header-only et zéro dépendance

CLI11 se distribue en un unique fichier header (CLI11.hpp), ou sous forme de headers séparés pour les projets qui préfèrent cette organisation. Elle ne dépend d'aucune librairie externe — pas de Boost, pas d'ICU, rien. Cela la rend triviale à intégrer, que ce soit par simple copie du fichier, via FetchContent dans CMake, ou à travers un gestionnaire de paquets comme Conan ou vcpkg.

Binding direct aux variables

L'une des forces majeures de CLI11 est le binding direct : les options sont liées à des variables C++ existantes. Pas de variables_map intermédiaire, pas de cast, pas de .as<int>(). Quand le parsing est terminé, vos variables contiennent directement les bonnes valeurs, avec le bon type :

int port = 8080;           // Valeur par défaut  
std::string host = "0.0.0.0";  
bool verbose = false;  

app.add_option("--port,-p", port, "Port d'écoute");  
app.add_option("--host,-H", host, "Adresse d'écoute");  
app.add_flag("--verbose,-v", verbose, "Mode verbeux");  

CLI11_PARSE(app, argc, argv);

// Ici, port, host et verbose ont les valeurs
// passées en ligne de commande (ou les valeurs par défaut)

Ce mécanisme fonctionne avec tous les types courants : entiers, flottants, std::string, bool, std::vector (pour les options répétables), std::optional, énumérations, et même des types personnalisés via des convertisseurs.

Système de validation intégré

CLI11 fournit des validateurs composables que l'on chaîne sur les options :

app.add_option("--port,-p", port, "Port")
    ->check(CLI::Range(1, 65535));

app.add_option("--config,-c", config_path, "Fichier de configuration")
    ->check(CLI::ExistingFile);

app.add_option("--output,-o", output_dir, "Répertoire de sortie")
    ->check(CLI::ExistingDirectory);

app.add_option("--format,-f", format, "Format de sortie")
    ->check(CLI::IsMember({"json", "text", "table"}));

app.add_option("--level,-l", level, "Niveau")
    ->check(CLI::Range(1, 10))
    ->check(CLI::PositiveNumber);

Quand une validation échoue, CLI11 produit un message d'erreur clair et quitte avec le code de retour approprié. Vous pouvez également créer vos propres validateurs.

Sous-commandes imbriquées

CLI11 supporte des sous-commandes sur plusieurs niveaux, comme les outils CLI modernes s'y attendent :

mytool remote add origin https://example.com  
mytool remote list --verbose  

Chaque sous-commande est un objet CLI::App à part entière, avec ses propres options, ses propres validateurs et sa propre aide. La méthode require_subcommand() permet de contrôler combien de sous-commandes sont attendues, et la méthode parsed() permet de déterminer laquelle a été invoquée après le parsing.

Groupes d'options et exclusions mutuelles

Pour les interfaces complexes, CLI11 permet de regrouper des options et de définir des contraintes logiques entre elles :

// Deux options mutuellement exclusives
auto* opt_json = app.add_flag("--json", use_json, "Sortie JSON");  
auto* opt_table = app.add_flag("--table", use_table, "Sortie tableau");  
opt_json->excludes(opt_table);  
opt_table->excludes(opt_json);  

// Une option qui en requiert une autre
auto* opt_user = app.add_option("--user", user, "Utilisateur");  
auto* opt_pass = app.add_option("--password", password, "Mot de passe");  
opt_pass->needs(opt_user);  // --password requiert --user  

Variables d'environnement

CLI11 peut lier une option à une variable d'environnement en fallback. Si l'utilisateur ne passe pas l'option en ligne de commande, CLI11 vérifie automatiquement la variable d'environnement correspondante :

app.add_option("--token", token, "Token d'API")
    ->envname("DEPTOOL_API_TOKEN");
$ export DEPTOOL_API_TOKEN="abc123"
$ deptool search boost
# token vaut "abc123" sans avoir été passé en argument

Cela suit la convention des Twelve-Factor Apps et s'intègre naturellement dans des environnements conteneurisés où la configuration passe par des variables d'environnement.

Fichiers de configuration

Au-delà des variables d'environnement, CLI11 peut lire des valeurs depuis un fichier de configuration au format INI ou TOML. L'ordre de priorité est intuitif : les arguments en ligne de commande priment sur les variables d'environnement, qui priment sur le fichier de configuration, qui prime sur les valeurs par défaut.

app.set_config("--config", "deptool.ini", "Fichier de configuration");

CLI11 comparé aux alternatives

CLI11 vs getopt_long

getopt_long reste présent dans de nombreuses codebases legacy. Voici le même parsing minimal avec les deux approches pour illustrer la différence d'expressivité.

Avec getopt_long :

#include <getopt.h>
#include <cstdlib>
#include <print>
#include <string>

int main(int argc, char* argv[]) {
    int port = 8080;
    std::string host = "0.0.0.0";
    bool verbose = false;

    static struct option long_options[] = {
        {"port",    required_argument, nullptr, 'p'},
        {"host",    required_argument, nullptr, 'H'},
        {"verbose", no_argument,       nullptr, 'v'},
        {"help",    no_argument,       nullptr, 'h'},
        {nullptr,   0,                 nullptr, 0}
    };

    int opt;
    while ((opt = getopt_long(argc, argv, "p:H:vh", long_options, nullptr)) != -1) {
        switch (opt) {
            case 'p':
                port = std::atoi(optarg);
                if (port < 1 || port > 65535) {
                    std::println(stderr, "Erreur: port invalide");
                    return 2;
                }
                break;
            case 'H':
                host = optarg;
                break;
            case 'v':
                verbose = true;
                break;
            case 'h':
                std::println("Usage: server [--port|-p PORT] "
                             "[--host|-H ADDR] [--verbose|-v]");
                return 0;
            default:
                return 2;
        }
    }

    std::println("Serveur sur {}:{} (verbose: {})", host, port, verbose);
    return 0;
}

Avec CLI11 :

#include <CLI/CLI.hpp>
#include <print>
#include <string>

int main(int argc, char* argv[]) {
    CLI::App app{"server — Serveur HTTP minimal"};

    int port = 8080;
    std::string host = "0.0.0.0";
    bool verbose = false;

    app.add_option("--port,-p", port, "Port d'écoute")
        ->check(CLI::Range(1, 65535));
    app.add_option("--host,-H", host, "Adresse d'écoute");
    app.add_flag("--verbose,-v", verbose, "Mode verbeux");

    CLI11_PARSE(app, argc, argv);

    std::println("Serveur sur {}:{} (verbose: {})", host, port, verbose);
    return 0;
}

Le constat est net : CLI11 élimine tout le boilerplate (le tableau option, le switch, la conversion manuelle de optarg, la validation manuelle, le message d'aide écrit à la main) pour ne garder que l'intention déclarative. Et le gain s'amplifie avec la complexité : dès que vous ajoutez des sous-commandes, des validations croisées ou des groupes d'options, getopt devient difficilement maintenable.

CLI11 vs Boost.Program_options

Boost.Program_options offre des fonctionnalités similaires à CLI11, mais avec plusieurs inconvénients pratiques :

  • Dépendance Boost : nécessite la compilation de Boost (ou au minimum le linkage avec libboost_program_options), là où CLI11 est un simple header.
  • API moins intuitive : l'accès aux valeurs passe par un variables_map avec des as<T>() et du count(), plus verbeux que le binding direct de CLI11.
  • Sous-commandes laborieuses : le support des sous-commandes dans Boost.Program_options est possible mais nécessite du code supplémentaire conséquent.
  • Messages d'erreur : les exceptions levées par Boost contiennent des messages techniques moins lisibles que les sorties formatées de CLI11.

Boost.Program_options reste un choix raisonnable si votre projet dépend déjà de Boost pour d'autres raisons, mais pour un nouveau projet, CLI11 est préférable.

CLI11 vs argparse

La librairie argparse (que nous couvrons en section 36.2) offre une API inspirée du module argparse de Python. Elle est plus simple que CLI11 et convient bien aux outils légers, mais elle est moins riche en fonctionnalités : son support des sous-commandes, des validateurs composables et des fichiers de configuration est plus limité. Pour un utilitaire avec quelques options, argparse est parfait. Pour un outil à la kubectl avec une hiérarchie de sous-commandes, CLI11 est le bon choix.


Quand utiliser CLI11

CLI11 est le bon choix quand votre programme remplit un ou plusieurs de ces critères :

  • Il expose des sous-commandes (pattern <tool> <command> [options]).
  • Il a plus d'une poignée d'options avec des validations non triviales.
  • Il doit supporter des fichiers de configuration et/ou des variables d'environnement.
  • Il est destiné à être distribué à des utilisateurs externes (l'aide auto-générée et les messages d'erreur clairs sont essentiels).
  • Vous voulez une API déclarative qui rend le code de parsing lisible et maintenable.

Pour un script interne avec deux flags, argparse ou même un parsing manuel reste acceptable. Mais dès que la complexité augmente, CLI11 rentabilise son adoption très rapidement.


Plan de la section

Les sous-sections suivantes vous guident de l'installation à la maîtrise :

Sous-section Contenu
36.1.1 Installation et premiers pas — intégration via CMake FetchContent, premier programme fonctionnel
36.1.2 Options, flags et sous-commandes — l'API en détail, patterns courants
36.1.3 Validation et callbacks — validateurs intégrés, validateurs personnalisés, logique post-parsing
36.1.4 Génération d'aide automatique — personnalisation du formatter, groupes d'options dans l'aide

CLI11 est une compétence directement applicable. La majorité des outils CLI modernes en C++ dans l'industrie — agents de monitoring, CLIs d'infrastructure, outils de build — utilisent CLI11 ou une approche équivalente. Maîtriser cette librairie, c'est être capable de livrer rapidement des outils que vos collègues et utilisateurs auront plaisir à utiliser.

⏭️ Installation et premiers pas