From 7c111c66d7cf0f6781edaf0d6de648e6ae79995a Mon Sep 17 00:00:00 2001 From: Andre Mueller Date: Fri, 26 Jan 2018 22:10:08 +0100 Subject: [PATCH] introduced 'greedy' option for parameters --- README.md | 155 +++++++++++++++++++++++++++++--------- examples/align.cpp | 81 ++++++++++++++++++++ examples/alternatives.cpp | 2 +- examples/finder.cpp | 4 +- examples/model.cpp | 136 +++++++++++++++++++++++++++++++++ examples/naval_fate.cpp | 19 +++-- examples/send.cpp | 4 +- examples/timing.cpp | 45 +++++++++++ include/clipp.h | 123 ++++++++++++++++++++++++++++-- 9 files changed, 514 insertions(+), 55 deletions(-) create mode 100644 examples/align.cpp create mode 100644 examples/model.cpp create mode 100644 examples/timing.cpp diff --git a/README.md b/README.md index b6c81a2..77b9756 100644 --- a/README.md +++ b/README.md @@ -136,18 +136,20 @@ There are two kinds of building blocks for command line interfaces: parameters a ```cpp bool a = false, f = false; string s; vector vs; -auto cli = ( // matches required positional repeatable - command("push"), // exactly yes yes no - required("-f", "--file").set(f), // exactly yes no no - option("-a", "--all", "-A").set(a), // exactly no no no +auto cli = ( // matches required positional repeatable + command("push"), // exactly yes yes no + required("-f", "--file").set(f), // exactly yes no no + option("-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 + 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 + 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: @@ -161,10 +163,10 @@ 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 + - 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 ##### [Flags + Values](#options-with-values) If you want parameters to be matched in sequence, you can tie them together using either ```operator &``` or the grouping function ```in_sequence```: @@ -189,7 +191,6 @@ auto cli = ( ); ``` - ##### [Filtering Value Parameters](#value-filters) 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: @@ -296,6 +297,7 @@ settings cmdline_settings(int argc, char* argv[]) { } ``` + #### Generating Documentation ([see also here](#documentation-generation)) Docstrings for groups and for parameters can either be set with the member function ```doc``` or with ```operator %```: ```cpp @@ -341,6 +343,7 @@ auto fmt = doc_formatting{}.start_column(2); cout << make_man_page(cli, "progname", fmt) << '\n'; ``` + #### (Error) Event Handlers ([see here](#error-handling), [and here](#per-parameter-parsing-report)) 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): ```cpp @@ -368,6 +371,21 @@ auto param = required("-nof").set(file,"") | ``` +#### 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: +```cpp +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](#greedy-parameters) with ```operator !```. This way, the next string after ```-f``` will always be matched with highest priority as soon as ```-f``` was given: +```cpp +auto cli = ( option("-a"), option("-f") & !value("filename"), option("-b") ); + // ^~~~~~ +``` +Be **very careful** with greedy parameters! + + + #### Parsing Result Analysis ```cpp auto cli = ( /* your interface here */ ); @@ -419,10 +437,11 @@ The repository folder "examples" contains code for most of the following example - [complex nestings](#complex-nestings) - [example from docopt](#an-example-from-docopt) - [value filters](#value-filters) +- [greedy parameters](#greedy-parameters) - [generalized joinable parameters](#generalized-joinable-parameters) - [custom value filters](#custom-value-filters) - [sanity checks](#sanity-checks) -- [error handling](#error-handling) +- [basic error handling](#basic-error-handling) - [parsing](#parsing) - [documentation generation](#documentation-generation) - [documentation filtering](#documentation-filtering) @@ -711,9 +730,9 @@ auto cli = ( ``` ```cpp auto cli = ( - (option("-n", "--count") & value("count").set(n)) % "number of iterations", - (option("-r", "--ratio") & value("ratio").call(print_ratio)) % "compression ratio", - (option("-m").set(domerge) & opt_value("lines=5").set(m)) % "merge lines (default: 5)" + (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](#coding-styles) for more on coding styles. @@ -919,7 +938,7 @@ OPTIONS -b use backup file -s use swap file - :vi, :st3, :atom, :emacs + :vim, :st3, :atom, :emacs editor(s) to use; multiple possible ``` @@ -945,17 +964,14 @@ auto cli = ( ) % "editor(s) to use; multiple possible" ); ``` -- Flags of parameters that take values cannot be joined with other flags. - Flags can be joined regardless of their length (second group in the example). -- Flags can only be joined if they have a non-empty common prefix (otherwise it would be too easy to confuse them with value parameters). -- The common prefix (```-``` or ```:``` in the example) must be given at least +- 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```, ... - + ```:vim```, ```:vim:atom```, ```:emacs:st3```, ```:vimatom```, ... #### More Examples @@ -1092,6 +1108,7 @@ SYNOPSIS OPTIONS -v, --verbose print detailed report + -b, --buffer [] sets buffer size in KiByte @@ -1318,15 +1335,18 @@ auto shipmove = ( 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 ) ) - | ( 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." ) - ) + ( 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." ); @@ -1355,12 +1375,13 @@ switch(m) { + ### 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](#custom-value-filters)). ```man -Usage: exec [-n ] [-l ...] [-r ] [-f ] +Usage: exec [-n ] [-l ...] [-b ] [-f ] ``` ```cpp @@ -1410,6 +1431,15 @@ auto p = parameter{ match::length{1,5} } ``` There are a couple of predefined filters in ```namespace clipp::match```, but you can of course write your own ones (see [here](#custom-value-filters)). +Here is another example that makes sure we don't catch any value starting with "-" as a filename: +```cpp +auto cli = ( + option("-a") + option("-f") & value(match::prefix_not("-"), "filename"), + option("-b") +); +``` + ```cpp namespace clipp { namespace match { @@ -1448,8 +1478,8 @@ namespace match { }; class length { - explicit numbers(size_t exact); - explicit numbers(size_t min, size_t max); + explicit length(size_t exact); + explicit length(size_t min, size_t max); subrange operator () (const string& arg); }; @@ -1458,6 +1488,55 @@ namespace match { +### 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: +```cpp +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: +```cpp +auto cli = ( option("-a"), option("-f") & greedy(value("filename")), option("-b") ); +``` +or using ```operator !```: +```cpp +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](#value-filters): +```cpp +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. @@ -1620,7 +1699,7 @@ assert( cli.common_flag_prefix() == "-" ); -### Error Handling +### 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 @@ -1639,12 +1718,14 @@ vector targets; vector 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(match::prefix_not("-"), "target", targets) + 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"; }) , diff --git a/examples/align.cpp b/examples/align.cpp new file mode 100644 index 0000000..fb795bf --- /dev/null +++ b/examples/align.cpp @@ -0,0 +1,81 @@ +/***************************************************************************** + * + * demo program - part of CLIPP (command line interfaces for modern C++) + * + * released under MIT license + * + * (c) 2017 André Müller; foss@andremueller-online.de + * + *****************************************************************************/ + +#include +#include +#include + +#include + + +int main(int argc, char* argv[]) +{ + using namespace clipp; + + enum class imode { file, args, stdio, random }; + enum class omode { file, stdio }; + auto input = imode::file; + auto output = omode::stdio; + std::int64_t minlen = 256; + std::int64_t maxlen = 1024; + std::string query, subject; + std::string outfile; + std::vector wrong; + + auto cli = ( + (option("-o", "--out").set(output,omode::file) & + value("file", outfile)) % "write results to file" + , + "read sequences from input files" % ( + command("-i", "--in"), + value("query file", query), + value("subject file", subject) + ) | + "specify sequences on the command line" % ( + command("-a", "--args").set(input,imode::args), + value("query string", query), + value("subject string", subject) + ) | + "generate random input sequences" % ( + command("-r", "--rand").set(input,imode::random), + opt_integer("min len", minlen) & + opt_integer("max len", maxlen) + ) | ( + "read sequences from stdin" % + command("-").set(input,imode::stdio) + ), + any_other(wrong) + ); + + + if(!parse(argc,argv, cli) || !wrong.empty()) { + if(!wrong.empty()) { + std::cout << "Unknown command line arguments:\n"; + for(const auto& a : wrong) std::cout << "'" << a << "'\n"; + std::cout << '\n'; + } + std::cout << make_man_page(cli, argv[0]) << '\n'; + return 0; + } + + switch(input) { + default: + case imode::file: /* ... */ break; + case imode::args: /* ... */ break; + case imode::stdio: /* ... */ break; + case imode::random: /* ... */ break; + } + + switch(output) { + default: + case omode::stdio: /* ... */ break; + case omode::file: /* ... */ break; + } +} diff --git a/examples/alternatives.cpp b/examples/alternatives.cpp index 7cd40c2..900a657 100644 --- a/examples/alternatives.cpp +++ b/examples/alternatives.cpp @@ -33,7 +33,7 @@ int main(int argc, char* argv[]) if(parse(argc, argv, cli)) { cout << "find '" << expr << "' in files: "; - for(const auto& f : files) cout << "'" << f << "' "; cout << '\n'; + for(const auto& f : files) { cout << "'" << f << "' "; } cout << '\n'; if(ifany) cout << "using 'any' mode\n"; if(ifall) cout << "using 'all' mode\n"; } diff --git a/examples/finder.cpp b/examples/finder.cpp index 837964b..c5d0866 100644 --- a/examples/finder.cpp +++ b/examples/finder.cpp @@ -51,11 +51,11 @@ int main(int argc, char* argv[]) switch(selected) { case mode::make: cout << "make new dictionary " << dict << " from wordfile(s) "; - for(const auto& s : input) cout << s << ' '; cout << '\n'; + for(const auto& s : input) { cout << s << ' '; } cout << '\n'; break; case mode::find: cout << "find words from dictionary " << dict << " in files "; - for(const auto& s : input) cout << s << ' '; cout << '\n'; + for(const auto& s : input) { cout << s << ' '; } cout << '\n'; cout << "output: "; if(split) cout << "splitted "; cout << "to "; diff --git a/examples/model.cpp b/examples/model.cpp new file mode 100644 index 0000000..7b3dc7d --- /dev/null +++ b/examples/model.cpp @@ -0,0 +1,136 @@ +/***************************************************************************** + * + * demo program - part of CLIPP (command line interfaces for modern C++) + * + * released under MIT license + * + * (c) 2017 André Müller; foss@andremueller-online.de + * + *****************************************************************************/ + +#include +#include +#include +#include + +#include + + +//------------------------------------------------------------------- +enum class mode { + none, train, validate, classify +}; + +struct settings { + mode selected = mode::none; + std::string imageFile; + std::string labelFile; + std::string modelFile = "out.mdl"; + std::size_t batchSize = 128; + std::size_t threads = 0; + std::size_t inputLimit = 0; + std::vector inputFiles; +}; + + +//------------------------------------------------------------------- +settings configuration(int argc, char* argv[]) +{ + using namespace clipp; + + settings s; + + std::vector unrecognized; + + auto isfilename = clipp::match::prefix_not("-"); + + auto inputOptions = ( + required("-i", "-I", "--img") & !value(isfilename, "image file", s.imageFile), + required("-l", "-L", "--lbl") & !value(isfilename, "label file", s.labelFile) + ); + + auto trainMode = ( + command("train", "t", "T").set(s.selected,mode::train) + .if_conflicted([]{std::cerr << "conflicting modes\n"; }), + + inputOptions, + + (option("-n") & integer("limit", s.inputLimit)) + % "limit number of input images", + + (option("-o", "-O", "--out") & !value("model file", s.modelFile)) + % "write model to specific file; default: 'out.mdl'", + + (option("-b", "--batch-size") & integer("batch size", s.batchSize)), + + (option("-p") & integer("threads", s.threads)) + % "number of threads to use; default: optimum for machine" + ); + + auto validationMode = ( + command("validate", "v", "V").set(s.selected,mode::validate), + !value(isfilename, "model", s.modelFile), + inputOptions + ); + + auto classificationMode = ( + command("classify", "c", "C").set(s.selected,mode::classify), + !value(isfilename, "model", s.modelFile), + !values(isfilename, "images", s.inputFiles) + ); + + auto cli = ( + trainMode | validationMode | classificationMode, + any_other(unrecognized) + ); + + auto res = parse(argc, argv, cli); + + debug::print(std::cout, res); + + if(!res || !unrecognized.empty()) { + std::string msg = "Wrong command line arguments!\n"; + + if(s.selected == mode::none) { + msg += "Please select a mode!\n"; + } + else { + for(const auto& m : res.missing()) { + if(!m.param()->flags().empty()) { + msg += "Missing option: " + m.param()->flags().front() + '\n'; + } + else if(!m.param()->label().empty()) { + msg += "Missing value: " + m.param()->label() + '\n'; + } + } + + for(const auto& arg : unrecognized) { + msg += "Argument not recognized: " + arg + '\n'; + } + } + + auto fmt = doc_formatting{}.start_column(8).doc_column(16); + //.max_flags_per_param_in_usage(3).surround_alternative_flags("(", ")"); + + msg += "\nUsage:\n" + usage_lines(cli, argv[0], fmt).str() + '\n'; + msg += "\nOptions:\n" + documentation(cli, fmt).str() + '\n'; + + throw std::invalid_argument{msg}; + } + + return s; +} + + + +//------------------------------------------------------------------- +int main(int argc, char* argv[]) +{ + try { + auto conf = configuration(argc, argv); + std::cout << "SUCCESS\n"; + } + catch(std::exception& e) { + std::cerr << "ERROR: " << e.what() << '\n'; + } +} diff --git a/examples/naval_fate.cpp b/examples/naval_fate.cpp index 039fab8..6ded8b7 100644 --- a/examples/naval_fate.cpp +++ b/examples/naval_fate.cpp @@ -68,15 +68,18 @@ int main(int argc, char* argv[]) 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 ) ) - | ( 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." ) - ) + ( 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." ); diff --git a/examples/send.cpp b/examples/send.cpp index ac7b931..edc18c7 100644 --- a/examples/send.cpp +++ b/examples/send.cpp @@ -22,12 +22,14 @@ int main(int argc, char* argv[]) std::vector wrong; + auto istarget = match::prefix_not("-"); + auto cli = ( value("file") .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(match::prefix_not("-"), "target") + required("-t") & values(istarget, "target") .if_missing([]{ cout << "You need to provide at least one target filename!\n"; } ) .if_blocked([]{ cout << "Target names must not be given before the file command and the source file name!\n"; }) , diff --git a/examples/timing.cpp b/examples/timing.cpp new file mode 100644 index 0000000..66a9dd0 --- /dev/null +++ b/examples/timing.cpp @@ -0,0 +1,45 @@ +/***************************************************************************** + * + * demo program - part of CLIPP (command line interfaces for modern C++) + * + * released under MIT license + * + * (c) 2017 André Müller; foss@andremueller-online.de + * + *****************************************************************************/ + +#include +#include +#include + +#include + + +int main(int argc, char* argv[]) +{ + using namespace clipp; + using std::string; + using std::cout; + + int n = 1; + bool errStop = false; + string exe; + std::vector args; + + auto cli = ( + option("-n", "--repeat") & value("times", n) % "execute multiple times", + option("-s", "--stop-on-error").set(errStop) % "stop on error", + value("executable", exe) % "client program", + option("--") & values("args", args) % "client arguments" + ); + + if(!parse(argc, argv, cli)) { + cout << make_man_page(cli, argv[0]) << '\n'; + } + + cout << "call: " << exe; + for(const auto& a : args) cout << ' ' << a; + cout << '\n'; + cout << n << " times\n"; + if(errStop) cout << "execution will be stopped on any error\n"; +} diff --git a/include/clipp.h b/include/clipp.h index 6682d80..eef2b8d 100644 --- a/include/clipp.h +++ b/include/clipp.h @@ -1631,6 +1631,40 @@ alphabetic(const arg_string& s) { +/*************************************************************************//** + * + * @brief predicate that returns false if the argument string is + * equal to any string from the exclusion list + * + *****************************************************************************/ +class none_of +{ +public: + none_of(arg_list strs): + excluded_{std::move(strs)} + {} + + template + none_of(arg_string str, Strings&&... strs): + excluded_{std::move(str), std::forward(strs)...} + {} + + template + none_of(const char* str, Strings&&... strs): + excluded_{arg_string(str), std::forward(strs)...} + {} + + bool operator () (const arg_string& arg) const { + return (std::find(begin(excluded_), end(excluded_), arg) + == end(excluded_)); + } + +private: + arg_list excluded_; +}; + + + /*************************************************************************//** * * @brief predicate that returns the first substring match within the input @@ -1861,7 +1895,7 @@ class parameter : parameter(): flags_{}, matcher_{predicate_adapter{match::none}}, - label_{}, required_{false} + label_{}, required_{false}, greedy_{false} {} /** @brief makes "flag" parameter */ @@ -1870,7 +1904,7 @@ class parameter : parameter(arg_string str, Strings&&... strs): flags_{}, matcher_{predicate_adapter{match::none}}, - label_{}, required_{false} + label_{}, required_{false}, greedy_{false} { add_flags(std::move(str), std::forward(strs)...); } @@ -1880,7 +1914,7 @@ class parameter : parameter(const arg_list& flaglist): flags_{}, matcher_{predicate_adapter{match::none}}, - label_{}, required_{false} + label_{}, required_{false}, greedy_{false} { add_flags(flaglist); } @@ -1893,7 +1927,7 @@ class parameter : parameter(match_predicate filter): flags_{}, matcher_{predicate_adapter{std::move(filter)}}, - label_{}, required_{false} + label_{}, required_{false}, greedy_{false} {} /** @brief makes "value" parameter with custom match function @@ -1903,7 +1937,7 @@ class parameter : parameter(match_function filter): flags_{}, matcher_{std::move(filter)}, - label_{}, required_{false} + label_{}, required_{false}, greedy_{false} {} @@ -1922,6 +1956,21 @@ class parameter : } + //--------------------------------------------------------------- + /** @brief returns if a parameter should match greedily */ + bool + greedy() const noexcept { + return greedy_; + } + + /** @brief determines if a parameter should match greedily */ + parameter& + greedy(bool yes) noexcept { + greedy_ = yes; + return *this; + } + + //--------------------------------------------------------------- /** @brief returns parameter label; * will be used for documentation, if flags are empty @@ -2046,6 +2095,7 @@ class parameter : match_function matcher_; doc_string label_; bool required_ = false; + bool greedy_ = false; }; @@ -3391,6 +3441,39 @@ using pattern = group::child; +/*************************************************************************//** + * + * @brief apply an action to all parameters in a group + * + *****************************************************************************/ +template +void for_all_params(group& g, Action&& action) +{ + for(auto& p : g) { + if(p.is_group()) { + for_all_params(p.as_group(), action); + } + else { + action(p.as_param()); + } + } +} + +template +void for_all_params(const group& g, Action&& action) +{ + for(auto& p : g) { + if(p.is_group()) { + for_all_params(p.as_group(), action); + } + else { + action(p.as_param()); + } + } +} + + + /*************************************************************************//** * * @brief makes a group of parameters and/or groups @@ -3658,6 +3741,23 @@ repeatable(group p1, P2 p2, Ps... ps) +/*************************************************************************//** + * + * @brief makes a parameter greedy (match with top priority) + * + *****************************************************************************/ +inline parameter +greedy(parameter p) { + return p.greedy(true); +} + +inline parameter +operator ! (parameter p) { + return greedy(p); +} + + + /*************************************************************************//** * * @brief recursively prepends a prefix to all flags @@ -4416,12 +4516,23 @@ class parser /** @brief try to find a parameter/pattern that matches 'arg' */ bool try_match(const arg_string& arg) { - //Note: flag-params will take precedence over value-params + //match greedy parameters before everything else + if(pos_->is_param() && pos_->blocking() && pos_->as_param().greedy()) { + const auto match = pos_->as_param().match(arg); + if(match && match.length() == arg.size()) { + add_match(detail::match_t{arg,pos_}); + return true; + } + } + + //try flags first (alone, joinable or strict sequence) if(try_match_full(arg, detail::select_flags{})) return true; if(try_match_joined_flags(arg)) return true; if(try_match_joined_sequence(arg, detail::select_flags{})) return true; + //try value params (alone or strict sequence) if(try_match_full(arg, detail::select_values{})) return true; if(try_match_joined_sequence(arg, detail::select_all{})) return true; + //try joinable params + values in any order if(try_match_joined_params(arg)) return true; return false; }