Git Product home page Git Product logo

clipp's Introduction

clipp - command line interfaces for modern C++

Linux build status MSVC build status

Easy to use, powerful and expressive command line argument handling for C++11/14/17 contained in a single header file.

Quick Intro

Simple Use Case — Simple Setup!

Consider this command line interface:

SYNOPSIS
    convert <input file> [-r] [-o <output format>] [-utf16]

OPTIONS
    -r, --recursive  convert files recursively
    -utf16           use UTF-16 encoding

Here 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]);
    // ...
}

A More Complex Example:

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 version

This 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';
}

Quick Reference

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))

Overview

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.

Basic Setup

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);
  • a required parameter has to match at least one command line argument
  • a repeatable parameter can match any number of arguments
  • non-positional (=non-blocking) parameters can match arguments in any order
  • a positional (blocking) parameter defines a "stop point", i.e., until it matches all parameters following it are not allowed to match; once it matched, all parameters preceding it (wihtin the current group) will become unreachable

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.

Value Parameters With Custom Filters
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

Groups

  • 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"

Interfacing With Your Code

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:

  • a fundamental type (int, long int, float, double, ...)
  • a type that is convertible from const char*
  • a callable entity: function, function object / lambda that either has an empty parameter list or exactly one parameter that is convertible from const char*

Generating Documentation (see also here)

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'; 

(Error) Event Handlers (see here, and here)

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) { /* ... */ } );

Special Cases

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!

Parsing Result Analysis

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';
}

Writing Your Own Convenience Factories

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)...);
}

Examples

Note that namespace qualifiers are omitted from all examples for better readability. The repository folder "examples" contains code for most of the following examples.

Options

SYNOPSIS
       switch [-a] [-b] [-c] [--hi]

OPTIONS
       -a          activates a
       -b          activates b
       -c, --noc   deactivates c
       --hi        says hi
bool 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.

Coding Styles

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") );

You can also use operator >> and operator << to define actions

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.

Step-by-step configuration of parameters:

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)};

Flag Strings

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 ------^

Same prefix for all flags

bool a = false, b = false;

auto cli = with_prefix("-", 
    option("a").set(a),     // -a
    option("b").set(b)      // -b
);

Same prefix for all flags: single vs. multiple character(s)

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
);

Same suffix for all flags

bool a = false, b = false;

auto cli = with_suffix(":", 
    option("a").set(a),     // a:
    option("b").set(b)      // b:
);

Same suffix for all flags: single vs. multiple character(s)

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=
);

Make Sure No Flag Occurs As Prefix Of Another Flag

auto cli = ( /* your command line interface here */ );
assert(cli.flags_are_prefix_free());

Note that identical flags will not trigger an error.

Grouping

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 F
bool 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"));
//...

Required Positional Values

SYNOPSIS
        myprogram <infile> <outfile> [-s]
   
OPTIONS
        infile        input filename
        outfile       output filename
        -s, --split   split files
string ifile, ofile;
bool split = false; 
auto cli = (
    value("infile", ifile)             % "input filename",
    value("outfile", ofile)            % "output filename",
    option("-s", "--split").set(split) % "split files" );

Alternative Value Mapping Style

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.

Options With Values

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)"
);

Alternative Value Mapping Styles

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.

Options With Multiple Values

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 the groups of values will only be matched after the options, hence the &.

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) )
);

Required Flags

Required flags are usually used together with non-optional values. Note that -i and -o are not positional in the following example, i.e., the relative order in which command line arguments for -i, -o and -r are provided is irrelevant.

SYNOPSIS
        myprogram [-r] -i <input dir> -o <output dir>

OPTIONS
        -r, --recursive
                search in subdirectories
        -i, --in <input dir>
                path to input directory
        -o, --out <output dir>
                path to output directory
bool recurse = false;
string inpath, outpath;

auto cli = (
    option("-r", "--recursive").set(recurse)                 % "search in subdirectories",
    (required("-i", "--in" ) & value("input dir", inpath))   % "path to input directory",
    (required("-o", "--out") & value("output dir", outpath)) % "path to output directory"
);

Repeatable Parameters

SYNOPSIS
        simplify <file>... [-c] [-i <line>...]
   
OPTIONS
        <file>...               input files
        -c, --compress          compress results             
        -i, --ignore <line>...  lines to be ignored
vector<string> files;
vector<int> lines;
bool zip = false;
auto cli = (
    values("file", files)                                % "input files",
    option("-c", "--compress").set(zip)                  % "compress results",
    (option("-i", "--ignore") & integers("line", lines)) % "lines to be ignored"
);

The call values("v") is shorthand for value("v").repeatable(true).

Note, that the value parameter line is repeatable, but the flag --ignore is not. So something like

    simplify file1 file2 --ignore 1 2 --ignore 3 4 -c

is taken to be an error. However, it is possible if you make the group of --ignore and line itself repeatable:

Repeatable Groups of Options with Repeatable Values

SYNOPSIS
        simplify <file>... [-c] [-i <line>...]...
   
OPTIONS
        <file>...               input files
        -c, --compress          compress results             
        -i, --ignore <line>...  lines to be ignored
vector<string> files;
vector<int> lines;
bool zip = false;
auto cli = (
    values("file", files)                            % "input files",
    option("-c", "--compress").set(zip)              % "compress results",
    repeatable(  // <-----
        option("-i", "--ignore") & integers("line", lines)
    )                                                % "lines to be ignored"
);

Now both the option --ignore and the value parameter value are repeatable. In all of the following examples lines will be set to {1,2,3,4} and c will be set to true:

    simplify file1 file2 -c --ignore 1 2 3 4 
    simplify file1 file2 --ignore 1 2 3 4 -c
    simplify file1 file2 --ignore 1 -c --ignore 2 3 4
    simplify file1 file2 --ignore 1 2 --ignore 3 4 -c
    simplify file1 file2 --ignore 1 --ignore 2 -c --ignore 3 --ignore 4
    simplify file1 file2 -c --ignore1 --ignore2 --ignore3 --ignore4

Actions

Actions are executed if a parameter matched an argument string in the command line arguments list. Actions are defined using the following member functions or operators:

  • parameter::call(f) or parameter::operator () (f): call a callable entity f (function, lambda, custom function object) for each one of the matched argument strings. If f accepts exactly one parameter that is convertible from const char*, the command line argument is passed to it. If the parameter list is empty, it is simply called without argument.

  • parameter::set(target, value): assign a fixed value to a target object; note that the assignment target = value; must be a valid statement

  • parameter::set(target):

    • A bool target is set to true if the flag/value is present and left unchanged otherwise.
    • A target object of fundamental type T (int, long, float, double, ...) will be assigned the result of converting the argument string to type T.
    • Targets of type std::vector<T> are appended with a value for each matched argument string. Note that T must either be (explicitly) convertible from const char* or a fundamental type.
  • operator << or operator >> assign arg strings to lvalues or call callable entities. Which kind of action will be performed is automatically determined through overload resolution.

Predefined Functions

int x = 0;                   // equivalent to:     
option("-x")(set(x))         // option("-x").set(x) 
option("-x")(set(x,2))       // option("-x").set(x,2) 
option("-x")(increment(x))   // option("-x")([&]{++x;})
option("-x")(decrement(x))   // option("-x")([&]{--x;})

bool b = false;              // equivalent to:
option("-b")(flip(b))        // option("-x")([&]{b = !b;})

Some Examples

bool a = false, b = false;
int i = 1, n = 0, m = 0;
float x = 0.0f;

auto cli = (                           //INFORMAL description
    option("-a").set(a),               //if(found("-a")) a = true;
    option("-b") >> b,                 //if(found("-b")) b = true;
    option("--toggle").call(flip(b)),  //if(found("--toggle")) flip(b);

    value("n").set(n),                 //n = std::atoi(arg);
    option("-i") & value("#",i),       //if(found("-i arg")) i = std::atoi(arg);
    option("-1").set(m,1),             //if(found("-1")) m = 1;
    option("-2").set(m,2),             //if(found("-2")) m = 2;

    //if(found("-z")) call_lambda_with_arg("-z");
    option("-z").call([](const char* s) { cout << s; }),

    //using 'operator()' instead of 'call'
    //if(found("bob")) call_lambda_with_arg("bob");
    option("bob")([](const std::string& s) { cout << s; }),

    //for_each_occurence("-x arg", call_lambda_with_arg(arg));
    repeatable( option("-x") & value("X")([&](const char* s) { x = std::atof(s); }) ),

    option("--all") >> []()              { cout << "found --all\n"; }
                    >> [](const char* s) { cout << "found flag " << s << '\n'; };
);

Joinable Flags

SYNOPSIS
        edit <file> [-rbs] ([:vim] [:st3] [:atom] [:emacs])

OPTIONS
        -r      open read-only
        -b      use backup file
        -s      use swap file

        :vim, :st3, :atom, :emacs
                editor(s) to use; multiple possible        
std::string file;
bool readonly = false, usebackup = false, useswap = false;
enum class editor {vim, sublime3, atom, emacs};
std::vector<editor> editors;
auto add = [&](editor e){ return [&]{ editors.push_back(e); }; };

auto cli = (
    value("file", file),
    joinable(
        option("-r").set(readonly)  % "open read-only",
        option("-b").set(usebackup) % "use backup file",
        option("-s").set(useswap)   % "use swap file"
    ),
    joinable(
        option(":vim")    >> add(editor::vim),
        option(":st3")    >> add(editor::sublime3),
        option(":atom")   >> add(editor::atom),
        option(":emacs")  >> add(editor::emacs)
    ) % "editor(s) to use; multiple possible"
);
  • Flags can be joined regardless of their length (second group in the example).
  • If the flags have a common prefix (- or : in the example) it must be given at least once as leading prefix in the command line argument.
  • Allowed args for the first group are: -r, -b, -s, -rb, -br, -rs, -sr, -sb, -bs, -rbs, -rsb, ...
  • Allowed args for the second group are: :vim, :vim:atom, :emacs:st3, :vimatom, ...

More Examples

joinable flags valid args
a, b ab, ba, a, b
-a, -b -ab, -ba, -a, -b, -a-b, -b-a
--a, --b --ab, --ba, --a, --b, --a--b, --b--a
,a, ,b ,ab, ,ba, ,a, ,b, ,a,b, ,b,a
Xab, Xcd Xabcd, Xcdab, XabXcd, XcdXab, Xab, Xcd
x:ab, x:cd x:abcd, x:cdab, x:abx:cd, x:cdx:ab, x:ab, x:cd

Alternatives

SYNOPSIS
        find <file>... -s <expr> [any|all]

OPTIONS
        <file>...  input filenames
        -s <expr>  string to look for
        any        report as soon as any matches
        all        report only if all match
vector<string> files;
string expr;
bool ifany = false, ifall = false;

auto cli = (
    values("file", files)                  % "input filenames",
    (required("-s") & value("expr", expr)) % "string to look for",
    option("any").set(ifany)               % "report as soon as any matches" |
    option("all").set(ifall)               % "report only if all match"
);

If you like it more verbose you can use the function one_of instead of operator |:

auto cli = (
    values("file", files)                  % "input filenames",
    (required("-s") & value("expr", expr)) % "string to look for",
    one_of( option("any").set(ifany)       % "report as soon as any matches",
            option("all").set(ifall)       % "report only if all match" )
);

gcc-style switches

SYNOPSIS
        format [-o <output file>] [-falign|-fnoalign)]

OPTIONS
        -o, --out <file>
                output filename

        -falign, -fnoalign 
                control alignment
string outfile = "a.out";
bool align = false;
     
auto cli = (
    (option("-o", "--out") & value("output file", outfile)) % "output filename",
    ( option("-falign"  ).set(align,true) |
      option("-fnoalign").set(align,false) )                % "control alignment"
);

Note, that the documentation string is attached to the group of parameters for better readability.

non-redundant prefix specification

//has the same meaning as the code above
string outfile = "a.out";
bool align = false;

auto cli = (
    (option("-o", "--out") & value("output file", outfile)) % "output filename",
    with_prefix("-f", option("align"  ).set(align,true) |
                      option("noalign").set(align,false) )  % "control alignment"
);

merge alternatives with common prefixes in documentation

Usage:   format [-o <output file>] [-f(align|noalign)]
auto fmt = doc_formatting{}.merge_alternative_flags_with_common_prefix(true);
cout << usage_lines(cli, "format", fmt) << '\n';

Commands

= positional, required flags

SYNOPSIS
        make_doc new <filename> [-e <enc>]

OPTIONS
        -e, --encoding  'utf8' or 'cp1252', default is UTF-8
    
std::string fname;
std::string enc = "utf8";

auto cli = (
    command("new"),
    value("filename", fname),
    option("-e", "--encoding") & value("enc", enc).doc("'utf8' or 'cp1252', default is " + enc)
);

Nested Alternatives

SYNOPSIS
        image-find help
        image-find build (new|add) <file>... [-v] [-b [<size=1024>]] [--init|--no-init]
        image-find query <infile> -o <outfile> [-f <format>]

OPTIONS
        -v, --verbose
                print detailed report

        -b, --buffer [<size=1024>]
                sets buffer size in KiByte

        --init, --no-init 
                do or don't initialize

        -f, --out-format <format>
                determine output format

Value handling actions are omitted; see examples/nested_alternatives.cpp for a fully functional demo.

auto cli = (
    command("help") 
    | ( command("build"),
        ( command("new") | command("add")),                  
        values("file"),
        option("-v", "--verbose")                           % "print detailed report",
        (option("-b", "--buffer") & opt_value("size=1024")) % "sets buffer size in KiByte",
        ( option("--init") | option("--no-init") )          % "do or don't initialize"
    ) 
    | ( command("query"),
        value("infile"),
        required("-o", "--out") & value("outfile"),
        (option("-f", "--out-format") & value("format"))  % "determine output format"
    )
);

Note:

doc_formatting::split_alternatives(bool)             //default: true
doc_formatting::alternatives_min_split_size(int)     //default: 3

control if the usage is split up into several lines if any group inside an alternative exceeds a given minimum size.

Complex Nestings

The combination of blocking parameters, alternatives and grouping makes it possible to define interfaces with decision trees/DAGs of arbitrary complexity.

SYNOPSIS
    complex_nesting [-v] [-i] (copy|move) [--all] [--replace] [-f] <files>... [-r] [-h]
    complex_nesting [-v] [-i] compare (date|content) [-b] [-q] <files>... [-r] [-h]
    complex_nesting [-v] [-i] merge (diff|patch) -o <outdir> [--show-conflicts] <files>... [-r] [-h]
    complex_nesting [-v] [-i] merge content [--git-style] [-m <marker>] -o <outdir> [--show-conflicts] <files>... [-r] [-h]
    complex_nesting [-v] [-i] list <files>... [-r] [-h]

OPTIONS
    user interface options:
        -v, --verbose      show detailed output
        -i, --interactive  use interactive mode

    copy mode:
        --all              copy all
        --replace          replace existing files
        -f, --force        don't ask for confirmation

    compare mode:
        -b, --binary       compare files byte by byte
        -q, --quick        use heuristics for faster comparison

    merge mode:
        diff               merge using diff
        patch              merge using patch
        content            merge based on content

        content based merge options:
            --git-style    emulate git's merge behavior
            <marker>       merge marker symbol

        <outdir>           target directory for merge result
        --show-conflicts   show merge conflicts during run

    mode-independent options:
        <files>...         input files
        -r, --recursive    descend into subdirectories
        -h, --help         show help

Actions and target variables are omitted in the code.

auto copyMode = "copy mode:" % (
    command("copy") | command("move"),
    option("--all")         % "copy all",
    option("--replace")     % "replace existing files",
    option("-f", "--force") % "don't ask for confirmation"
);

auto compareMode = "compare mode:" % (
    command("compare"),
    (command("date") | command("content")),
    option("-b", "--binary") % "compare files byte by byte",
    option("-q", "--quick")  % "use heuristics for faster comparison"
);

auto mergeAlgo = (
    command("diff")  % "merge using diff"  |
    command("patch") % "merge using patch" |
    (   command("content") % "merge based on content",
        "content based merge options:" % (
          option("--git-style") % "emulate git's merge behavior",
          option("-m", "--marker") & value("marker") % "merge marker symbol"
        )
    )
);

auto mergeMode = "merge mode:" % (
    command("merge"),
    mergeAlgo,
    required("-o") & value("outdir") % "target directory for merge result",
    option("--show-conflicts")       % "show merge conflicts during run"
);

auto firstOpt = "user interface options:" % (
    option("-v", "--verbose")     % "show detailed output",
    option("-i", "--interactive") % "use interactive mode"
);
auto lastOpt = "mode-independent options:" % (
    values("files")             % "input files",
    option("-r", "--recursive") % "descend into subdirectories",
    option("-h", "--help")      % "show help"
);

auto cli = (
    firstOpt,
    copyMode | compareMode | mergeMode | command("list"),
    lastOpt
);

if(parse(argc, argv, cli)) {
     // program logic...
} else {
    auto fmt = doc_formatting{}.doc_column(31);
    cout << make_man_page(cli, argv[0], fmt) << '\n';
}

You could of course write down everything as one big expression (docstrings are omitted)...:

auto cli = (
    option("-v", "--verbose"),
    option("-i", "--interactive"),
    (
        (   (command("copy") | command("move")),
            option("--all"), option("--replace"),
            option("-f", "--force") 
        )
        | ( command("compare"),
            (command("date") | command("content")),
            option("-b", "--binary"), option("-q", "--quick") 
        ) 
        | ( command("merge"),
            (
                ( command("content"),
                  option("--git-style"),
                  option("-m", "--marker") & value("marker")
                )
                | command("diff")
                | command("patch")
            ),
            required("-o") & value("outdir"),
            option("--show-conflicts")
        ) 
        | command("list")
    ),
    values("files"),
    option("-r", "--recursive"),
    option("-h", "--help")
);

...but it is probably more readable and maintainable if you break up the CLI definition into logical parts.

Note:

doc_formatting::split_alternatives(bool)             //default: true
doc_formatting::alternatives_min_split_size(int)     //default: 3

control whether the usage is split up into several lines if any group inside an alternative exceeds a given minimum size.

An Example From docopt

Naval Fate.

Usage:
  naval_fate ship new <name>...
  naval_fate ship <name> move <x> <y> [--speed= <kn>]
  naval_fate ship shoot <x> <y>
  naval_fate mine (set|remove) <x> <y> [--moored|--drifting]
  naval_fate -h | --help
  naval_fate --version

Options:
  --speed= <kn>  Speed in knots [default: 10].
  --moored       Moored (anchored) mine.
  --drifting     Drifting mine.
  -h, --help     Show this screen.
  --version      Show version.

This code defines the command line interface, handles the parsing result and generates the above man page snippet.

int x = 0, y = 0;
float speed = 0.0f;
bool drift = true;
vector<string> names;
enum class mode { none, help, shipnew, shipmove, shipshoot, mineset, minerem};
mode selected = mode::none;

//define command line interface
auto coordinates = ( value("x", x), value("y", y) );

auto shipnew  = ( command("new").set(selected,mode::shipnew),
                  values("name", names) );

auto shipmove = (
    value("name", names),
    command("move").set(selected,mode::shipmove), coordinates,
    option("--speed=") & value("kn",speed) % "Speed in knots [default: 10]");

auto shipshoot = ( command("shoot").set(selected,mode::shipshoot),
                   coordinates );

auto mines = ( 
    command("mine"),
    (command("set"   ).set(selected,mode::mineset) | 
     command("remove").set(selected,mode::minerem) ),
    coordinates,
    (option("--moored"  ).set(drift,false) % "Moored (anchored) mine." | 
     option("--drifting").set(drift,true)  % "Drifting mine."          )
);

auto navalcli = (
    ( command("ship"), ( shipnew | shipmove | shipshoot ) )
    | mines,
    | command("-h", "--help").set(selected,mode::help)     % "Show this screen."
    | command("--version")([]{ cout << "version 1.0\n"; }) % "Show version."
);

parse(argc, argv, navalcli);

//handle results
switch(m) {
    case mode::none: 
        break;
    case mode::help: {
        auto fmt = doc_formatting{}
            .first_column(2).doc_column(16)
            .max_flags_per_param_in_usage(4);

        cout << "Naval Fate.\n\nUsage:\n"
             << usage_lines(navalcli, "naval_fate", fmt)
             << "\n\nOptions:\n"
             << documentation(navalcli, fmt) << '\n';
        }
        break;
    }
    //... 
}

Value Filters

If a parameter doesn't have flags, i.e. it is a value-parameter, a filter function will be used to test if it matches an argument string. The default filter is clipp::match::nonempty which will match any non-empty argument string. If you want more control over what is matched, you can use some other predefined filters or you can write your own ones (see here).

Usage:   exec [-n <times>] [-l <line>...] [-b <ratio>] [-f <term>]
int n = 1;
std::vector<int> lines;
double r = 1.0;
string term, name;
auto cli = (
    option("-n", "--repeat") & integer("times", n),
    option("-l", "--line")   & integers("#", lines),
    option("-r", "--ratio)   & number("ratio", r),
    option("-f", "--find")   & word("term", term)
);

Predefined Filtering Value-Parameters

auto cli = (
    value("x"),    //non-empty string
    word("x"),     //alphanumeric string
    number("x"),   //string representing integer or floating point number
    integer("x")   //string representing integral number
);

Note that there are additional functions for

  • optional parameters: opt_value, opt_word, ...
  • repeatable parameters: values, words, ...
  • repeatable, optional parameters: opt_values, opt_words, ...

Using Filters Explicitly

Two kinds of filters are supported right now that can be passed as first argument of value, values, opt_value or opt_values as well as argument of the constructor parameter::parameter(Filter&&)

  • Predicates (const string&) -> bool which should return true if and only if an argument is an exact match.

  • Substring matchers (const string&) -> subrange which in case of a match also indicate the position and length of the matched substring within a command line argument.

string s;
value( match::length{1,5}, "str", s);

//or using the parameter class directly
auto p = parameter{ match::length{1,5} }
         .positional(true).required(true)
         .label("str").set(s);

There are a couple of predefined filters in namespace clipp::match, but you can of course write your own ones (see here).

Here is another example that makes sure we don't catch any value starting with "-" as a filename:

auto cli = (  
    option("-a")  
    option("-f") & value(match::prefix_not("-"), "filename"),
    option("-b")  
);
namespace clipp {
namespace match {

  //simple predicates
  bool none (const string&);
  bool any (const string&);
  bool nonempty (const string&);
  bool alphabetic (const string&);
  bool alphanumeric (const string&);

  //filters with settings and substring matching
  class numbers {
      explicit numbers(char decimalPoint = '.', char digitSeparator = ',', char exponentSeparator = 'e');
      subrange operator () (const string& arg);
  };

  class integers {
      explicit integers(char digitSeparator = ',');
      subrange operator () (const string& arg);
  };

  class substring {
      explicit substring(const string& str);
      subrange operator () (const string& arg);
  };

  class prefix {
      explicit prefix(const string& prefix);
      subrange operator () (const string& arg);
  };

  class prefix_not {
      explicit prefix_not(const string& prefix);
      subrange operator () (const string& arg);
  };

  class length {
      explicit length(size_t exact);
      explicit length(size_t min, size_t max);
      subrange operator () (const string& arg);
  };

} }

Greedy Parameters

By default, the parser tries to identify a command line argument (in that order) as

  • a single flag
  • a concatenation of multiple, joinable flags (in any order)
  • a concatenation of a joinable flag sequence in the order defined in the CLI code
  • a single value parameter
  • a concatenation of a joinable flag/value sequence in the order defined in the CLI code
  • a concatenation of joinable flags & values in no particular order

If no match was found, the parser tries the same list again without any restrictions imposed by blocking (positional) parameters, conflicting alternatives, etc. If this leads to any match, an error will be reported. This way, potential, but illegal matches can be found and, e.g., conflicting alternatives can be reported.

Consider this CLI:

auto cli = (  option("-a"),  option("-f") & value("filename"),  option("-b")  );

If we give -f -b or -b -f -a as command line arguments, an error will be reported, since the value after -f is not optional.

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 filename value parameter greedy, so that the next string after -f will always be matched with highest priority as soon as -f was given:

auto cli = (  option("-a"),  option("-f") & greedy(value("filename")),  option("-b")  );

or using operator !:

auto cli = (  option("-a"),  option("-f") & !value("filename"),   option("-b")  );

Now, every string coming after an -f will be used as filename.

If we don't want just any kind of match accepted, but still retain a higher priority for a value parameter, we could use a value filter:

auto cli = (  
    (   command("A"),
        option("-f") & !value(match::prefix_not("-"), "filename"),
        option("-b")  
    ) |
    (   command("B"),
        option("-x")  
    )
);

This way, the command line arguments A -f B will set the filename to "B" and produce no conflict error between the alternative commands A and B but A -f -b will still give an error.

Note, that there is an inherent decision problem: either we want the filename value to match no matter what, or we won't get proper error handling if someone forgets to specify a filename and gives A -f -b Also, there might be interfaces where we really want to catch something like A -f B as a command conflict.

Generalized Joinable Parameters

Not only flags, but arbitrary combinations of flags and values can be made joinable. This feature is especially powerful if combined with repeatable groups. Important: in order for an argument to be matched by such expressions, no parameter requirements must be violated during the matching. Also, no partial argument matches are allowed.

Example 1: Counting Letters

Usage:   counter [a|b]...
int as = 0, bs = 0;

auto cli = joinable( repeatable(
        option("a").call([&]{++as;}) |
        option("b").call([&]{++bs;})
    ) );

if(parse(argc, argv, cli))     
    cout << "as: " << as << "\nbs: " << bs << '\n';
else 
    cout << "Usage:\n" << usage_lines(cli, argv[0]) << '\n';

Valid input includes:

$ ./counter a                               
$ ./counter b
$ ./counter ab
$ ./counter abba
$ ./counter a b baba
$ ./counter a babba abab abbbbba b a ba a
    ...

Example 2: Listing Numbers

Usage:   numbers ([,] [<number>])...
std::vector<double> nums;

auto cli = joinable(repeatable( option(",") , opt_number("number", nums) ));

if(parse(argc, argv, cli)) {
    cout << "numbers:\n";
    for(auto n : nums) cout << n << '\n';
} else {
    cout << "Usage:\n" << usage_lines(cli, argv[0]) << '\n';
}

Valid input includes:

$ ./numbers 1
$ ./numbers 1 2 3
$ ./numbers 1 , 2
$ ./numbers 1 , 2 , 3
$ ./numbers 1, 2, 3
$ ./numbers 1 ,2 ,3
$ ./numbers 1,2
$ ./numbers 1,2,3
$ ./numbers 1.1 , 2
$ ./numbers 1,2.3,4.5
$ ./numbers 1,2,3 4.2 5,6 2 7.1,8.23,9 

Warning: Be careful with joinable and repeatable parameters! The resulting command line interface might be a lot less intuitive to use than you think. It can also be hard to get the "grammar" of complex parsing expressions right. The following definition for example, contains a subtle pitfall:

auto cli = joinable(repeatable( option(",") , number("number", nums) ));
//                                            ^^^ non-optional

This will not match arguments like "1,". This is, because, if the repeat group is 'hit' by any of its child parameters, all non-optional parameters must also match within the current 'repeat cycle'. So, if the parser hits the "," it expects to find a number arg as well, because it is blocking (positional) and required. Only after seeing this number can it enter the next repeat cycle. Thus, the argument will not be matched, since joined matches are only valid if no error occured. Making the number optional solves the problem.

Custom Value Filters

Two kinds of filters are supported right now that can be passed as first argument of value, values, opt_value or opt_values as well as argument of the constructor parameter::parameter(Filter&&):

  • Predicates (const string&) -> bool which should return true if and only if an argument is an exact match.

  • Substring matchers (const string&) -> subrange which in case of a match also indicate the position and length of the matched substring within a command line argument.

Simple Predicate Example

Usage:   annotate auto | (label <character>)
auto is_char = [](const string& arg) { return arg.size() == 1 && std::isalpha(arg[0]); };

char lbl = ' ';
auto cli = (  command("auto") | ( command("label"), value(is_char, "character", lbl) )  );

Substring Matcher Example

Let's write a program that takes strings and lists all tag names (<tag>) contained in them:

Usage:   tagnames <string>...
//custom filter
auto tag_name = [] (const string& arg) {
    if(arg.size() < 3) return subrange{}; //too short
    auto i = arg.find("<");
    if(i == string::npos) return subrange{}; //no tag start found
    auto j = arg.find(">", i+1);
    if(j == string::npos) return subrange{}; //didn't find end of tag
    return subrange{i,j-i+1}; //partial match {start, length}
};

std::set<string> tags;
auto cli = joinable(
    values(tag_name, "string", 
           [&](const string& arg){ if(arg[1] != '/') tags.insert(arg);})
);

if(parse(argc, argv, cli)) {
    cout << "tag names:\n";
    for(const auto& t : tags) cout << t << '\n';
} else {
    cout << "Usage:\n" << usage_lines(cli, "tagnames") << '\n';
}
$ ./tagnames "<cee><d><e></e></d></cee>" "<a><bo></bo></a>"
tag names:
<a>
<bo>
<cee>
<d>
<e>

Sanity Checks

Check, if no flag occurs as prefix of any other flag (identical flags will be ignored):

auto cli = ( /* command line interface definition */);

assert( cli.flags_are_prefix_free() );

Check common prefix of all flags, like for example "-" (or "/" on Windows):

auto cli = ( /* command line interface definition */);

assert( cli.common_flag_prefix() == "-" );

Basic Error Handling

Each parameter can have error handler functions/lambdas/function objects for different fail cases attached to it:

  • if_repeated is raised each time an argument is mapped to a parameter regardless of that parameter's repeatability setting
  • if_missing is raised if a required parameter has no argument associated with it
  • if_conflicted is raised if two or more arguments are mapped to more than one parameter of a group of alternatives
  • if_blocked is raised if an argument can only be mapped to a parameter that was not reachable at the time (e.g. because a positional value was expected before that parameter or the parameter was in a non-active alternative branch)

Example:

Usage:   send <file> -t <target>... [--http|--ftp]
string filename;
vector<string> targets;
vector<string> wrong;
bool http = true;

auto istarget = match::prefix_not("-");

auto cli = (
    value("file", filename)
        .if_missing([]{ cout << "You need to provide a source filename!\n"; } )
        .if_repeated([](int idx){ cout << "Only one source file allowed! (index " << idx << ")\n"; } )
    ,
    required("-t") & values(istarget, "target", targets)
        .if_missing([]{ cout << "You need to provide at least one target filename!\n"; } )
        .if_blocked([]{ cout << "Target names must not be given before the source file name!\n"; })
    ,
    option("--http").set(http,true) | 
    option("--ftp").set(http,false) % "protocol, default is http"
        .if_conflicted([]{ cout << "You can only use one protocol at a time!\n"; } )
    ,
    any_other(wrong)
);

if(parse(argc, argv, cli) && wrong.empty()) {
    cout << "OK\n";
    /* ... */
} else {
    for(const auto& arg : wrong) cout << "'" << arg << "' is not a valid argument\n";
    cout << "Usage:" << usage_lines(cli,argv[0]) << '\n';
}

An error handler can either have an empty parameter list or take an int which is set to the command line argument index where the error occured first.

The catch-all parameter made by any_other is used to catch command line arguments that are not supported.

The value parameter target will only match command line arguments that do not begin with "-", so that wrongly spelled options cannot be parsed as target value.

Parsing

auto cli = ( 
    command("make"),
    value("file")           % "name of file to make",
    option("-f", "--force") % "overwrite existing file"
);

//excludes argv[0]
parse(argc, argv, cli);

//if you want to include argv[0]
parse(argv, argv+argc, cli);

parse({"make", "out.txt"}, cli);

auto args = std::vector<std::string> {"make", "out.txt", "-f"};
parse(args, cli);

The parse functions return an object of parsing_result which can be used for detailed analysis and will (explicitly) convert to false if any error occured during parsing.

auto result = parse(argc, argv, cli);

auto doc_label = [](const parameter& p) {
    if(!p.flags().empty()) return p.flags().front();
    if(!p.label().empty()) return p.label();
    return doc_string{"<?>"};
};

cout << "args -> parameter mapping:\n";
for(const auto& m : result) {
    os << "#" << m.index() << " " << m.arg() << " -> ";
    auto p = m.param();
    if(p) {
        os << doc_label(*p) << " \t";
        if(m.repeat() > 0) {
            os << (m.bad_repeat() ? "[bad repeat " : "[repeat ")
               <<  m.repeat() << "]";
        }
        if(m.blocked())  os << " [blocked]";
        if(m.conflict()) os << " [conflict]";
        os << '\n';
    }
    else {
        os << " [unmapped]\n";
    }
}

cout << "missing parameters:\n";
for(const auto& m : result.missing()) {
    auto p = m.param();
    if(p) {
        os << doc_label(*p) << " \t";
        os << " [missing after " << m.after_index() << "]\n";
    }
}

Documentation Generation

Generate usage lines and documentation from parameters:

auto cli = ( /* command line interface definition */ );

//used default formatting
cout << "Usage:\n" << usage_lines(cli, "progname")
     << "\nOptions:\n" << documentation(cli) << '\n';

... or generate an entire man page in one go:

auto cli = ( /* command line interface definition */ );

cout << make_man_page(cli, "progname")
        .prepend_section("DESCRIPTION", "    The best thing since sliced bread.")
        .append_section("LICENSE", "    GPL3");

Example

DESCRIPTION
    Builds a database of words from text files.

SYNOPSIS
    worddb help
    worddb build (new|add) <file>
    worddb query -i <infile> [-p]
    worddb info space
    worddb info statistics (words|chars)
    worddb remove (any|all) <regex>
    worddb modify [-c] [-u] [-m <size>]

OPTIONS
    build commands
        new                  make new database
        add                  append to existing database

    query settings
        <infile>             input file
        -p, --pretty-print   human friendly output

    database info modes
        space                detailed memory occupation analysis

        statistics analysis
            words            word frequency table
            chars            character frequency table

    remove mode
        <regex>              regular expression filter

    modification operations
        -c, --compress       compress database in-memory
        -u, --unique         keep only unique entries
        -m, --memlimit       max. size in RAM

LICENSE
    GPL3

The full code:

auto cli = (
    command("help") | 
    ( command("build"),
        "build commands" %
        (   command("new")  % "make new database"
          | command("add")  % "append to existing database"
        ),
        value("file")
    ) | 
    ( command("query"),
        "query settings" %
        (   required("-i", "--input") & value("infile") % "input file",
            option("-p", "--pretty-print") % "human friendly output")
    ) | 
    ( command("info"),
        "database info modes" % (
            command("space") % "detailed memory occupation analysis" |
            (
                command("statistics"),
                "statistics analysis" % (
                    command("words") % "word frequency table" |
                    command("chars") % "character frequency table"
                )
            )
        )
    ) | 
    "remove mode" % (
        command("remove"),
        "modify" % ( command("any") | command("all") ),
        value("regex") % "regular expression filter"
    ) | 
    ( command("modify"),
        "modification operations" % (
            option("-c", "--compress") % "compress database in-memory",
            option("-u", "--unique")   % "keep only unique entries",
            option("-m", "--memlimit") % "max. size in RAM" & value("size")
        )
    )
);

auto fmt = doc_formatting{} .first_column(4) .doc_column(28) .last_column(80);

cout << make_man_page(cli, "worddb", fmt)
    .prepend_section("DESCRIPTION", "    Builds a database of words from text files.")
    .append_section("LICENSE", "    GPL3") << '\n';

Formatting Options

//all formatting options (with their default values)
auto fmt = doc_formatting{}
    .first_column(8)                           //left border column for text body
    .doc_column(20)                            //column where parameter docstring starts
    .last_column(100)                          //right border column for text body
    .indent_size(4)                            //indent of documentation lines for children of a documented group
    .line_spacing(0)                           //number of empty lines after single documentation lines
    .paragraph_spacing(1)                      //number of empty lines before and after paragraphs
    .flag_separator(", ")                      //between flags of the same parameter
    .param_separator(" ")                      //between parameters 
    .group_separator(" ")                      //between groups (in usage)
    .alternative_param_separator("|")          //between alternative flags 
    .alternative_group_separator(" | ")        //between alternative groups 
    .surround_group("(", ")")                  //surround groups with these 
    .surround_alternatives("(", ")")           //surround group of alternatives with these
    .surround_alternative_flags("", "")        //surround alternative flags with these
    .surround_joinable("(", ")")               //surround group of joinable flags with these
    .surround_optional("[", "]")               //surround optional parameters with these
    .surround_repeat("", "...")                //surround repeatable parameters with these
    .surround_value("<", ">")                  //surround values with these
    .empty_label("")                           //used if parameter has no flags and no label
    .max_flags_per_param_in_usage(1)           //max. # of flags per parameter in usage
    .max_flags_per_param_in_doc(32)            //max. # of flags per parameter in detailed documentation
    .split_alternatives(true)                  //split usage into several lines for large alternatives
    .alternatives_min_split_size(3)            //min. # of parameters for separate usage line
    .merge_alternative_flags_with_common_prefix(false)  //-ab(cdxy|xy) instead of -abcdxy|-abxy
    .merge_joinable_flags_with_common_prefix(true)     //-abc instead of -a -b -c
    .ignore_newline_chars(false)               //ignore '\n' in docstrings
    ;

cout << "Usage:\n" << usage_lines(cli, "progname", fmt)
     << "\nOptions:\n" << documentation(cli, fmt) << '\n';

//or generate entire man page in one go
cout << make_man_page(cli, "progname", fmt)
        .prepend_section("DESCRIPTION", "This program lets you format text.")
        .append_section("LICENSE", "GPLv3");

Documentation Filtering

The documentation generator class documentation can also take an additional third constructor argument that allows to filter parameters according to their properties.

int main(int argc, char* argv[]) {
    auto cli = (
        value("input file"),
        option("-r", "--recursive").set(rec).doc("convert files recursively"),
        option("-o") & value("output format", fmt),
        option("-utf16").set(utf16).doc("use UTF-16 encoding")
    );

    auto fmt = doc_formatting{}.doc_column(28);

    auto filter = param_filter{}.prefix("--");

    cout << "Usage:\n" << usage_lines(cli, argv[0]) << "\n\n"
         << "Parameters:\n" << documentation(cli, fmt, filter) << '\n';}

Which results in:

Usage:
        convert <input file> [-r] [-o <output format>] [-utf16]

Parameters:
        -r, --recursive    convert files recursively

Parameter Filters

Any function/lambda that maps a parameter to a bool can be used as filter predicate. CLIPP also comes with a default parameter filter class:

//all param_filter options (with their default values)
auto filter = param_filter{}
    .prefix("")               //only parameters with given prefix
    .required(tri::either)    //required parameters 
    .blocking(tri::either)    //blocking/positional parameters
    .repeatable(tri::either)  //repeatable parameters
    .has_doc(tri::yes)        //parameters with/without docstrings
    ;

which uses a dedicated tristate type:

namespace clipp {
   enum class tri { no, yes, either };
}

Motivation

Well, I didn't find a library that makes building simple command line interfaces simple, yet can also be used to build complex CLIs. I really don't want to write 20 lines of boilerplate just to have 3 simple command line options. I also don't want to drag along monstrosities like boost or Qt for that. Over time, clipp evolved into a domain specific language (in pure C++) that I hope can at least somewhat approach the readability of docopt but can leverage the benefits (toolability, etc.) of C++'s type system.

Other libraries (Boost, POCO, Qt, adishavit/Argh, Taywee/args ... or 'getopt')

  • often involve a lot of boilerplate (also for very simple use cases)
  • Some libs might be okay in terms of usability, but don't let you build complex interfaces with nested alternatives, mixed commands & options, positional values, more than 2 flags per option, etc.
  • I really like ad-hoc parsers like Argh for their simplicity, but they don't generate usage / man pages and don't allow complex interfaces with error checking.
  • Most libs make it really hard to figure out the resulting command line interface by looking at the code (OK, you be the judge if I did a better job at this...).
  • Some libs come with a ton of dependencies (Qt, Boost). I want a single header file!
  • Some libs require the separation of code related to one command/option. I find this harder to maintain than having everything related to one option in one place.
  • Sometimes flag strings of commands/options have to be repeated several times over.
  • Many libs come with restrictions regarding flag names, formatting, joinability, etc.

What about docopt?

I like the basic idea, but I don't like redundancy in code, especially if it involves repeating string literals. Docopt generates a command line argument parser from a "man page docstring". After parsing, you have to query a dictionary to get values or check for option presence. That means you either have to mention the same flag names twice (in the docstring and in the query) or you have to use string variables which makes the docstring hard to read and kind of defeats the purpose of docopt. Furthermore, the code tends to be littered with string-to-whatever conversions.

I also wanted the ability to keep everything related to one option/command/value together in the code which I found very useful for programs with lots of command line options. Docopt doesn't let you do that since the interface definition and the code performing any actions is always separated (unless you give up on the "beautiful doc page" idea at the heart of docopt and build your input string to the parser generator piece-by-piece).

Design Goals

  • minimize boilerplate
  • simple code for simple use cases
  • good code readability (as far as C++ allows for it)
  • avoid ambiguities
  • eliminate repetitions
  • ability to keep code related to one option/command/value together
  • support many different command line interface conventions
  • support different coding styles, conventions & tastes
  • value semantics wherever possible
  • do not break clipp's general interface and conventions in the future

Requirements

  • requires a mostly C++11 conforming compiler

Compilers, clipp compiles with

  • g++ 5.3.0, g++ 5.4.1, g++ 6, g++ 7
  • clang++ 3.8, clang++ 3.9, clang++ 4.0, clang++ 5.0
  • MSVC 14.11.25503 (compiler 19.11.25547)

Feedback

I would be delighted to hear from anyone who uses clipp in their project(s). If you find bugs or design flaws or want to suggest improvements please open an issue or submit a pull request.

Development

clipp is still very young, so I probably won't add new features any time soon and rather:

  • fix bugs
  • improve test cases and coverage
  • add more code documentation
  • clean up the code / make it more understandable
  • add the ability to use other string classes

clipp's People

Contributors

heavywatal avatar kessido avatar muellan avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

clipp's Issues

how to define single repeatable joinable option

I am trying to figure out, how to implement an option for a "verbosity" level -v. The idea is that it should be repeatable and joinable, i.e. -v should set it to 1, -vvv should set it to 3.

I have found an example for repeatable and joinable options:

auto cli = joinable(
        repeatable(
            option("a")([&]{++as;}) |
            option("b")([&]{++bs;})
        )
    );

which works fine. But when I try to define my option:

auto verb = 0;
bool d = false;
auto cli = (<...>,
        joinable(repeatable(option("-v").call(increment(verb)) % verb_help))
//        joinable(repeatable(option("-v").call(increment(verb)) % verb_help, option("-d").set(d)))
        );

the parsing fails on anything different from -v. What I found that it starts working as expected when I add "dummy" option -d to the joinable and repeatable list.

Parse bug with option and value within command

Using the following:

auto dither =
(
    command("dither").set(run_mode, mode::dither),
    option("--palette") & value("palette", palette),
    option("--hicolor").set(hicolor, true),
    option("--error-diffusion").set(error_diffusion, true),
    value("image").set(in)
);

Help string looks OK:

tool dither [--palette <palette>] [--hicolor] [--error-diffusion] <image>

However nothing gets accepted; I tried all the following:

tool dither
tool dither image.jpeg
tool dither --palette x image.jpeg
tool dither image.jpeg --palette x

What does work:

  • remove the --palette option
  • move the value("image") part before the option parts; but I want to be able to specify options before the final argument in the commandline

Repeatable required option parses incorrectly.

From the README, I expected

exe (-l <lines>)...      repeatable( required("-l") & value("lines", ids) )

to parse exe -l 0 -l 1 -l 2 where ids would contain 0, 1, and 2. However, it tries to parse the additional -l's, and converts them to 0 values (i.e., ids contains 0, 0, 1, 0, 2).

Also, exe -l 0 1 2 is accepted (which I think should be rejected) and ids contains 0, 1, 2.

Keep empty arguments as they are

Empty arguments seem to be dropped in parsing process (in parser::operator(), I guess). Is it possible to change this behavior on the user side and pass an empty argument "" as is? If not, I would suggest it should be changed for the consistency and flexibility.

Code:

int main(int argc, char* argv[]) {
    std::string optional = "default";
    std::string positional;
    auto cli = (
      clipp::option("-o", "--optional")
      & clipp::value(clipp::match::any, "label-o", optional),
      clipp::value("label-p", positional)
    );
    auto parsed = clipp::parse(argc, argv, cli);
    if (!parsed) std::cout << "ERROR!\n";
    clipp::debug::print(std::cout, parsed);
    std::cout << "optional: " << optional << "\n";
    std::cout << "positional: " << positional << "\n";
    return 0;
}

Product

% ./a.out -o "" hello
ERROR!
#1 -o -> -o
#3 hello -> label-o
label-p          [missing after 0]
optional: hello
positional:

I expect optional: (empty) and positional: hello.
Thanks.

option string value with special char

Could you help me to parse an option with a string like this:

command --arg "abc: - (cde, 0)"

is it possible to use delimiter, like ", to specify the start and the end of a string value?

Compiler error when compiling with MSVC & C++20

The std::result_of type trait was removed with C++20 in favor of std::invoke_result. With on of MSVCs latest updates they did this removal (if set to compile as C++20), leading to compile errors of clipp because of it's use in e.g.

clipp/include/clipp.h

Lines 160 to 163 in 2c32b2f

check_is_callable(int) -> decltype(
std::declval<Fn>()(std::declval<Args>()...),
std::integral_constant<bool,
std::is_same<Ret,typename std::result_of<Fn(Args...)>::type>::value>{} );

To reproduce you can run the testsuite with /std:c++latest when using at least MSVC16.7.3 (did not check if the change occured with this or an earlier version)

Infinite loop in possibly wrong clipp descriptor

Hello, when build the following code I'm getting stuck into a infinite loop up until a SIGSEV crash.

#include "clipp.h"
#include <iostream>

enum class Validation
{
	SynthesisVoting,
	Help
};


int main(int argc, char* argv[])
{

	Validation validation_action = Validation::Help;

	std::string source_image_path;
	std::string target_image_path;
	std::string result_path;

	bool source_image_to_be_loaded = false;
	bool target_image_to_be_loaded = false;

	{
		auto cli = (
				(clipp::command("voting")
			 	 .set(validation_action, Validation::SynthesisVoting),
			 	 clipp::value("source image", &source_image_path).set(source_image_to_be_loaded, true),
			 	 clipp::value("target image", &target_image_path).set(target_image_to_be_loaded, true),
			 	 clipp::value("result path",  &result_path)) |
				(clipp::command("help", "-?", "--help", "-h").set(validation_action, Validation::Help))
				);
		auto parse_result = clipp::parse(argc, argv, cli);

		if (!parse_result)
		{
			clipp::debug::print(std::cerr, parse_result);
			return EXIT_FAILURE;
		}
		if (validation_action == Validation::Help)
		{
			auto format = clipp::doc_formatting{} .start_column(4);
			std::cerr << clipp::make_man_page(cli, "validate", format);
			return EXIT_SUCCESS;
		}
	}

	return 0;
}

Here is a sample of the bottom of the call stack in GDB. I'm not including the remaing parts as I think you must have got it from the first three.

(gdb) f 261909
#261909 0x00000000004024dd in main (argc=1, argv=0x7fffffffdf78) at ./clipp_rec_loop.cpp:29
29                                       clipp::value("result path",  &result_path)) |
(gdb) f 261908
#261908 0x0000000000413a75 in clipp::value<std::string*>(std::string const&, std::string*&&) (label="result path", 
    tgts#0=<unknown type in /home/nbrack/Projects/minimal_failing_product/clipp_rec_loop, CU 0x0, DIE 0x490ff>) at ./clipp.h:2091
2091            .required(true).blocking(true).repeatable(false);
(gdb) f 261907
#261907 0x000000000041b250 in clipp::detail::action_provider<clipp::parameter>::target<std::string*>(std::string*&&) (this=0x7fffffffd2b0, 
    t=<unknown type in /home/nbrack/Projects/minimal_failing_product/clipp_rec_loop, CU 0x0, DIE 0x42305>) at ./clipp.h:1308
1308            target(std::forward<T>(t));
(gdb) f 261906
#261906 0x000000000041b250 in clipp::detail::action_provider<clipp::parameter>::target<std::string*>(std::string*&&) (this=0x7fffffffd2b0, 
    t=<unknown type in /home/nbrack/Projects/minimal_failing_product/clipp_rec_loop, CU 0x0, DIE 0x42305>) at ./clipp.h:1308
1308            target(std::forward<T>(t));
(gdb) f 261905
#261905 0x000000000041b250 in clipp::detail::action_provider<clipp::parameter>::target<std::string*>(std::string*&&) (this=0x7fffffffd2b0, 
    t=<unknown type in /home/nbrack/Projects/minimal_failing_product/clipp_rec_loop, CU 0x0, DIE 0x42305>) at ./clipp.h:1308
1308            target(std::forward<T>(t));

I'm building with g++7 under CentOS Linux 7:

$ /opt/rh/devtoolset-7/root/bin/g++ --version
g++ (GCC) 7.2.1 20170829 (Red Hat 7.2.1-1)
Copyright (C) 2017 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

If you need any more information, please ask.

Parse error for float/double with leading dot

I could have misunderstood how to use clipp properly but I have found using multiple optional numeric values with leading dot leads to incorrect parsing, for example:

double a, b;
auto FOO = (command("foo").set(selected,mode::FOO),
            opt_value("a", a) & opt_value("b", b));

gives the correct values when called as "> foo 0.1 0.2", but when called as "> foo .1 .2" this results in

a = 0
b = 1.2

Wrong usage line for (option & value | option)

int main(int argc, char* argv[]) {
    using namespace clipp;
    int i = 0;
    auto cli = (option("-b") & integer("size", i)) | option("-a");
    std::cout << usage_lines(cli, argv[0]) << '\n';
}

observed: ./usage ([-b <size>] | -a)
expected: ./usage [-b <size>] | -a]

Support std::optional<..>

I would like to be able to parse a struct containing std::optional<..> fields. For example:

struct arg_values {
     std::optional<int> pid;
} values;

auto cli = (
    clipp::option("--pid") & clipp::opt_integer("PID", values.parent_pid)
);

(feature request) Automatically print default values for options

From docs it looks like the task of displaying a default value for an option is fully on the shoulders of the deveolper. i.e. one should manually write option("-s") & integer("filesize", size).doc("max size of the file (default: " + to_string(size) + ")").

It would be nice if the library would provide method to easily display defaults in some sort of "standard way" if it's needed, maybe something like option() & integer.doc().print_default()

Weird problem with order of elements in the cli spec

This program gives:

test(false)=Failed
test(true)=OK

With HEAD on Linux Ubuntu with gcc 9.2.1 and clang 9.0.0.

I don't think the changein order should matter.

#include "clipp.h"

#include
#include

using namespace clipp;
using namespace std;

enum class Mode
{
Unknown,
Error,
Help,
DeviceList,
DeviceInfo
};

bool test_(int argc, char* argv[], bool order)
{
Mode m = Mode::Unknown;
string target;

auto helpMode = (
    command("help").set(m, Mode::Help)
);

auto deviceListMode = (
    command("devicelist")
        .set(m, Mode::DeviceList)
        .doc("List available hardware devices")
);

auto deviceInfoMode = (
    command("deviceinfo")
        .set(m, Mode::DeviceInfo)
        .doc("Display information about a hardware device")
        ,
    value("device", target)
        .doc("Device name to provide information for")
);

auto cli = order ? (
        helpMode | deviceInfoMode | deviceListMode
    ) : (
        helpMode | deviceListMode | deviceInfoMode
    );

return (bool)clipp::parse(argc, argv, cli);

}

const char* test(bool order)
{
static char* argv[] = {
(char*)"app", (char*)"deviceinfo", (char*)"foo", nullptr
};

auto rc = test_(3, argv, order);

return rc ? "OK" : "Failed";

}

int main(int, char*[])
{
std::cout << "test(false)=" << test(false) << std::endl;
std::cout << "test(true)=" << test(true) << std::endl;
return 0;
}
<<<<<<<
bug.cpp.gz

How to repeat repeatable values.

How do i correctly write:

std::string cmdFnTypeDef;
std::vector<std::string> cmdFnArgs;
auto cli = (
	clipp::repeatable(clipp::option("-f", "--func") & clipp::value("typedef", cmdFnTypeDef) & clipp::values("args", cmdFnArgs))
);

i want it to look like:
-f blah(a1,a2,a3,a4) 0 1 2 3
-f blah2(a1,a2) 0 1
-f blah2(a0) 4

but as is the first -f is only matched and all the rest of the args end up in the vector. I could duplicate the group but then i'd have hard cap on the count of -f's i support.

Possible memory leak

data(): param{} {}

It seems that here param is initialized but sometimes not destroyed. ASAN Log (see #5):

    #0 0x3b85d2 in operator new(unsigned long) /opt/llvm/src/projects/compiler-rt/lib/asan/asan_new_delete.cc:92:3
    #1 0x534142 in std::__1::__allocate(unsigned long) /opt/llvm/stage/bin/../include/c++/v1/new:226:10
    #2 0x534142 in std::__1::allocator<std::__1::__function::__func<clipp::parameter::predicate_adapter, std::__1::allocator<clipp::parameter::predicate_adapter>, clipp::subrange (std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&)> >::allocate(unsigned long, void const*) /opt/llvm/stage/bin/../include/c++/v1/memory:1747
    #3 0x534142 in std::__1::function<clipp::subrange (std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&)>::function<clipp::parameter::predicate_adapter, void>(clipp::parameter::predicate_adapter) /opt/llvm/stage/bin/../include/c++/v1/functional:1774
    #4 0x5301d2 in clipp::parameter::parameter() .../clipp.h:1820:9
    #5 0x52f81b in clipp::group::child_t<clipp::parameter, clipp::group>::data::data() .../clipp.h:2590:21
    #6 0x552079 in clipp::group::child_t<clipp::parameter, clipp::group>::child_t(clipp::group::child_t<clipp::parameter, clipp::group> const&) .../clipp.h:2437:38
    #7 0x551da8 in void std::__1::allocator<clipp::group::child_t<clipp::parameter, clipp::group> >::construct<clipp::group::child_t<clipp::parameter, clipp::group>, clipp::group::child_t<clipp::parameter, clipp::group>&>(clipp::group::child_t<clipp::parameter, clipp::group>*, clipp::group::child_t<clipp::parameter, clipp::group>&&&) /opt/llvm/stage/bin/../include/c++/v1/memory:1759:31
    #8 0x551da8 in void std::__1::allocator_traits<std::__1::allocator<clipp::group::child_t<clipp::parameter, clipp::group> > >::__construct<clipp::group::child_t<clipp::parameter, clipp::group>, clipp::group::child_t<clipp::parameter, clipp::group>&>(std::__1::integral_constant<bool, true>, std::__1::allocator<clipp::group::child_t<clipp::parameter, clipp::group> >&, clipp::group::child_t<clipp::parameter, clipp::group>*, clipp::group::child_t<clipp::parameter, clipp::group>&&&) /opt/llvm/stage/bin/../include/c++/v1/memory:1670
    #9 0x551da8 in void std::__1::allocator_traits<std::__1::allocator<clipp::group::child_t<clipp::parameter, clipp::group> > >::construct<clipp::group::child_t<clipp::parameter, clipp::group>, clipp::group::child_t<clipp::parameter, clipp::group>&>(std::__1::allocator<clipp::group::child_t<clipp::parameter, clipp::group> >&, clipp::group::child_t<clipp::parameter, clipp::group>*, clipp::group::child_t<clipp::parameter, clipp::group>&&&) /opt/llvm/stage/bin/../include/c++/v1/memory:1516
    #10 0x551da8 in void std::__1::allocator_traits<std::__1::allocator<clipp::group::child_t<clipp::parameter, clipp::group> > >::__construct_range_forward<clipp::group::child_t<clipp::parameter, clipp::group>*, clipp::group::child_t<clipp::parameter, clipp::group>*>(std::__1::allocator<clipp::group::child_t<clipp::parameter, clipp::group> >&, clipp::group::child_t<clipp::parameter, clipp::group>*, clipp::group::child_t<clipp::parameter, clipp::group>*, clipp::group::child_t<clipp::parameter, clipp::group>*&) /opt/llvm/stage/bin/../include/c++/v1/memory:1600
    #11 0x551da8 in std::__1::enable_if<__is_forward_iterator<clipp::group::child_t<clipp::parameter, clipp::group>*>::value, void>::type std::__1::vector<clipp::group::child_t<clipp::parameter, clipp::group>, std::__1::allocator<clipp::group::child_t<clipp::parameter, clipp::group> > >::__construct_at_end<clipp::group::child_t<clipp::parameter, clipp::group>*>(clipp::group::child_t<clipp::parameter, clipp::group>*, clipp::group::child_t<clipp::parameter, clipp::group>*, unsigned long) /opt/llvm/stage/bin/../include/c++/v1/vector:1030
    #12 0x54fea8 in std::__1::vector<clipp::group::child_t<clipp::parameter, clipp::group>, std::__1::allocator<clipp::group::child_t<clipp::parameter, clipp::group> > >::vector(std::__1::vector<clipp::group::child_t<clipp::parameter, clipp::group>, std::__1::allocator<clipp::group::child_t<clipp::parameter, clipp::group> > > const&) /opt/llvm/stage/bin/../include/c++/v1/vector:1213:9
    #13 0x520006 in clipp::group::group(clipp::group const&) .../clipp.h:2936:5
    #14 0x3c4b5c in clipp::operator,(clipp::parameter, clipp::parameter) .../clipp.h:3333:12
    #15 0x3c34a1 in main .../main.cpp:22:86
    #16 0x7fde62ff2f39 in __libc_start_main (/lib64/libc.so.6+0x20f39)

SUMMARY: AddressSanitizer: 128 byte(s) leaked in 2 allocation(s).

data probably shouldn't initialize param at all since the enclosing child_t doesn't have a default ctor anyway. This gets rid of the leak.

data() {} 

Parameter flagged as missing even though it appears

Hi. I get the following behavior using the latest version of clipp. The issue seems to be that the required options are only properly parsed if they appear in a certain order. Consider the following small test program:

#include "clipp.h"
#include <iostream>

int main(int argc, char* argv[]) {
  using namespace clipp;
  enum class mode {help, index};
  mode selected = mode::help;
  std::string odir;
  std::string gfaFile;
  std::string rfile;
  int k{31};
  bool isSparse{false};
  int extensionSize{4};

  auto indexMode = (
                    command("index").set(selected, mode::index),
                    (required("-o", "--output") & value("output dir", odir)) % "directory where index is written",
                    (required("-g", "--gfa") & value("gfa file",gfaFile)) % "path to the GFA file",
                    (required("-r", "--ref") & value("ref file",rfile)) % "path to the reference fasta file",
                    (option("-k", "--klen") & value("kmer length",k))  % "length of the k-mer with which the dBG was built (default = 31)",
                    (option("-s", "--sparse").set(isSparse, true)) % "use the sparse pufferfish index (less space, but slower lookup)",
                    (option("-e", "--extension") & value("extension size",extensionSize)) % "length of the extension to store in the sparse index (default = 4)"
                    );

auto cli = (
              (indexMode | command("help").set(selected,mode::help) ),
              option("-v", "--version").call([]{std::cout << "version 0.1.0\n\n";}).doc("show version"));

  decltype(parse(argc, argv, cli)) res;
  try {
    res = parse(argc, argv, cli);
  } catch (std::exception& e) {
    std::cout << "\n\nParsing command line failed with exception: " << e.what() << "\n";
    std::cout << "\n\n";
    std::cout << make_man_page(cli, "x");
    return 1;
  }


  if(res) {
    switch(selected) {
    case mode::index: std::cout << "successful; call index\n";  break;
    case mode::help: std::cout << make_man_page(cli, "x"); break;
    }
  } else {
    auto b = res.begin();
    auto e = res.end();
    std::cerr << "any blocked " << res.any_blocked() << "\n";
    std::cerr << "any conflict " << res.any_conflict() << "\n";
    std::cerr << "any bad repeat " << res.any_bad_repeat() << "\n";
    std::cerr << "any error " << res.any_error() << "\n";
    for( auto& m : res.missing() ) {
      std::cerr << "missing " << m.param()->label() << "\n";
    }
    if (std::distance(b,e) > 0) {
      if (b->arg() == "index") {
        std::cout << make_man_page(indexMode, "x");
      } else {
        std::cout << "There is no command \"" << b->arg() << "\"\n";
        std::cout << usage_lines(cli, "x") << '\n';
        return 1;
      }
    } else {
      std::cout << usage_lines(cli, "x") << '\n';
      return 1;
    }
  }
return 0;
}

When I pass all of the required options as follows, the parser fails. All of the options do look to have the appropriate associated value, but the "gfa_file" option is marked as missing even though it is clearly present.

rob@server:~/main/build$ ./main index -g valid_file.gfa -o out_dir -r rfile 
any blocked 0
any conflict 0
any bad repeat 0
any error 1
missing
missing gfa_file
SYNOPSIS
        main index -o <output_dir> -g <gfa_file> -r <ref_file> [-k <kmer_length>] [-s] [-e <extension_size>]

OPTIONS
        -o, --output <output_dir>
                    directory where index is written

        -g, --gfa <gfa_file>
                    path to the GFA file

        -r, --ref <ref_file>
                    path to the reference fasta file

        -k, --klen <kmer_length>
                    length of the k-mer with which the dBG was built (default = 31)

        -s, --sparse
                    use the sparse pufferfish index (less space, but slower lookup)

        -e, --extension <extension_size>
                    length of the extension to store in the sparse index (default = 4)
rob@server:~/main/build$

Strangely, if I simply change the order of the named options, everything works as expected:

rob@server:~/main/build$ ./main index -o out_dir -g valid_file.gfa -r rfile 
successful; call index
rob@server:~/main/build$

Presumably, both of these invocations should be equivalent. Any idea why the former is failing?

Thanks!
Rob

[Edit: In case it's helpful, I found the function you have to print the parsing results in the debug namespace. Here is what it gives with the example that fails]

#1 index -> index
#2 -g -> -g
#3 /mnt/scratch7/pufferfish_data/k31_n_gencode.v25.pc_transcripts_fixed.pufferized.gfa -> gfa_file
#4 -o -> -o
#5 test_new_parser -> output_dir
#6 -r -> -r
#7 /mnt/scratch7/gencode.v25.pc_transcripts_fixed.fa -> ref_file
-g       [missing after 4]
gfa_file         [missing after 4]

Parameter Verification

Hi,

I had q quick look through the docs but apart from generating a "filter", i.e "integer", I didn't find any way to define the required type of options.

For example, if I specify option "n" as an integer, I'd ideally like an automated error message during parsing if the user specifies something that can't be parsed as an integer. I'd also like the integer requirement to be stated in the auto-generated documentation.

Is this possible at the moment?

Group of single parameter

Hi,

I am playing around this lib right now and found it is great.

I wanted to created a group of one prarameter:

cli = (
    "group 1" % (
        option("-a").set(a) % "option a"
    ),
    "group 2" % (
        option("-b").set(b) % "option b",
        option("-c").set(c) % "option c"
    )
)

but the documentation generated from this is incorrect:

-a    group1
group2
    -b option b
    -c option c

The problem is that when there is only one entry in the parenthesis, no group is created. I tried doing clipp::group(option("-a").set(a) % "option a") but it seems does not fix the problem.

Any idea?

Is there a way to define a "default" command (if none is given)?

I am using the commands semantics to implement several mandatory choices, but I would like to have a default command choice, when the user does not include the command on the command line.

So far I have found a workaround which parses the arguments twice. First with the (mandatory) commands and then without them. If the second parsing succeeds I assume the command was not given and supply the default one. I.e. like this:

clipp::group cli_fallback = (
    (option("-a"),
    (option("-b"),
    (option("-c")
    );

clipp::group cli = (
    (command("cmd1") \
        | command("cmd2") \
        | command("cmd3") \
        | command("cmd4")),
    cli_fallback);

if (parse(argc, argv, cli)) {
        res = run(...);
} else {
    if (parse(argc, argv, cli_fallback)) {
        res = run(default_cmd, ...);
    } else {
        std::cout << "Usage:\n" << usage_lines(...) << '\n';
    }
}

This works, but seems a bit heavy.

Included Windows.h causes std::min bug

Problem

When using the library on Windows, with the header Windows.h included, I get an error in the places that std::min is used. Wherever min is used, the macro expands to:

(a < b) ? a : b

This is because Microsoft defines min and max in its headers.

Solution

To fix this you must define NOMINMAX before including Windows.h.

#define NOMINMAX 
#include <Windows.h>
#include <clipp.h>

Proposed Fix

I would recommend performing a check and if on Windows, defining NOMINMAX.

Multiple commands problem - use one_of instead of |

Hi,

some time ago I stumbled upon a problem with CLI, which consisted of three or more commands. I have found a work-around and I am posting it here hoping it will help others struggling with the same.

The smallest non-working example is:

    auto cli = (
        command("a").set(m.a) |
        command("b").set(m.b) |
        (
            command("c").set(m.c) & value("i", m.i)
        )
    );

where clipp would parse a, b, but not c 123.

It turned out that I can make clipp do what I want if instead of specifying the alternative of commands with | I'd use clipp::one_of.
The example above becomes:

    auto cli = one_of(
        command("a").set(m.a),
        command("b").set(m.b),
        command("c").set(m.c) & value("i", m.i)
    );

The docs, unless I am missing something, specify that one_of is the same as |, yet they produce different usage lines. The first one gives

((a|b) | (c <i>))

while the second one

(a | b | (c <i>))

Strange behavior with one_of

I have this code:

#include <iostream>

#include "clipp.h"

int main(int argc, char *argv[])
{
	std::string foo, bar, baz;

	auto a = (
		clipp::value("bar", bar)
	);

	auto b = (
		clipp::value("foo", foo),
		clipp::value("bar", bar)
	);

	auto cli = (a | b);

	if (!clipp::parse(argc, argv, cli)) {
		std::cerr << clipp::usage_lines(cli, argv[0]) << std::endl;
		return EXIT_FAILURE;
	}
}

Running c++ -std=c++17 main.cpp && ./a.out foo bar shows me the usage lines even though it's compatible.

String option eats a value without its flag after repeatable values

It looks like a bug to me, but it is highly plausible that I misunderstand repeatable values or something. I would appreciate if you could explain the logic behind the following parsing result.

Code:

void main(int argc, char* argv[]) {
    std::vector<int> vec;
    std::string str = "hungry";
    auto cli = (
      clipp::option("-v", "--vec") & clipp::integers("label-v", vec),
      clipp::option("-s", "--str") & clipp::value("label-s", str)
    );
    auto parsed = clipp::parse(argc, argv, cli);
    if (!parsed) std::cout << "ERROR!\n";
    clipp::debug::print(std::cout, parsed);
    std::cout << "vec : [";
    for (const auto i: vec) std::cout << i << ",";
    std::cout << "]\n";
    std::cout << "str : " << str << "\n";
}

Product:

% ./a.out -v 1 2 hello
#1 -v -> -v
#2 1 -> label-v
#3 2 -> label-v         [repeat 1]
#4 hello -> label-s
vec : [1,2,]
str : hello

I expect "ERROR!" is printed in this case.
Thanks

parse(): no known conversion from 'clipp::parameter' to 'const clipp::group' for 'cli'

I cannot compile a simple example. Am I doing anything wrong?

clipp 2c32b2f
clang version 8.0.1 (tags/RELEASE_801/final 366581)
g++ (SUSE Linux) 9.1.1 20190723 [gcc-9-branch revision 273734]

% cat main.cpp
#include <iostream>
#include "clipp.h"
int main(int argc, char** argv)
{
        using namespace clipp;
        bool help = false;
        auto cli = (option("-h").set(help));
        parse(argc, argv, cli);
        return 0;
}
% clang++ main.cpp
main.cpp:8:2: error: no matching function for call to 'parse'
        parse(argc, argv, cli);
        ^~~~~
./clipp.h:5325:1: note: candidate function not viable: no known conversion from 'clipp::parameter' to
      'const clipp::group' for 3rd argument
parse(const int argc, char* argv[], const group& cli, arg_index offset = 1)
^
./clipp.h:5313:1: note: candidate template ignored: deduced conflicting types for parameter 'InputIterator'
      ('int' vs. 'char **')
parse(InputIterator first, InputIterator last, const group& cli)
^
./clipp.h:5281:1: note: candidate function not viable: requires 2 arguments, but 3 were provided
parse(arg_list args, const group& cli)
^
./clipp.h:5294:1: note: candidate function not viable: requires 2 arguments, but 3 were provided
parse(std::initializer_list<const char*> arglist, const group& cli)
^
1 error generated.
% g++ main.cpp                                                                        :(
main.cpp: In function ‘int main(int, char**)’:
main.cpp:8:23: error: no matching function for call to ‘parse(int&, char**&, clipp::parameter&)’
    8 |  parse(argc, argv, cli);
      |                       ^
In file included from main.cpp:2:
clipp.h:5281:1: note: candidate: ‘clipp::parsing_result clipp::parse(clipp::arg_list, const clipp::group&)’
 5281 | parse(arg_list args, const group& cli)
      | ^~~~~
clipp.h:5281:1: note:   candidate expects 2 arguments, 3 provided
clipp.h:5294:1: note: candidate: ‘clipp::parsing_result clipp::parse(std::initializer_list<const char*>, const clipp::group&)’
 5294 | parse(std::initializer_list<const char*> arglist, const group& cli)
      | ^~~~~
clipp.h:5294:1: note:   candidate expects 2 arguments, 3 provided
clipp.h:5313:1: note: candidate: ‘template<class InputIterator> clipp::parsing_result clipp::parse(InputIterator, InputIterator, const clipp::group&)’
 5313 | parse(InputIterator first, InputIterator last, const group& cli)
      | ^~~~~
clipp.h:5313:1: note:   template argument deduction/substitution failed:
main.cpp:8:23: note:   deduced conflicting types for parameter ‘InputIterator’ (‘int’ and ‘char**’)
    8 |  parse(argc, argv, cli);
      |                       ^
In file included from main.cpp:2:
clipp.h:5325:1: note: candidate: ‘clipp::parsing_result clipp::parse(int, char**, const clipp::group&, clipp::arg_index)’
 5325 | parse(const int argc, char* argv[], const group& cli, arg_index offset = 1)
      | ^~~~~
clipp.h:5325:50: note:   no known conversion for argument 3 from ‘clipp::parameter’ to ‘const clipp::group&’
 5325 | parse(const int argc, char* argv[], const group& cli, arg_index offset = 1)
      |                                     ~~~~~~~~~~~~~^~~

Move most documentation out of README.md

This repository's README.md is way too long. The documentation specifics should be in a separate .md file, on the wiki, or both; and the main README.md should focus on the sections other than the Detailed Examples. In fact, Overview is also too long; I'd stick to a couple of examples, no more than that for the repo landing page.

Double move/forward

clipp/include/clipp.h

Lines 1827 to 1833 in 384d106

parameter(arg_string str, Strings&&... strs):
flags_{std::move(str), std::forward<Strings>(strs)...},
matcher_{predicate_adapter{match::none}},
label_{}, required_{false}
{
flags(std::move(str), std::forward<Strings>(strs)...);
}

arg_string str and Strings&&... strs are moved/forwarded twice. Possible bug/typo?

any_other() records the wrong argument

auto cli = (
	option("--help").set(show_help)
		% "Show this help message",
	(option("-J", "--threads") & value("num_threads", num_threads))
		% "The number of threads to use (default 0, meaning 'auto')",
	value("db_dir").set(dbdir).required(true).blocking(true)
		% "The database directory location",
	any_other(invalid_params)
);

I'm trying to use any_other to error out on unknown flags. In the above, if I run with ./my_prog --askdfjaksjd some_directory, invalid_params receives some_directory instead of the garbage flag. Is there a way to get the garbage flag in a list somewhere to give better error messages?

Repeatable required option fails to parse with one value

Right from the README quick reference, trying to use repeatable(required("-l") & value("lines", ids)) fails to parse correctly, where the parsing_result considers the required value as a missing argument:

#include "clipp.h"
#include <iostream>

int main(int argc, char *argv[])
{
    using namespace clipp;

    std::vector<size_t> ids;

    auto cli = repeatable(required("-l") & value("lines", ids));
    auto result = parse(argc, argv, cli);
    if (!result) {
        debug::print(std::cerr, result);
        return 1;
    }
    return 0;
}

Result of running:

$ ./a.out -l 0
#1 -l -> -l 
#2 0 -> lines 
lines    [missing after 0]

Note that more than one argument (e.g., ./a.out -l 0 -l 1) parses correctly.

Wide string support?

Any plans for this? I took a look at the code, it seems the majority of the library is written with a type alias so that is nice. I think a #define CLIPP_STR_TYPE std::wstring then using arg_str = CLIPP_STR_TYPE would be an easy solution, does this seem difficult?

Matching flags with similar prefix

Let's assume we have the following interface:

  cli = (option("--flag"),
         option("--flag-extra", "--flag-extra=") & value)

Passing arguments --flag --flag-extra=foo will fail, because --flag-extra= will first partially match with --flag in try_match_joined_sequenceprefix_match. One option to fix this would be to reorder groups, or perhaps when the matched string is not exactly the same length we could continue iterating through arguments to find the longest match. What would be a good way to fix it?

help is never sets to true and required makes all invalid

In the example I expect that in case of args1 the variable _help will be set to true. But its always false.

If I set the address as clipp::required then all except args1 is considered valid. But even in case of args1, _help is set to false.

#include "clipp.h"
#include <string>
#include <cstdint>
#include <iostream>

template <typename It>
void parse(It first, It last){
    std::string _target;
    std::uint32_t _batch = 0, _wait = 0;
    bool _help = false;
    
    auto command_options = (
        clipp::value("address", _target) % "target address",
        clipp::option("-b", "--batch") & clipp::value("size", _batch)   % "number of requests in one batch",
        clipp::option("-w", "--wait")  & clipp::value("seconds", _wait) % "number of seconds to wait after each batch of requests completes"
    );
    auto options = (
        command_options | clipp::command("-h", "--help").set(_help, false) % "show this help message"
    );
    auto res = clipp::parse(first, last, options);
    if(res.any_error() || _help){
        std::cout << "help: " << _help << std::endl;
        clipp::man_page man = clipp::make_man_page(options, "cmd");
        std::cout << man; 
    }else{
        std::cout << "cmd" << " " << _target << " -b" << _batch << " -w" << _wait << std::endl;
    }
    std::cout << "-------------------" << std::endl;
    std::cout << "_target: " << _target << " _batch: " << _batch << " _wait: " << _wait << " _help: " << _help << std::endl;
    std::cout << "-------------------" << std::endl;
}

int main(){
    std::vector<std::string> args1 = {"-h"};
    parse(args1.begin(), args1.end());
    
    std::vector<std::string> args2 = {"target", "-b", "20", "-w", "4"};
    parse(args2.begin(), args2.end());
    
    std::vector<std::string> args3 = {"target"};
    parse(args3.begin(), args3.end());
    
    std::vector<std::string> args4 = {};
    parse(args4.begin(), args4.end());
    
    return 0;
}

Parse failing on otherwise valid arguments

	auto compress = (
		command("compress").set(sc, subcommand::COMPRESS)
		| command("decompress").set(sc, subcommand::DECOMPRESS)
	) % "(de)compress stdin to stdout";

	auto convert = (
		command("convert").set(sc, subcommand::CONVERT),
		value("input.vox", in_path),
		value("output.tox", out_path)
	) % "convert a .VOX scene to .TOX asset";

	auto info = (
		command("info").set(sc, subcommand::INFO),
		value("input.tox", in_path)
	) % "show information about a .TOX asset";

	auto verbose_flag = option("-v", "--verbose").set(verbose, true)
		% "enable verbose output";
	auto help_flag = option("--help").set(help, true)
		% "show this help message";

	auto cli = one_of(
		(
			verbose_flag,
			one_of(
				compress,
				convert,
				info
			)
		),
		help_flag
	);

With the above, I get the following manpage output (looks 100% correct):

SYNOPSIS
        tox [-v] (compress|decompress)
        tox [-v] convert <input.vox> <output.tox>
        tox [-v] info <input.tox>
        tox --help

OPTIONS
        -v, --verbose
                    enable verbose output

        compress|decompress
                    (de)compress stdin to stdout

        convert <input.vox> <output.tox>
                    convert a .VOX scene to .TOX asset

        info <input.tox>
                    show information about a .TOX asset

        --help      show this help message

However, only compress and decompress work; convert and info cause clipp to fail at the parsing step.

Seems like a bug - is that the case? Or is there some sort of workaround for this?

Option docstrings in generated documentation

Using VS 2017 Community 15.9.13 to build the following code:

int main(int argc, char *argv[])
{
    using namespace clipp; using std::cout; using std::string;

    enum class mode { help };
    mode selected = mode::help;
    std::vector<string> input;

    auto cli = (
        (command("help").set(selected, mode::help).doc("help with the thing")),
        option("-i", "--input") & value("name", input) % "input file",
        option("-v", "--version").call([] {cout << "version 1.0\n\n"; }).doc("show version"));

    parse(argc, argv, cli);
    cout << make_man_page(cli, "clipp-test");
    return 0;
}

The output is missing --input option name in the OPTIONS:

SYNOPSIS
        clipp-test help [-i <name>] [-v]

OPTIONS
        help        help with the thing
        <name>      input file
        -v, --version
                    show version

If I however prioritize the & operator in the option definition by adding the parenthesis like this:

        (option("-i", "--input") & value("name", input)) % "input file",

The output shows correctly:

SYNOPSIS
        clipp-test help [-i <name>] [-v]

OPTIONS
        help        help with the thing
        -i, --input <name>
                    input file

        -v, --version
                    show version

cmake version

cmake version 3.8 is too high for nothing
on debian, standard version is 3.7.?
I don't know for other distrib but I guess 3.8 is too high for most of them
perhaps someone could details the version of cmake on mainstream distrib, to help choosing a better version level :)

thx for that clever lib!

CMakeLists.txt is not included in the release package

Hello:

The CMakeLists.txt file is not included in the release package. As a result, it is not possible to use the tagged code with CMake FetchContent. Yes, using the latest master is possible, but it would be nice to use the release versions also.

Issue with optional flag

See this example:

int main(int argc, char **argv) {
	bool optionSpecified = false;
	std::string optionvalue;

	auto helpCmd = command("help");
	auto cmd1 = command("cmd1");
	auto cmd2 = command("cmd2");
	auto cmd3 = command("cmd3");

	auto optionFlag = (option("-o").set(optionSpecified, true) & value("optionvalue").set(optionvalue));

	auto cli = (helpCmd | cmd1 | ((cmd2 | cmd3), optionFlag));

	if (parse(argc, argv, cli)) {
		tfm::printf("Option: %s Value: %s\n", optionSpecified, optionvalue);
	} else {
		tfm::printf("Parse failed!\n");
		std::cerr << usage_lines(cli, argv[0]) << std::endl;
	}
	return 0;
}

This should accept the "help", "cmd1", "cmd2" or "cmd3" commands, but allow the "-o value" flag to be specified optionally on the "cmd2" or "cmd3" commands. However, while the usage_lines output seems to be correct, the parse fails if "cmd2" or "cmd3" is specified at all, with or without the "-o value" flag.

(tfm::printf is from https://github.com/c42f/tinyformat)

Am I doing something stupid or is this a bug?

Case insensitive matching

Is it possible for the parameter matching to be case insensitive ?
For example, option("-ext") would parse correctly with "-Ext", "-ext", "-EXT", "-eXt", "-ExT"

Wrong Documentation line for Option

Option Definition:

option("-b") & value("size=1", bs) % "Maximum b size",

Documentation formatting:
<size=1> Maximum b size
Expected:
-b <size=1> Maximum b size

documentation formatting wrong for groups with only value options

Code:

auto cli = (option("--a") & value("A") % "help a", option("-b") & value("B") % "help b") % "group 1";
std::cout << make_man_page(cli, "test");

Expected:

SYNOPSIS
        test [--a <A>] [-b <B>]

OPTIONS
        group 1
            <A>     help a
            <B>     help b

Actual:

SYNOPSIS
        test [--a <A>] [-b <B>]

OPTIONS
        [--a <A>] [-b <B>]
                    group 1

This works fine:

auto cli = (option("--a") & value("A") % "help a", option("-b") % "help b") % "group 1";
std::cout << make_man_page(cli, "test");
SYNOPSIS
        test [--a <A>] [-b]

OPTIONS
        group 1
            <A>     help a
            -b      help b

live example: https://wandbox.org/permlink/m9OeLGfP7ZDiOjD1

Problem with option_value having the same prefix as a command

I have this code

auto run_spec = "run options" % (
	command("run").set(args.selected_mode, mode::run),
	value("input file or folder", args.target) % "Path to a file",
	(option("--test_spec") & value("wildcard pattern", args.test_spec)) % "Run specific tests"
);
auto cli = ( run_spec | command("help").set(args.show_help) );
if (!parse(argc, argv, cli)) {
	std::cout << make_man_page(cli, "testo") << std::endl;
	return -1;
}

Running like this

program run some_file --test_spec something is totally fine, but

Running like this

program run some_file --test_spec run_something gives me usage lines

not set string value

sample code:

#include <iostream>
#include <filesystem>
#include <vector>
#include <clipp.h>

#define VERSION "0.0.0.0"
class app {
public:
    app(int argc, const char** argv) {
        args.assign(argv, argv + argc);
    }

    int exec() {
        try {
            cli =
                ("General:" %
                     ((clipp::option("-c", "--config") & clipp::value("file name", config_file)) %
                          "config file",
                      (clipp::option("-h", "--help").set(f_help)) % "help message",
                      (clipp::option("-v", "--version").set(f_version)) % "show version") ,
                 "Commands:" %
                     ("service" %
                      (clipp::command("service") % "service controll",
                       (((clipp::option("-I", "--install").set(cmd_install)) % "install service") |
                        ((clipp::option("-R", "--remove").set(cmd_remove)) % "remove service")))));

            auto parse_res = clipp::parse(args, cli);

            if(!parse_res.any_error()) {
                show_help();
                return 1;
            }

            if(f_help) {
                show_help();
                return 0;
            }

            if(f_version) {
                std::cout << VERSION << std::endl;
                return 0;
            }

            std::filesystem::path fname(config_file);
            std::cout << "cfg: " << config_file << "\n";
            std::cout << fname.is_absolute() << "\n";
            std::cout << fname << "\n";

            if(cmd_install) {
                //install
                return 0;
            }

            if(cmd_remove) {
                //remove
                return 0;
            }
        } catch(std::exception& rcError) {
            std::cerr << rcError.what() << std::endl;
            return 1;
        }
        return 0;
    }
    void show_help() {
        auto fmt = clipp::doc_formatting {}
                       .first_column(4)
                       .doc_column(36)
                       .merge_alternative_flags_with_common_prefix(true);

        std::cout << clipp::make_man_page(cli, "server", fmt)
                         .prepend_section("AUTHORS", "\tauthor")

                         .prepend_section("DESCRIPTION", "\tThe App")
                  << '\n';
    }

private:
    std::vector<std::string> args;
    clipp::group cli;
    bool f_help = false;
    bool f_version = false;
    bool cmd_install = false;
    bool cmd_remove = false;
    std::string config_file;
};

int main(int argc, const char* argv[]) {	
    app app_instance(argc, argv);
    return app_instance.exec();
}

flag output:

 .\cpp_clipp.exe -c config.toml

DESCRIPTION
        The App

AUTHORS
        author

SYNOPSIS
    server [-c <file name>] [-h] [-v] service [-(I|R)]

OPTIONS
    General:
        -c, --config <file name>    config file
        -h, --help                  help message
        -v, --version               show version

    Commands:
        service                     service controll
        -I, --install               install service
        -R, --remove                remove service

Output

.\cpp_clipp.exe -c config.toml
cfg:
0
""

variable config_file is empty.

(feature request) Support for conversion of std::filesystem::path

Since CLIs often have to deal with files and directories being passed to them on the command line, the C++17 filesystem classes make life a lot easier.
It would be nice to be able to just do this:

#include <filesystem>
std::filesystem::path input_file;
auto cli = (
		value("input file", input_file),
);

As far as I can tell, this would require another specialization of the make<> template.
While the issue is not really all that pressing for the official library (vendors are still working on officially implementing the C++17 library extensions, although std::experimental::filesystem does exist for VS2017), is there a documented way of providing user-supplied specializations of make<>?

Timing example not working with flags

The timing.cpp example is broken when passing flags:

This works fine:

./timing -n 10 exename -- some_string

This gives a parse error:

./timing -n 10 exename -- --another-flag -a

I would like to have functionality similar to this so I can pass the arguments after -- to another executable like in this example.

Don't duplicate documentation for re-used options

Is it possible to dedup args that are listed multiple times in different commands? Here's an example:

bool val1, val2, val3;
auto arg1 = required("--arg1").set(val1) % "doc for arg1";
auto arg2 = required("--arg2").set(val2) % "doc for arg2";
auto arg3 = required("--arg3").set(val3) % "doc for arg3";

auto cli = (command("cmd1"), arg1, arg2) | (command("cmd2"), arg2, arg3) | (command("cmd3"), arg1, arg3);
SYNOPSIS
        prog cmd1 --arg1 --arg2
        prog cmd2 --arg2 --arg3
        prog cmd3 --arg1 --arg3

OPTIONS
        --arg1      doc for arg1
        --arg2      doc for arg2
        --arg2      doc for arg2
        --arg3      doc for arg3
        --arg1      doc for arg1
        --arg3      doc for arg3

how to ignore arguments, like --undefok in glog

glog --undefok: https://gflags.github.io/gflags/

It is a fatal error to specify a flag on the commandline that has not been DEFINED somewhere in the executable. If you need that functionality for some reason -- say you want to use the same set of flags for several executables, but not all of them DEFINE every flag in your list -- you can specify --undefok to suppress the error.

How to specify number of times a value can be repeated

What is the best way to achieve a command line like the following:

exe fetch <origin> <origin> <origin>

The number of origins should be fixed to a given number.

The only way I could come up with after reading the documentation would be something like this:

std::string origin1, origin2, origin3;
command("fetch"), value("origin", origin1), value("origin", origin2), value("origin", origin3)

Is there any better way as this feels a bit clumsy? I was expecting something along these lines:

std::vector<std::string> origins;
command("fetch"), values(3, "origin", origins)

unused imports

For a lot of my projects I like to define log() so I can type as little as possible. This might be a bad idea, but nonetheless.. When I take it out #include <cmath> it compiles just fine (g++ 7.x), and the less imports the better IMO.

Edit: actually cloned and ran the tests.py file. And there a lot of errors popped up for missing imports. But for some, it seems the import is only needed by the test cpp files. In the case above, values_conversion_test.cpp failed, but putting the include into the test.cpp and it passed.

Use "=" as an optional separator of a long option and its value

Is it possible to use = as an option-value separator like in boost::program_options and Python argparse?

Current behavior (AFAIK):

# (option("--longopt") & value("arg", target))
Command           target
--longopt foo  => "foo"
--longoptfoo   => "foo"
--longopt=foo  => "=foo"

# (option("--longopt=") & value("arg", target))
Command           target
--longopt foo  => Not match
--longoptfoo   => Not match
--longopt=foo  => "foo"

Proposed behavior:

Command           target
--longopt foo  => "foo"
--longopt=foo  => "foo"

Thanks.

Prefix free check seems broken

The prefix free check doesn't seem to work (i.e., enabling it on various programs from the examples directory leads to failed assertions). I'm not entirely sure why this happens, but at least one issue seems to be that value options, when encountered in the prefix_free check are parsed as the empty string, and therefore appear as a prefix of everything else.

Error while compiling with vs2015 default compiler

in clipp.h:

Severity Code Description Project File Line Suppression State
Error C3256 'npos': variable use does not produce a constant expression
line: 101

Severity Code Description Project File Line Suppression State
Error C3256 'npos': variable use does not produce a constant expression
line: 122

Severity Code Description Project File Line Suppression State
Error C2066 cast to function type is illegal ArgumentParser
line: 174

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.