Gestion des arguments de ligne de commande facile à utiliser, puissante et expressive pour C++ 11/14/17 contenue dans un seul fichier d'en-tête .
options, options+valeur(s), valeurs de position, commandes de position, alternatives imbriquées, arbres de décision, indicateurs joignables, filtres de valeurs personnalisés, ...
génération de documentation (lignes d'utilisation, pages de manuel) ; gestion des erreurs
beaucoup d'exemples; grand ensemble de tests
Considérez cette interface de ligne de commande :
SYNOPSIS convertir <fichier d'entrée> [-r] [-o <format de sortie>] [-utf16] OPTIONS -r, --recursive convertir les fichiers de manière récursive -utf16 utilise l'encodage UTF-16
Voici le code qui définit le input file
de valeur de position et les trois options -r
, -o
et -utf16
. Si l'analyse échoue, l'extrait de code par défaut ci-dessus, semblable à une page de manuel, sera imprimé sur la sortie standard.
#include <iostream>#include "clipp.h"en utilisant l'espace de noms clipp ; en utilisant std :: cout ; en utilisant std::string;int main(int argc, char* argv[]) { bool rec = false, utf16 = false; string infile = "", fmt = "csv";auto cli = (value("input file", infile),option("-r", "--recursive").set(rec).doc("convertir les fichiers récursivement"),option("-o") & value("format de sortie", fmt),option("-utf16").set(utf16).doc("utiliser l'encodage UTF-16") );if(!parse(argc, argv, cli)) cout << make_man_page(cli, argv[0]);// ...}
SYNOPSIS finder make <fichier de mots> -dict <dictionnaire> [--progress] [-v] finder find <infile>... -dict <dictionary> [-o <outfile>] [-split|-nosplit] [-v] aide du chercheur [-v] OPTIONS --progress, -p afficher la progression -o, --output <outfile> écrire dans un fichier au lieu de stdout -split, -nosplit (ne pas) diviser la sortie -v, --version afficher la version
Cette CLI a trois commandes alternatives ( make
, find
, help
), des arguments de valeur de position ( <wordfile>
, <infile>
) dont un est répétable, un indicateur obligatoire avec un argument de valeur ( -dict <dictionary>
), un option avec argument valeur ( -o <outfile>
), une option avec deux alternatives ( -split
, -nosplit
) et deux options conventionnelles ( -v
, --progress
).
Voici le code qui définit l'interface, génère l'extrait de page de manuel ci-dessus et gère le résultat de l'analyse :
en utilisant l'espace de noms clipp ; en utilisant std :: cout ; en utilisant std::string;//variables stockant le résultat de l'analyse ; initialisés avec leurs valeurs par défaut en mode classe {make, find, help} ; mode sélectionné = mode::help; entrée std::vector<string> ; chaîne dict, out;bool split = false, progr = false;dictionnaire automatique = requis("-dict") & value("dictionnaire", dict);auto makeMode = (command("make").set(selected,mode ::faire), valeurs ("fichier mot", entrée), dictionnaire, option("--progress", "-p").set(progr) % "show progress" );auto findMode = (command("find").set(selected,mode::find), valeurs("infile", entrée), dictionnaire, (option("-o", "--output") & value("outfile", out)) % "écrire dans un fichier au lieu de la sortie standard", ( option("-split" ).set(split,true) | option("-nosplit").set(split,false) ) % "(ne pas) diviser la sortie" );auto cli = ( (makeMode | findMode | command("help").set(selected,mode::help) ),option("-v", "--version").call([]{cout << "version 1.0nn" ;}).doc("show version") );if(parse(argc, argv, cli)) {switch(selected) {case mode::make: /* ... */ break;case mode::find : /* ... */ break;case mode::help: cout << make_man_page(cli, "finder"); casser; } } autre { cout << usage_lines(cli, "finder") << 'n'; }
Vous trouverez ci-dessous quelques exemples qui devraient vous donner une idée du fonctionnement de clipp. Considérez cette configuration de base avec quelques variables que nous souhaitons définir à l'aide d'arguments de ligne de commande :
int main(int argc, char* argv[]) { using namespace clipp;// définir certaines variablesbool a = false, b = false;int n = 0, k = 0;double x = 0.0, y = 0.0; std::vector<int> ids;auto cli = ( /* L'INTERFACE DE LIGNE DE COMMANDE DE DÉFINITION DU CODE VA ICI */ );parse(argc, argv, cli); // exclut argv[0]std::cout << usage_lines(cli, "exe") << 'n'; }
Interface ( usage_lines ) | Code (contenu des parenthèses cli ) |
---|---|
exe [-a] | option("-a", "--all").set(a) |
exe [--all] | option("--all", "-a", "--ALL").set(a) |
exe [-a] [-b] | option("-a").set(a), option("-b").set(b) |
exe -a | required("-a").set(a) |
exe [-a] -b | option("-a").set(a), required("-b").set(b) |
exe [-n <times>] | option("-n", "--iter") & value("times", n) |
exe [-n [<times>]] | option("-n", "--iter") & opt_value("times", n) |
exe -n <times> | required("-n", "--iter") & value("times", n) |
exe -n [<times>] | required("-n", "--iter") & opt_value("times", n) |
exe [-c <x> <y>] | option("-c") & value("x", x) & value("y", y) |
exe -c <x> <y> | required("-c") & value("x", x) & value("y", y) |
exe -c <x> [<y>] | required("-c") & value("x", x) & opt_value("y", y) |
exe [-l <lines>...] | option("-l") & values("lines", ids) |
exe [-l [<lines>...]] | option("-l") & opt_values("lines", ids) |
exe [-l <lines>]... | repeatable( option("-l") & value("lines", ids) ) |
exe -l <lines>... | required("-l") & values("lines", ids) |
exe -l [<lines>...] | required("-l") & opt_values("lines", ids) |
exe (-l <lines>)... | repeatable( required("-l") & value("lines", ids) ) |
exe fetch [-a] | command("fetch").set(k,1), option("-a").set(a) |
exe init | fetch [-a] | command("init").set(k,0) | (command("fetch").set(k,1), option("-a").set(a)) |
exe [-a|-b] | option("-a").set(a) | option("-b").set(b) |
exe [-ma|b] | option("-m") & (required("a").set(a) | required("b").set(b)) |
Consultez la section exemples pour des explications détaillées sur chaque sujet.
Les qualificateurs d'espace de noms sont omis de tous les exemples pour une meilleure lisibilité. Toutes les entités sont définies dans namespace clipp
.
int main(int argc, char* argv[]) { using namespace clipp;auto cli = ( /* L'INTERFACE DE LIGNE DE COMMANDE DE DÉFINITION DU CODE VA ICI */ );parse(argc, argv, cli); //exclut argv[0]//si vous souhaitez inclure argv[0]//parse(argv, argv+argc, cli);}
Il existe deux types de blocs de construction pour les interfaces de ligne de commande : les paramètres et les groupes. Les fonctions d'usine aux noms pratiques produisent des paramètres ou des groupes avec les paramètres souhaités appliqués.
bool a = faux, f = faux ; chaîne s ; vector<string> vs;auto cli = ( // correspond à la commande positionnelle répétable requise("push"), // exactement oui oui norequired("-f", "--file").set(f), // exactement oui non norequired("-a", "--all", "-A").set(a), // exactement non non non value("file", s), // tout argument oui oui novalues("file", vs), // tout argument oui oui ouiopt_value("file", s), // tout argument non oui noopt_values("file" , vs), // tout argument non oui oui//paramètre "catch all" - utile pour la gestion des erreursany_other(vs), // tout argument non non oui//attrape les arguments qui remplissent un prédicat et ne correspondent pas à d'autres paramètresany(prédicat, vs) // prédicat non non oui);
Les fonctions ci-dessus sont des usines de commodité :
bool f = vrai ; chaîne s;auto v1 = valeurs("fichier", s);// est équivalent à :auto v2 = paramètre{match::nonempty}.label("fichier").blocking(true).repeatable(true).set (s);auto r1 = requis("-f", "--file").set(f);// est équivalent à :auto r2 = paramètre{"-f", "--file"}.required (vrai).set(f);
un paramètre obligatoire doit correspondre à au moins un argument de ligne de commande
un paramètre répétable peut correspondre à n'importe quel nombre d'arguments
les paramètres non positionnels (= non bloquants) peuvent correspondre aux arguments dans n'importe quel ordre
un paramètre positionnel (bloquant) définit un "point d'arrêt", c'est-à-dire que jusqu'à ce qu'il corresponde, tous les paramètres qui le suivent ne sont pas autorisés à correspondre ; une fois qu'il correspond, tous les paramètres qui le précèdent (dans le groupe actuel) deviendront inaccessibles
Si vous souhaitez que les paramètres correspondent dans l'ordre, vous pouvez les lier ensemble à l'aide de operator &
ou de la fonction de regroupement in_sequence
:
entier n = 1 ; chaîne s ; vector<int> ls;auto cli = (//option avec valeur requiseoption("-n", "--repeat") & value("times", n),//indicateur requis avec valeur facultativerequired("--file ") & opt_value("name", s), //option avec exactement deux valeursoption("-p", "--pos") & value("x") & value("y"),//identique à avant v vin_sequence( option("-p", "--pos") , value("x") , value("y") ), //option avec au moins une valeur (et éventuellement plus)option("-l") & valeurs("lignes", ls) );
Les paramètres de valeur utilisent une fonction de filtre pour tester s'ils sont autorisés à correspondre à une chaîne d'argument. Le filtre par défaut match::nonempty
utilisé par value
, values
, opt_value
et opt_values
correspondra à toute chaîne d'argument non vide. Vous pouvez soit fournir d'autres fonctions de filtre/objets de fonction comme premier argument de value
, values
, etc., soit utiliser l'une de ces fonctions d'usine abrégées intégrées couvrant les cas les plus courants :
nom de chaîne ; doubler = 0,0 ; int n = 0;auto cli = (value("user", name), // correspond à n'importe quel mot de chaîne non vide ("user", name), // correspond à n'importe quel numéro de chaîne alphanumérique non vide ("ratio", r) , // correspond aux représentations sous forme de chaîne de nombres entiers("times", n) // correspond aux représentations sous forme de chaîne d'entiers);
De manière analogue à value
, opt_value
, etc., il existe également des fonctions pour words
, opt_word
, etc.
auto is_char = [](const string& arg) { return arg.size() == 1 && std::isalpha(arg[0]); };char c = ' '; // correspond à la valeur répétitive de position requise (is_char, "c", c); // un caractère oui oui non
regrouper les paramètres mutuellement compatibles avec des parenthèses et des virgules :
auto cli = ( option("-a"), option("-b"), option("-c") );
regrouper des paramètres mutuellement exclusifs comme alternatives à l'aide operator |
ou one_of
:
auto cli1 = ( value("input_file") | command("list") | command("flush") );auto cli2 = one_of( value("input_file") , command("list") , command("flush" ) );
regrouper les paramètres afin qu'ils doivent correspondre dans l'ordre à l'aide operator &
ou in_sequence
:
double x = 0, y = 0, z = 0;auto cli1 = ( option("-pos") & valeur("X",x) & valeur("Y",y) & valeur("Z",z ) );auto cli2 = in_sequence( option("-pos") , value("X",x) , value("Y",y) , value("Z",z) );
Notez que les groupes environnants ne sont pas affectés par cela, de sorte que -a
et -b
peuvent correspondre dans n'importe quel ordre tandis que -b
et la valeur X
doivent correspondre dans l'ordre :
bool a = faux, b = faux ; int x = 0;auto cli = ( option("-a").set(a), option("-b").set(b) & value("X",x) );
les groupes peuvent être imbriqués et combinés pour former des interfaces arbitrairement complexes (voir ici et ici) :
auto cli = ( command("push") | ( command("pull"), option("-f", "--force") ) );
les groupes peuvent également être reproductibles :
auto cli1 = répétable( command("flip") | command("flop") );
forcer les préfixes communs sur un groupe de drapeaux :
int x = 0;auto cli1 = with_prefix("-", option("a"), option("b") & value("x",x), ... ); // => -a -b ^non affecté^auto cli2 = with_prefix_short_long("-", "--", option("a", "all"), option("b"), ... ); // => -a --all -b
forcer les suffixes communs sur un groupe de drapeaux :
int x = 0;auto cli1 = with_suffix("=", option("a") & value("x",x), ... ); // => a= ^non affecté^auto cli2 = with_suffix_short_long(":", ":=", option("a", "all"), option("b"), ... ); // => a : tous := b :
créer un groupe de drapeaux joignables :
auto cli1 = joinable( option("-a"), option("-b")); //correspondra à "-a", "-b", "-ab", "-ba"//fonctionne également avec des préfixes communs arbitraires :auto cli2 = joinable( option("--xA0"), option("- -xB1")); // correspondra également à "--xA0B1" ou "--xB1A0"
Le moyen le plus simple de connecter l'interface de ligne de commande au reste de votre code est de lier les valeurs d'objet ou les appels de fonction (objet) aux paramètres (voir aussi ici) :
bool b = faux ; int je = 5; entier m = 0 ; chaîne x ; ifstream fs;auto cli = ( option("-b").set(b), // "-b" détecté -> définir b sur trueoption("-m").set(m,2), // " -m" détecté -> définir m sur 2option("-x") & value("X", x), // définir la valeur de x à partir de la chaîne arg option("-i") & opt_value("i", i) , // définit la valeur de i à partir de arg string option("-v").call( []{ cout << "v"; } ), // appelle la fonction (objet) / lambdaoption("-v")( []{ cout << "v"; } ), // identique à la ligneoption précédente(" -f") & value("file").call([&](string f){ fs.open(f); }) );
Dans le code de production, on utiliserait probablement une classe de paramètres :
paramètres de structure { bool x = false ; /* ... */ }; paramètres cmdline_settings(int argc, char* argv[]) { paramètres s;auto cli = ( option("-x").set(sx), /* ... */ );parse(argc, argv, cli);return s; }
Notez que la cible doit être soit :
un type fondamental ( int, long int, float, double, ...
)
un type convertible à partir de const char*
une entité appelable : fonction, objet fonction / lambda qui a soit une liste de paramètres vide, soit exactement un paramètre convertible à partir de const char*
Les docstrings pour les groupes et pour les paramètres peuvent être définis avec la fonction membre doc
ou avec operator %
:
autocli = ( ( option("x").set(x).doc("ensembles X"),option("y").set(y) % "ensembles Y" ), "groupe documenté 1 :" % ( option("-g").set(g).doc("active G"), option("-h").set(h) % "active H" ), ( option("-i").set(i) % "active I", option("-j").set(j) % "active J" ).doc("groupe documenté 2 :") );
Lignes d'utilisation :
cout << usage_lines(cli, "progname") << 'n';//avec options de formatageauto fmt = doc_formatting{} .première_colonne(3) .last_column(79); cout << usage_lines(cli, "progname", fmt) << 'n';
Documentation détaillée :
cout << documentation(cli) << 'n';//avec options de formatageauto fmt = doc_formatting{} .première_colonne(7) .doc_colonne(15) .dernière_colonne(99); cout << documentation(cli, fmt) << 'n';
Pages de manuel :
auto cli = ( /*L'INTERFACE DE LIGNE DE COMMANDE DE DÉFINITION DU CODE VA ICI*/ ); cout << make_man_page(cli, "progname") << 'n';//avec options de formatageauto fmt = doc_formatting{} .première_colonne(7) .doc_colonne(15) .dernière_colonne(99); cout << make_man_page(cli, "progname", fmt) << 'n';
Chaque paramètre peut être associé à des fonctions de gestionnaire d'événements. Ceux-ci sont invoqués une fois pour chaque argument mappé au paramètre (ou une fois par événement manquant) :
string file = "default.txt";auto param = requis("-nof").set(file,"") | requis("-f") & valeur("fichier", fichier) // le 2ème, le 3ème, le 4ème,... match (serait une erreur dans ce cas) .if_repeated( [] { /* ... */ } ) // si le paramètre de valeur requis était manquant .if_missing( [] { /* ... */ } ) // si inaccessible, par exemple pas d'indicateur "-f" avant le nom de fichier .if_blocked( [] { /* ... */ } ) // si la correspondance est en conflit avec une autre alternative "-nof" .if_conflicted( [] { /* ... */ } );
Les fonctions du gestionnaire peuvent également prendre un int, qui est défini sur l'index d'argument auquel l'événement s'est produit en premier :
string file = "default.txt";auto param = require("-nof").set(file,"") | requis("-f") & valeur("fichier", fichier) .if_repeated ( [] (int argIdx) { /* ... */ } ) .if_missing ( [] (int argIdx) { /* ... */ } ) .if_blocked ( [] (int argIdx) { /* ... */ } ) .if_conflicted( [] (int argIdx) { /* ... */ } );
Si nous donnons -f -b
ou -b -f -a
comme arguments de ligne de commande pour la CLI suivante, une erreur sera signalée, car la valeur après -f
n'est pas facultative :
auto cli = ( option("-a"), option("-f") & value("filename"), option("-b") );
Ce comportement convient à la plupart des cas d'utilisation. Mais que se passe-t-il si nous voulons que notre programme prenne n'importe quelle chaîne comme nom de fichier, car nos noms de fichiers peuvent également entrer en collision avec des noms d'indicateurs ? Nous pouvons rendre le paramètre value gourmand avec operator !
. De cette façon, la chaîne suivante après -f
aura toujours la priorité la plus élevée dès que -f
sera donnée :
auto cli = ( option("-a"), option("-f") & !value("filename"), option("-b") );// ^~~~~~
Soyez très prudent avec les paramètres gourmands !
auto cli = ( /* votre interface ici */ );auto res = parse(argc, argv, cli);if(res.any_error()) { /* ... */ }//aggregated erreursif(res.unmapped_args_count ()) { /* ... */ }if(res.any_bad_repeat()) { /* ... */ }if(res.any_blocked()) { /* ... */ }if(res.any_conflict()) { /* ... */ }