muellan/clipp(opens in a new tab)
Language: C++
easy to use, powerful & expressive command line argument parsing for modern C++ / single header / usage & doc generation
- stars
- 1,325
- watchers
- 26
- forks
- 153
- open issues
- 55
- Last updated
- 05/30/2024
Language: C++
easy to use, powerful & expressive command line argument parsing for modern C++ / single header / usage & doc generation
Easy to use, powerful and expressive command line argument handling for C++11/14/17 contained in a single header file.
options, options+value(s), positional values, positional commands, nested alternatives, decision trees, joinable flags, custom value filters, ...
documentation generation (usage lines, man pages); error handling
lots of examples; large set of tests
Consider this command line interface:
SYNOPSIS
convert <input file> [-r] [-o <output format>] [-utf16]
OPTIONS
-r, --recursive convert files recursively
-utf16 use UTF-16 encodingHere is the code that defines the positional value input file and the three options -r, -o and -utf16. If parsing fails, the above default man page-like snippet will be printed to stdout.
#include <iostream> #include "clipp.h" using namespace clipp; using std::cout; using 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("convert files recursively"), option("-o") & value("output format", fmt), option("-utf16").set(utf16).doc("use UTF-16 encoding") ); if(!parse(argc, argv, cli)) cout << make_man_page(cli, argv[0]); // ... }
SYNOPSIS
finder make <wordfile> -dict <dictionary> [--progress] [-v]
finder find <infile>... -dict <dictionary> [-o <outfile>] [-split|-nosplit] [-v]
finder help [-v]
OPTIONS
--progress, -p show progress
-o, --output <outfile> write to file instead of stdout
-split, -nosplit (do not) split output
-v, --version show versionThis CLI has three alternative commands (make, find, help), some positional value-arguments (<wordfile>, <infile>) of which one is repeatable, a required flag with value-argument (-dict <dictionary>), an option with value-argument (-o <outfile>), one option with two alternatives (-split, -nosplit) and two conventional options (-v, --progress).
Here is the code that defines the interface, generates the man page snippet above and handles the parsing result:
using namespace clipp; using std::cout; using std::string; //variables storing the parsing result; initialized with their default values enum class mode {make, find, help}; mode selected = mode::help; std::vector<string> input; string dict, out; bool split = false, progr = false; auto dictionary = required("-dict") & value("dictionary", dict); auto makeMode = ( command("make").set(selected,mode::make), values("wordfile", input), dictionary, option("--progress", "-p").set(progr) % "show progress" ); auto findMode = ( command("find").set(selected,mode::find), values("infile", input), dictionary, (option("-o", "--output") & value("outfile", out)) % "write to file instead of stdout", ( option("-split" ).set(split,true) | option("-nosplit").set(split,false) ) % "(do not) split output" ); auto cli = ( (makeMode | findMode | command("help").set(selected,mode::help) ), option("-v", "--version").call([]{cout << "version 1.0\n\n";}).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"); break; } } else { cout << usage_lines(cli, "finder") << '\n'; }
Below are a few examples that should give you an idea for how clipp works. Consider this basic setup with a few variables that we want to set using command line arguments:
int main(int argc, char* argv[]) { using namespace clipp; // define some variables bool a = false, b = false; int n = 0, k = 0; double x = 0.0, y = 0.0; std::vector<int> ids; auto cli = ( /* CODE DEFINING COMMAND LINE INTERFACE GOES HERE */ ); parse(argc, argv, cli); //excludes argv[0] std::cout << usage_lines(cli, "exe") << '\n'; }
Interface (usage_lines) |
Code (content of cli parentheses ) |
|---|---|
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 [-m a|b] |
option("-m") & (required("a").set(a) | required("b").set(b)) |
See the examples section for detailed explanations of each topic.
Namespace qualifiers are omitted from all examples for better readability. All entities are defined in namespace clipp.
int main(int argc, char* argv[]) { using namespace clipp; auto cli = ( /* CODE DEFINING COMMAND LINE INTERFACE GOES HERE */ ); parse(argc, argv, cli); //excludes argv[0] //if you want to include argv[0] //parse(argv, argv+argc, cli); }
There are two kinds of building blocks for command line interfaces: parameters and groups. Convieniently named factory functions produce parameters or groups with the desired settings applied.
bool a = false, f = false; string s; vector<string> vs; auto cli = ( // matches required positional repeatable command("push"), // exactly yes yes no required("-f", "--file").set(f), // exactly yes no no required("-a", "--all", "-A").set(a), // exactly no no no value("file", s), // any arg yes yes no values("file", vs), // any arg yes yes yes opt_value("file", s), // any arg no yes no opt_values("file", vs), // any arg no yes yes //"catch all" parameter - useful for error handling any_other(vs), // any arg no no yes //catches arguments that fulfill a predicate and aren't matched by other parameters any(predicate, vs) // predicate no no yes );
The functions above are convenience factories:
bool f = true; string s; auto v1 = values("file", s); // is equivalent to: auto v2 = parameter{match::nonempty}.label("file").blocking(true).repeatable(true).set(s); auto r1 = required("-f", "--file").set(f); // is equivalent to: auto r2 = parameter{"-f", "--file"}.required(true).set(f);
If you want parameters to be matched in sequence, you can tie them together using either operator & or the grouping function in_sequence:
int n = 1; string s; vector<int> ls; auto cli = ( //option with required value option("-n", "--repeat") & value("times", n), //required flag with optional value required("--file") & opt_value("name", s), //option with exactly two values option("-p", "--pos") & value("x") & value("y"), //same as before v v in_sequence( option("-p", "--pos") , value("x") , value("y") ), //option with at least one value (and optionally more) option("-l") & values("lines", ls) );
Value parameters use a filter function to test if they are allowed to match an argument string. The default filter match::nonempty that is used by value, values, opt_value and opt_values will match any non-empty argument string.
You can either supply other filter functions/function objects as first argument of value, values, etc. or use one of these built-in shorthand factory functions covering the most common cases:
string name; double r = 0.0; int n = 0; auto cli = ( value("user", name), // matches any non-empty string word("user", name), // matches any non-empty alphanumeric string number("ratio", r), // matches string representations of numbers integer("times", n) // matches string representations of integers );
Analogous to value, opt_value, etc. there are also functions for words, opt_word, etc.
auto is_char = [](const string& arg) { return arg.size() == 1 && std::isalpha(arg[0]); }; char c = ' '; // matches required positional repeatable value(is_char, "c", c); // one character yes yes no
group mutually compatible parameters with parentheses and commas:
auto cli = ( option("-a"), option("-b"), option("-c") );
group mutually exclusive parameters as alternatives using operator | or one_of:
auto cli1 = ( value("input_file") | command("list") | command("flush") ); auto cli2 = one_of( value("input_file") , command("list") , command("flush") );
group parameters so that they must be matched in sequence using operator & or in_sequence:
double x = 0, y = 0, z = 0; auto cli1 = ( option("-pos") & value("X",x) & value("Y",y) & value("Z",z) ); auto cli2 = in_sequence( option("-pos") , value("X",x) , value("Y",y) , value("Z",z) );
Note that surrounding groups are not affected by this, so that -a and -b can be matched in any order while -b and the value X must match in sequence:
bool a = false, b = false; int x = 0; auto cli = ( option("-a").set(a), option("-b").set(b) & value("X",x) );
groups can be nested and combined to form arbitrarily complex interfaces (see here and here):
auto cli = ( command("push") | ( command("pull"), option("-f", "--force") ) );
groups can be repeatable as well:
auto cli1 = repeatable( command("flip") | command("flop") );
force common prefixes on a group of flags:
int x = 0; auto cli1 = with_prefix("-", option("a"), option("b") & value("x",x), ... ); // => -a -b ^unaffected^ auto cli2 = with_prefix_short_long("-", "--", option("a", "all"), option("b"), ... ); // => -a --all -b
force common suffixes on a group of flags:
int x = 0; auto cli1 = with_suffix("=", option("a") & value("x",x), ... ); // => a= ^unaffected^ auto cli2 = with_suffix_short_long(":", ":=", option("a", "all"), option("b"), ... ); // => a: all:= b:
make a group of flags joinable:
auto cli1 = joinable( option("-a"), option("-b")); //will match "-a", "-b", "-ab", "-ba" //works also with arbitrary common prefixes: auto cli2 = joinable( option("--xA0"), option("--xB1")); //will also match "--xA0B1" or "--xB1A0"
The easiest way to connect the command line interface to the rest of your code is to bind object values or function (object) calls to parameters (see also here):
bool b = false; int i = 5; int m = 0; string x; ifstream fs; auto cli = ( option("-b").set(b), // "-b" detected -> set b to true option("-m").set(m,2), // "-m" detected -> set m to 2 option("-x") & value("X", x), // set x's value from arg string option("-i") & opt_value("i", i), // set i's value from arg string option("-v").call( []{ cout << "v"; } ), // call function (object) / lambda option("-v")( []{ cout << "v"; } ), // same as previous line option("-f") & value("file").call([&](string f){ fs.open(f); }) );
In production code one would probably use a settings class:
struct settings { bool x = false; /* ... */ }; settings cmdline_settings(int argc, char* argv[]) { settings s; auto cli = ( option("-x").set(s.x), /* ... */ ); parse(argc, argv, cli); return s; }
Note that the target must either be:
int, long int, float, double, ...)const char*const char*Docstrings for groups and for parameters can either be set with the member function doc or with operator %:
auto cli = ( ( option("x").set(x).doc("sets X"), option("y").set(y) % "sets Y" ), "documented group 1:" % ( option("-g").set(g).doc("activates G"), option("-h").set(h) % "activates H" ), ( option("-i").set(i) % "activates I", option("-j").set(j) % "activates J" ).doc("documented group 2:") );
Usage Lines:
cout << usage_lines(cli, "progname") << '\n'; //with formatting options auto fmt = doc_formatting{} .first_column(3) .last_column(79); cout << usage_lines(cli, "progname", fmt) << '\n';
Detailed Documentation:
cout << documentation(cli) << '\n'; //with formatting options auto fmt = doc_formatting{} .first_column(7) .doc_column(15) .last_column(99); cout << documentation(cli, fmt) << '\n';
Man Pages:
auto cli = ( /*CODE DEFINING COMMAND LINE INTERFACE GOES HERE*/ ); cout << make_man_page(cli, "progname") << '\n'; //with formatting options auto fmt = doc_formatting{} .first_column(7) .doc_column(15) .last_column(99); cout << make_man_page(cli, "progname", fmt) << '\n';
Each parameter can have event handler functions attached to it. These are invoked once for each argument that is mapped to the parameter (or once per missing event):
string file = "default.txt"; auto param = required("-nof").set(file,"") | required("-f") & value("file", file) // on 2nd, 3rd, 4th,... match (would be an error in this case) .if_repeated( [] { /* ... */ } ) // if required value-param was missing .if_missing( [] { /* ... */ } ) // if unreachable, e.g. no flag "-f" before filename .if_blocked( [] { /* ... */ } ) // if match is in conflict with other alternative "-nof" .if_conflicted( [] { /* ... */ } );
The handler functions can also take an int, which is set to the argument index at which the event occurred first:
string file = "default.txt"; auto param = required("-nof").set(file,"") | required("-f") & value("file", file) .if_repeated ( [] (int argIdx) { /* ... */ } ) .if_missing ( [] (int argIdx) { /* ... */ } ) .if_blocked ( [] (int argIdx) { /* ... */ } ) .if_conflicted( [] (int argIdx) { /* ... */ } );
If we give -f -b or -b -f -a as command line arguments for the following CLI, an error will be reported, since the value after -f is not optional:
auto cli = ( option("-a"), option("-f") & value("filename"), option("-b") );
This behavior is fine for most use cases.
But what if we want our program to take any string as a filename, because our filenames might also collide with flag names? We can make the value parameter greedy with operator !. This way, the next string after -f will always be matched with highest priority as soon as -f was given:
auto cli = ( option("-a"), option("-f") & !value("filename"), option("-b") ); // ^~~~~~
Be very careful with greedy parameters!
auto cli = ( /* your interface here */ ); auto res = parse(argc, argv, cli); if(res.any_error()) { /* ... */ } //aggregated errors if(res.unmapped_args_count()) { /* ... */ } if(res.any_bad_repeat()) { /* ... */ } if(res.any_blocked()) { /* ... */ } if(res.any_conflict()) { /* ... */ } for(const auto& m : res.missing()) { cout << "missing " << m.param() << " after index " << m.after_index() << '\n'; } //per-argument mapping for(const auto& m : res) { cout << m.index() << ": " << m.arg() << " -> " << m.param(); cout << " repeat #" << m.repeat(); if(m.blocked()) cout << " blocked"; if(m.conflict()) cout << " conflict"; cout << '\n'; }
Sometimes it can make your CLI code more expressive and increase maintainability, if you create your own factory functions for making parameters:
//value that can only connect to one object with automatic default value documentation template<class Target> clipp::parameter documented_value(const std::string& name, Target& tgt, const std::string& docstr) { using std::to_string; return clipp::value(name,tgt).doc(docstr + "(default: " + to_string(tgt) + ")"); }
//value that only matches strings without prefix '-' template<class Target, class... Targets> clipp::parameter nodash_value(std::string label, Target&& tgt, Targets&&... tgts) { return clipp::value(clipp::match::prefix_not{"-"}, std::move(label), std::forward<Target>(tgt), std::forward<Targets>(tgts)...); }
Note that namespace qualifiers are omitted from all examples for better readability. The repository folder "examples" contains code for most of the following examples.
SYNOPSIS
switch [-a] [-b] [-c] [--hi]
OPTIONS
-a activates a
-b activates b
-c, --noc deactivates c
--hi says hibool a = false, b = false, c = true; //target variables auto cli = ( option("-a").set(a) % "activates a", option("-b").set(b) % "activates b", option("-c", "--noc").set(c,false) % "deactivates c", option("--hi")([]{cout << "hi!\n";}) % "says hi"); if(parse(argc, argv, cli)) cout << "a=" << a << "\nb=" << b << "\nc=" << c << '\n'; else cout << make_man_page(cli, "switch");
This will set a to true, if -a is found, b to true, if -b is found, c to false, if -c or --noc are found and prints "hi" if --hi is found in argv. In case of parsing errors a man page will be printed.
If you like it more verbose use set to set variables, call to call functions and doc for docstrings. The sequence of member function calls doesn't matter.
auto cli = ( option("-b").set(b).doc("activates b"), option("-c", "--noc").set(c,false).doc("deactivates c"), option("--hi").call([]{cout << "hi!\n";}).doc("says hi") );
auto cli = ( option("-b") % "activates b" >> b, option("-c", "--noc") % "deactivates c" >> set(c,false), option("--hi") % "says hi" >> []{cout << "hi!\n";} );
auto cli = ( option("-b") % "activates b" >> b, option("-c", "--noc") % "deactivates c" >> set(c,false), option("--hi") % "says hi" >> []{cout << "hi!\n";} );
auto cli = ( b << option("-b") % "activates b", set(c,false) << option("-c", "--noc") % "deactivates c", []{cout << "hi!\n";} << option("--hi") % "says hi" );
auto cli = ( "activates b" % option("-b") >> b, "deactivates c" % option("-c", "--noc") >> set(c,false), "says hi" % option("--hi") >> []{cout << "hi!\n";} );
Note that % has a higher precedence than << and >> which means that you either have to keep the docstrings closer to the command line parameters than the actions or use parentheses.
You should also have a look at actions for more details.
int n = 1; auto optN = parameter{"-n", "-N", "--iterations", "--repeats"}.required(true); auto valN = parameter{match::any} .label("times") .set(n) .call([](string s) { if(!str::represents_number(s)) throw runtime_error{"invalid value for 'times'"}; }) .if_missing([]{ cout << "value 'times' not found!\n"; }) .doc("number of iterations (default = " + std::to_string(n) + ")"); auto cli = group{}; cli.push_back(std::move(optN)); cli.push_back(std::move(valN)); // or: auto cli = group{std::move(optN), std::move(valN)};
There are no limitations regarding formatting and you can have an arbitrary number of flags per command line parameter.
bool onetwo = false; auto myopt = option("-1", "-2", "1", "2", ":1", ":2", "--no1", "--no2").set(onetwo); // ^----- will match any one of these strings ------^
bool a = false, b = false; auto cli = with_prefix("-", option("a").set(a), // -a option("b").set(b) // -b );
Single-letter flags will get the first prefix, flags with more than one letter will get the second one.
bool a = false, b = false; auto cli = with_prefixes_short_long("-", "--", option("a", "all").set(a), // -a, --all option("b", "bottom").set(b) // -b, --bottom );
bool a = false, b = false; auto cli = with_suffix(":", option("a").set(a), // a: option("b").set(b) // b: );
Single-letter flags will get the first suffix (empty in this example), flags with more than one letter will get the second one.
int a = 0, b = 0; auto cli = with_suffixes_short_long("", "=", option("a", "all") & value("A", a), // -a, --all= option("b", "bottom") & value("B", b) // -b, --bottom= );
auto cli = ( /* your command line interface here */ ); assert(cli.flags_are_prefix_free());
Note that identical flags will not trigger an error.
Groups can be nested (see here) and have their own documentation string.
The statement auto cli = ( ... ); creates a group, if there are more than two parameters/groups declared inside the parentheses.
SYNOPSIS
myprogram [x] [y] [a] [b] [-c] [-d] [-e] [-f]
OPTIONS
x sets X
y sets Y
documented group 1:
a activates A
b activates B
documented group 2:
-c activates C
-d activates D
-e, -f activates E or Fbool x = false, y = false, a = false, b = false; bool g = false, h = false, e = false, f = false; auto cli = ( ( option("x").set(x) % "sets X", //simple group option("y").set(y) % "sets Y" ), ( option("a").set(a) % "activates A", option("b").set(b) % "activates B" ) % "documented group 1:" //docstring after group , "documented group 2:" % ( //docstring before group option("-g").set(g) % "activates G", option("-h").set(h) % "activates H" ), "activates E or F" % ( option("-e").set(e), //no docstrings inside group option("-f").set(f) ) ); cout << make_man_page(cli, "myprogram");
The above example is in fact shorthand for this:
group cli{
group{
parameter{"x"}.set(x).doc("sets X"),
parameter{"y"}.set(y).doc("sets Y")
},
group{
parameter{"a"}.set(a).doc("activates A"),
parameter{"b"}.set(b).doc("activates B")
}.doc("documented group 1:")
,
group{
parameter{"-g"}.set(g).doc("activates G"),
parameter{"-h"}.set(h).doc("activates H")
}.doc("documented group 2:")
,
group{
parameter{"-e"}.set(e),
parameter{"-f"}.set(f)
}.doc("activates E or F")
};
cout << make_man_page(cli, "myprogram");You can of course also fill groups one-by-one:
group cli; cli.push_back(option("x").sets(x).doc("sets X")); //...
SYNOPSIS
myprogram <infile> <outfile> [-s]
OPTIONS
infile input filename
outfile output filename
-s, --split split filesstring ifile, ofile; bool split = false; auto cli = ( value("infile", ifile) % "input filename", value("outfile", ofile) % "output filename", option("-s", "--split").set(split) % "split files" );
auto cli = ( value("infile") % "input filename" >> ifile, value("outfile") % "output filename" >> ofile, option("-s", "--split") % "split files" >> split );
See here for more on possible mapping styles.
Parameters can be sequenced using operator & or the function in_sequence. Sequenced parameters can only be matched one after the other. This mechanism can be used to attach a value parameter to an option.
SYNOPSIS
simplify [-n <count>] [-r <ratio>] [-m [<lines=5>]]
OPTIONS
-n, --count <count> number of iterations
-r, --ratio <ratio> compression ratio
-m <lines=5> merge lines (default: 5)int n = 0; bool domerge = false; long m = 5; auto print_ratio = [](const char* r) { cout << "using ratio of " << r << '\n'; }; auto cli = ( (option("-n", "--count") & value("count", n)) % "number of iterations", (option("-r", "--ratio") & value("ratio", print_ratio)) % "compression ratio", (option("-m").set(domerge) & opt_value("lines=5", m)) % "merge lines (default: 5)" );
auto cli = ( (option("-n", "--count") & value("count") >> n ) % "number of iterations", (option("-r", "--ratio") & value("ratio") >> print_ratio ) % "compression ratio", (option("-m" ) & opt_value("lines=5") >> m >> domerge) % "merge lines (default: 5)" );
auto cli = ( (option("-n", "--count") & value("count").set(n)) % "number of iterations", (option("-r", "--ratio") & value("ratio")(print_ratio)) % "compression ratio", (option("-m").set(domerge) & opt_value("lines=5").set(m)) % "merge lines (default: 5)" );
See here for more on coding styles.
Parameters can be sequenced using operator & or the function in_sequence. Sequenced parameters can only be matched one after the other. This mechanism can be used to attach multiple values to an option.
SYNOPSIS
transform <geometry file> [-translate <x> <y> <z>] [-rotate <azimuth> <polar>]string infile; bool tr = false, rot = false; double x = 0, y = 0, z = 0; double phi = 0, theta = 0; auto cli = ( value("geometry file", infile), option("-translate").set(tr) & value("x", x) & value("y", y) & value("z", z), option("-rotate").set(rot) & value("azimuth", phi) & value("polar", theta) );
Note that the following interface definition is equivalent to the above. Since value is positional we can list it with ,, but we have to make sure that
| |
| |
| |
| |
| |
| |