From 418b7175f5223aa28ab65744c76132fd05fd8f39 Mon Sep 17 00:00:00 2001 From: Philip Top Date: Sat, 9 Nov 2019 11:06:16 -0800 Subject: [PATCH] Type size refactor (#325) * add expanded type_size specification * add some more checks for type_size_max * continued work on getting type sizes more flexible * make some more tweaks to option to split up validate and reduce sections * git rid of exceptions on the type_size functions exceptions, allow any number to be entered for the min and max and don't make a distinction between flags and other types. * add expected count * add the allow extra args flag in an option * start working in allow_extra_args * write some stuff in the book, and continue working on the failing test cases * fix a few more of the helpers tests * a few more test cases running * all tests pass, fixing calls in ini files * get vector working and all tests passing * change callback to use reference to remove allocation and copy operation * add support and test for vector> * change Validators_ to validators_ for consistency * fix linux warnings and errors by reording some templates and adding some typename keywords * add support for std::vector as the cross conversion type so optional> is supported using the full template of add_option. * a few more test cases to take care of some coverage gaps * add missing parenthesis * add some more tests for coverage gaps * add test for flag like option * add transform test for `as` function and make it pass through the defaults * add a few more tests and have vector default string interpreted correctly. * add test for defaulted integer, and route default string for defaulted value which would otherwise be empty * some code cleanup and comments and few more test coverage gap tests * add more tests and fix a few bugs on the type size and different code paths * remove path in results by fixing the clear of options so they go back to parsing state. * get coverage back to 100% * clang_tidy, and codacy fixes * reorder the lexical_conversion definitions * update some formatting * update whitespace on book chapter --- book/chapters/options.md | 125 +++++--- include/CLI/App.hpp | 225 +++++++------- include/CLI/Config.hpp | 4 +- include/CLI/Error.hpp | 9 +- include/CLI/Formatter.hpp | 15 +- include/CLI/Option.hpp | 577 +++++++++++++++++++++++------------- include/CLI/StringTools.hpp | 4 +- include/CLI/TypeTools.hpp | 165 +++++++++-- include/CLI/Validators.hpp | 10 +- tests/AppTest.cpp | 297 ++++++++++++++++++- tests/CreationTest.cpp | 40 --- tests/DeprecatedTest.cpp | 2 +- tests/HelpersTest.cpp | 39 ++- tests/NewParseTest.cpp | 72 ++++- tests/OptionalTest.cpp | 32 ++ tests/TransformTest.cpp | 18 ++ tests/app_helper.hpp | 4 +- 17 files changed, 1183 insertions(+), 455 deletions(-) diff --git a/book/chapters/options.md b/book/chapters/options.md index 11295980..70b84c67 100644 --- a/book/chapters/options.md +++ b/book/chapters/options.md @@ -72,23 +72,26 @@ When you call `add_option`, you get a pointer to the added option. You can use t | Modifier | Description | |----------|-------------| -| `->required()`| The program will quit if this option is not present. This is `mandatory` in Plumbum, but required options seems to be a more standard term. For compatibility, `->mandatory()` also works. | -| `->expected(N)`| Take `N` values instead of as many as possible, only for vector args.| -| `->needs(opt)`| This option requires another option to also be present, opt is an `Option` pointer.| -| `->excludes(opt)`| This option cannot be given with `opt` present, opt is an `Option` pointer.| -| `->envname(name)`| Gets the value from the environment if present and not passed on the command line.| -| `->group(name)`| The help group to put the option in. No effect for positional options. Defaults to `"Options"`. `"Hidden"` will not show up in the help print.| -| `->ignore_case()`| Ignore the case on the command line (also works on subcommands, does not affect arguments).| -| `->ignore_underscore()`| Ignore any underscores on the command line (also works on subcommands, does not affect arguments, new in CLI11 1.7).| -| `->multi_option_policy(CLI::MultiOptionPolicy::Throw)` | Sets the policy if 1 argument expected but this was received on the command line several times. `Throw`ing an error is the default, but `TakeLast`, `TakeFirst`, and `Join` are also available. See the next three lines for shortcuts to set this more easily. | -| `->take_last()` | Only use the last option if passed several times. This is always true by default for bool options, regardless of the app default, but can be set to false explicitly with `->multi_option_policy()`.| +| `->required()` | The program will quit if this option is not present. This is `mandatory` in Plumbum, but required options seems to be a more standard term. For compatibility, `->mandatory()` also works. | +| `->expected(N)` | Take `N` values instead of as many as possible, mainly for vector args. | +| `->expected(Nmin,Nmax)` | Take between `Nmin` and `Nmax` values. | +| `->needs(opt)` | This option requires another option to also be present, opt is an `Option` pointer. | +| `->excludes(opt)` | This option cannot be given with `opt` present, opt is an `Option` pointer. | +| `->envname(name)` | Gets the value from the environment if present and not passed on the command line. | +| `->group(name)` | The help group to put the option in. No effect for positional options. Defaults to `"Options"`. `"Hidden"` will not show up in the help print. | +| `->ignore_case()` | Ignore the case on the command line (also works on subcommands, does not affect arguments). | +| `->ignore_underscore()` | Ignore any underscores on the command line (also works on subcommands, does not affect arguments, new in CLI11 1.7). | +| `->allow_extra_args()` | Allow extra argument values to be included when an option is passed. Enabled by default for vector options. | +| `->multi_option_policy(CLI::MultiOptionPolicy::Throw)` | Sets the policy for handling multiple arguments if the option was received on the command line several times. `Throw`ing an error is the default, but `TakeLast`, `TakeFirst`, `TakeAll`, and `Join` are also available. See the next three lines for shortcuts to set this more easily. | +| `->take_last()` | Only use the last option if passed several times. This is always true by default for bool options, regardless of the app default, but can be set to false explicitly with `->multi_option_policy()`. | | `->take_first()` | sets `->multi_option_policy(CLI::MultiOptionPolicy::TakeFirst)` | -| `->join()` | sets `->multi_option_policy(CLI::MultiOptionPolicy::Join)`, which uses newlines to join all arguments into a single string output. | -| `->check(CLI::ExistingFile)`| Requires that the file exists if given.| -| `->check(CLI::ExistingDirectory)`| Requires that the directory exists.| -| `->check(CLI::NonexistentPath)`| Requires that the path does not exist.| -| `->check(CLI::Range(min,max))`| Requires that the option be between min and max (make sure to use floating point if needed). Min defaults to 0.| -| `->each(void(std::string))` | Run a function on each parsed value, *in order*.| +| `->join()` | sets `->multi_option_policy(CLI::MultiOptionPolicy::Join)`, which uses newlines or the specified delimiter to join all arguments into a single string output. | +| `->join(delim)` | sets `->multi_option_policy(CLI::MultiOptionPolicy::Join)`, which uses `delim` to join all arguments into a single string output. | +| `->check(CLI::ExistingFile)` | Requires that the file exists if given. | +| `->check(CLI::ExistingDirectory)` | Requires that the directory exists. | +| `->check(CLI::NonexistentPath)` | Requires that the path does not exist. | +| `->check(CLI::Range(min,max))` | Requires that the option be between min and max (make sure to use floating point if needed). Min defaults to 0. | +| `->each(void(std::string))` | Run a function on each parsed value, *in order*. | The `->check(...)` modifiers adds a callback function of the form `bool function(std::string)` that runs on every value that the option receives, and returns a value that tells CLI11 whether the check passed or failed. @@ -101,7 +104,7 @@ CLI::Option* opt = app.add_flag("--opt"); CLI11_PARSE(app, argv, argc); -if(*opt) +if(* opt) std::cout << "Flag recieved " << opt->count() << " times." << std::endl; ``` @@ -109,10 +112,10 @@ if(*opt) One of CLI11's systems to allow customizability without high levels of verbosity is the inheritance system. You can set default values on the parent `App`, and all options and subcommands created from it remember the default values at the point of creation. The default value for Options, specifically, are accessible through the `option_defaults()` method. There are four settings that can be set and inherited: -* `group`: The group name starts as "Options" -* `required`: If the option must be given. Defaults to `false`. Is ignored for flags. -* `multi_option_policy`: What to do if several copies of an option are passed and one value is expected. Defaults to `CLI::MultiOptionPolicy::Throw`. This is also used for bool flags, but they always are created with the value `CLI::MultiOptionPolicy::TakeLast` regardless of the default, so that multiple bool flags does not cause an error. But you can override that flag by flag. -* `ignore_case`: Allow any mixture of cases for the option or flag name +* `group`: The group name starts as "Options" +* `required`: If the option must be given. Defaults to `false`. Is ignored for flags. +* `multi_option_policy`: What to do if several copies of an option are passed and one value is expected. Defaults to `CLI::MultiOptionPolicy::Throw`. This is also used for bool flags, but they always are created with the value `CLI::MultiOptionPolicy::TakeLast` regardless of the default, so that multiple bool flags does not cause an error. But you can override that flag by flag. +* `ignore_case`: Allow any mixture of cases for the option or flag name An example of usage: @@ -148,36 +151,68 @@ std::complex val; app.add_complex("--cplx", val); ``` -### Optionals (New in CLI11 1.5) - -If you have a compiler with `__has_include`, you can use `std::optional`, `std::experimental::optional`, and `boost::optional` in `add_option`. You can manually enforce support for one of these by defining the corresponding macro before including CLI11 (or in your build system). For example: - -```cpp -#define CLI11_BOOST_OPTIONAL -#include - -... - -boost::optional x; -app.add_option("-x", x); - -CLI11_PARSE(app, argc, argv); - -if(x) - std::cout << *x << std::endl; -``` - ### Windows style options (New in CLI11 1.7) You can also set the app setting `app->allow_windows_style_options()` to allow windows style options to also be recognized on the command line: -* `/a` (flag) -* `/f filename` (option) -* `/long` (long flag) -* `/file filename` (space) -* `/file:filename` (colon) +* `/a` (flag) +* `/f filename` (option) +* `/long` (long flag) +* `/file filename` (space) +* `/file:filename` (colon) Windows style options do not allow combining short options or values not separated from the short option like with `-` options. You still specify option names in the same manor as on Linux with single and double dashes when you use the `add_*` functions, and the Linux style on the command line will still work. If a long and a short option share the same name, the option will match on the first one defined. +## Parse configuration + +How an option and its arguments are parsed depends on a set of controls that are part of the option structure. In most circumstances these controls are set automatically based on the function used to create the option and the type the arguments are parsed into. The variables define the size of the underlying type (essentially how many strings make up the type), the expected size (how many groups are expected) and a flag indicating if multiple groups are allowed with a single option. And these interact with the `multi_option_policy` when it comes time to parse. + +### examples +How options manage this is best illustrated through some examples +```cpp +std::string val; +app.add_option("--opt",val,"description"); +``` +creates an option that assigns a value to a `std::string` When this option is constructed it sets a type_size of 1. meaning that the assignment uses a single string. The Expected size is also set to 1 by default, and `allow_extra_args` is set to false. meaning that each time this option is called 1 argument is expected. This would also be the case if val were a `double`, `int` or any other single argument types. + +now for example +```cpp +std::pair val; +app.add_option("--opt",val,"description"); +``` + +In this case the typesize is automatically detected to be 2 instead of 1, so the parsing would expect 2 arguments associated with the option. + +```cpp +std::vector val; +app.add_option("--opt",val,"description"); +``` + +detects a type size of 1, since the underlying element type is a single string, so the minimum number of strings is 1. But since it is a vector the expected number can be very big. The default for a vector is (1<<30), and the allow_extra_args is set to true. This means that at least 1 argument is expected to follow the option, but arbitrary numbers of arguments may follow. These are checked if they have the form of an option but if not they are added to the argument. + +```cpp +std::vector> val; +app.add_option("--opt",val,"description"); +``` +gets into the complicated cases where the type size is now 3. and the expected max is set to a large number and `allow_extra_args` is set to true. In this case at least 3 arguments are required to follow the option, and subsequent groups must come in groups of three, otherwise an error will result. + +```cpp +bool val; +app.add_flag("--opt",val,"description"); +``` + +Using the add_flag methods for creating options creates an option with an expected size of 0, implying no arguments can be passed. + +### Customization + +The `type_size(N)`, `type_size(Nmin, Nmax)`, `expected(N)`, `expected(Nmin,Nmax)`, and `allow_extra_args()` can be used to customize an option. For example + +```cpp +std::string val; +auto opt=app.add_flag("--opt{vvv}",val,"description"); +opt->expected(0,1); +``` +will create a hybrid option, that can exist on its own in which case the value "vvv" is used or if a value is given that value will be used. + [^1]: For example, enums are not printable to `std::cout`. [^2]: There is a small difference. An combined unlimited option will not prioritize over a positional that could still accept values. diff --git a/include/CLI/App.hpp b/include/CLI/App.hpp index 1c238c26..a5d3ae23 100644 --- a/include/CLI/App.hpp +++ b/include/CLI/App.hpp @@ -554,27 +554,29 @@ class App { // LCOV_EXCL_END } - /// Add option for non-vectors (duplicate copy needed without defaulted to avoid `iostream << value`) - - template ::value, detail::enabler> = detail::dummy> + /// Add option for assigning to a variable + template ::value, detail::enabler> = detail::dummy> Option *add_option(std::string option_name, - T &variable, ///< The variable to set + AssignTo &variable, ///< The variable to set std::string option_description = "", bool defaulted = false) { - auto fun = [&variable](CLI::results_t res) { // comment for spacing - return detail::lexical_conversion(res, variable); + auto fun = [&variable](const CLI::results_t &res) { // comment for spacing + return detail::lexical_conversion(res, variable); }; Option *opt = add_option(option_name, fun, option_description, defaulted, [&variable]() { - return CLI::detail::checked_to_string(variable); + return CLI::detail::checked_to_string(variable); }); - opt->type_name(detail::type_name()); - // these must be actual variable since (std::max) sometimes is defined in terms of references and references + opt->type_name(detail::type_name()); + // these must be actual variables since (std::max) sometimes is defined in terms of references and references // to structs used in the evaluation can be temporary so that would cause issues. - auto Tcount = detail::type_count::value; - auto XCcount = detail::type_count::value; + auto Tcount = detail::type_count::value; + auto XCcount = detail::type_count::value; opt->type_size((std::max)(Tcount, XCcount)); + opt->expected(detail::expected_count::value); return opt; } @@ -584,7 +586,7 @@ class App { const std::function &func, ///< the callback to execute std::string option_description = "") { - auto fun = [func](CLI::results_t res) { + auto fun = [func](const CLI::results_t &res) { T variable; bool result = detail::lexical_conversion(res, variable); if(result) { @@ -596,6 +598,7 @@ class App { Option *opt = add_option(option_name, std::move(fun), option_description, false); opt->type_name(detail::type_name()); opt->type_size(detail::type_count::value); + opt->expected(detail::expected_count::value); return opt; } @@ -667,8 +670,9 @@ class App { remove_option(opt); throw IncorrectConstruction::PositionalFlag(pos_name); } - - opt->type_size(0); + opt->multi_option_policy(MultiOptionPolicy::TakeLast); + opt->expected(0); + opt->required(false); return opt; } @@ -694,7 +698,7 @@ class App { T &flag_count, ///< A variable holding the count std::string flag_description = "") { flag_count = 0; - CLI::callback_t fun = [&flag_count](CLI::results_t res) { + CLI::callback_t fun = [&flag_count](const CLI::results_t &res) { try { detail::sum_flag_vector(res, flag_count); } catch(const std::invalid_argument &) { @@ -702,7 +706,8 @@ class App { } return true; }; - return _add_flag_internal(flag_name, std::move(fun), std::move(flag_description)); + return _add_flag_internal(flag_name, std::move(fun), std::move(flag_description)) + ->multi_option_policy(MultiOptionPolicy::TakeAll); } /// Other type version accepts all other types that are not vectors such as bool, enum, string or other classes @@ -716,15 +721,10 @@ class App { T &flag_result, ///< A variable holding true if passed std::string flag_description = "") { - CLI::callback_t fun = [&flag_result](CLI::results_t res) { - if(res.size() != 1) { - return false; - } + CLI::callback_t fun = [&flag_result](const CLI::results_t &res) { return CLI::detail::lexical_cast(res[0], flag_result); }; - Option *opt = _add_flag_internal(flag_name, std::move(fun), std::move(flag_description)); - opt->multi_option_policy(CLI::MultiOptionPolicy::TakeLast); - return opt; + return _add_flag_internal(flag_name, std::move(fun), std::move(flag_description)); } /// Vector version to capture multiple flags. @@ -733,7 +733,7 @@ class App { Option *add_flag(std::string flag_name, std::vector &flag_results, ///< A vector of values with the flag results std::string flag_description = "") { - CLI::callback_t fun = [&flag_results](CLI::results_t res) { + CLI::callback_t fun = [&flag_results](const CLI::results_t &res) { bool retval = true; for(const auto &elem : res) { flag_results.emplace_back(); @@ -741,7 +741,8 @@ class App { } return retval; }; - return _add_flag_internal(flag_name, std::move(fun), std::move(flag_description)); + return _add_flag_internal(flag_name, std::move(fun), std::move(flag_description)) + ->multi_option_policy(MultiOptionPolicy::TakeAll); } /// Add option for callback that is triggered with a true flag and takes no arguments @@ -749,19 +750,14 @@ class App { std::function function, ///< A function to call, void(void) std::string flag_description = "") { - CLI::callback_t fun = [function](CLI::results_t res) { - if(res.size() != 1) { - return false; - } + CLI::callback_t fun = [function](const CLI::results_t &res) { bool trigger; auto result = CLI::detail::lexical_cast(res[0], trigger); if(trigger) function(); return result; }; - Option *opt = _add_flag_internal(flag_name, std::move(fun), std::move(flag_description)); - opt->multi_option_policy(CLI::MultiOptionPolicy::TakeLast); - return opt; + return _add_flag_internal(flag_name, std::move(fun), std::move(flag_description)); } /// Add option for callback with an integer value @@ -769,13 +765,14 @@ class App { std::function function, ///< A function to call, void(int) std::string flag_description = "") { - CLI::callback_t fun = [function](CLI::results_t res) { + CLI::callback_t fun = [function](const CLI::results_t &res) { int64_t flag_count = 0; detail::sum_flag_vector(res, flag_count); function(flag_count); return true; }; - return _add_flag_internal(flag_name, std::move(fun), std::move(flag_description)); + return _add_flag_internal(flag_name, std::move(fun), std::move(flag_description)) + ->multi_option_policy(MultiOptionPolicy::TakeAll); } #ifdef CLI11_CPP14 @@ -996,34 +993,52 @@ class App { } /// Add a complex number - template + template Option *add_complex(std::string option_name, T &variable, std::string option_description = "", bool defaulted = false, std::string label = "COMPLEX") { - std::string simple_name = CLI::detail::split(option_name, ',').at(0); - CLI::callback_t fun = [&variable, simple_name, label](results_t res) { - if(res[1].back() == 'i') - res[1].pop_back(); - double x, y; - bool worked = detail::lexical_cast(res[0], x) && detail::lexical_cast(res[1], y); + CLI::callback_t fun = [&variable](const results_t &res) { + XC x, y; + bool worked; + if(res.size() >= 2 && !res[1].empty()) { + auto str1 = res[1]; + if(str1.back() == 'i' || str1.back() == 'j') + str1.pop_back(); + worked = detail::lexical_cast(res[0], x) && detail::lexical_cast(str1, y); + } else { + auto str1 = res.front(); + auto nloc = str1.find_last_of('-'); + if(nloc != std::string::npos && nloc > 0) { + worked = detail::lexical_cast(str1.substr(0, nloc), x); + str1 = str1.substr(nloc); + if(str1.back() == 'i' || str1.back() == 'j') + str1.pop_back(); + worked = worked && detail::lexical_cast(str1, y); + } else { + if(str1.back() == 'i' || str1.back() == 'j') { + str1.pop_back(); + worked = detail::lexical_cast(str1, y); + x = XC{0}; + } else { + worked = detail::lexical_cast(str1, x); + y = XC{0}; + } + } + } if(worked) - variable = T(x, y); + variable = T{x, y}; return worked; }; - auto default_function = [&variable]() { - std::stringstream out; - out << variable; - return out.str(); - }; + auto default_function = [&variable]() { return CLI::detail::checked_to_string(variable); }; CLI::Option *opt = add_option(option_name, std::move(fun), std::move(option_description), defaulted, default_function); - opt->type_name(label)->type_size(2); + opt->type_name(label)->type_size(1, 2)->delimiter('+'); return opt; } @@ -1922,14 +1937,22 @@ class App { protected: /// Check the options to make sure there are no conflicts. /// - /// Currently checks to see if multiple positionals exist with -1 args and checks if the min and max options are - /// feasible + /// Currently checks to see if multiple positionals exist with unlimited args and checks if the min and max options + /// are feasible void _validate() const { + // count the number of positional only args auto pcount = std::count_if(std::begin(options_), std::end(options_), [](const Option_p &opt) { - return opt->get_items_expected() < 0 && opt->get_positional(); + return opt->get_items_expected_max() >= detail::expected_max_vector_size && !opt->nonpositional(); }); - if(pcount > 1) - throw InvalidError(name_); + if(pcount > 1) { + auto pcount_req = std::count_if(std::begin(options_), std::end(options_), [](const Option_p &opt) { + return opt->get_items_expected_max() >= detail::expected_max_vector_size && !opt->nonpositional() && + opt->get_required(); + }); + if(pcount - pcount_req > 1) { + throw InvalidError(name_); + } + } size_t nameless_subs{0}; for(const App_p &app : subcommands_) { @@ -2199,15 +2222,9 @@ class App { if(opt->count() != 0) { ++used_options; } - // Required or partially filled - if(opt->get_required() || opt->count() != 0) { - // Make sure enough -N arguments parsed (+N is already handled in parsing function) - if(opt->get_items_expected() < 0 && opt->count() < static_cast(-opt->get_items_expected())) - throw ArgumentMismatch::AtLeast(opt->get_name(), -opt->get_items_expected()); - - // Required but empty - if(opt->get_required() && opt->count() == 0) - throw RequiredError(opt->get_name()); + // Required but empty + if(opt->get_required() && opt->count() == 0) { + throw RequiredError(opt->get_name()); } // Requires for(const Option *opt_req : opt->needs_) @@ -2408,7 +2425,7 @@ class App { if(op->empty()) { // Flag parsing - if(op->get_type_size() == 0) { + if(op->get_expected_min() == 0) { auto res = config_formatter_->to_flag(item); res = op->get_flag_value(item.name, res); @@ -2459,7 +2476,6 @@ class App { positional_only = true; } break; - // LCOV_EXCL_START default: throw HorribleError("unrecognized classifier (you should not see this!)"); @@ -2473,10 +2489,9 @@ class App { size_t retval = 0; for(const Option_p &opt : options_) { if(opt->get_positional() && (!required_only || opt->get_required())) { - if(opt->get_items_expected() > 0 && static_cast(opt->count()) < opt->get_items_expected()) { - retval += static_cast(opt->get_items_expected()) - opt->count(); - } else if(opt->get_required() && opt->get_items_expected() < 0 && opt->count() == 0ul) { - retval += 1; + if(opt->get_items_expected_min() > 0 && + static_cast(opt->count()) < opt->get_items_expected_min()) { + retval += static_cast(opt->get_items_expected_min()) - opt->count(); } } } @@ -2486,9 +2501,9 @@ class App { /// Count the required remaining positional arguments bool _has_remaining_positionals() const { for(const Option_p &opt : options_) - if(opt->get_positional() && - ((opt->get_items_expected() < 0) || ((static_cast(opt->count()) < opt->get_items_expected())))) + if(opt->get_positional() && ((static_cast(opt->count()) < opt->get_items_expected_min()))) { return true; + } return false; } @@ -2507,8 +2522,7 @@ class App { if(arg_rem <= remreq) { for(const Option_p &opt : options_) { if(opt->get_positional() && opt->required_) { - if(static_cast(opt->count()) < opt->get_items_expected() || - (opt->get_items_expected() < 0 && opt->count() == 0lu)) { + if(static_cast(opt->count()) < opt->get_items_expected_min()) { if(validate_positionals_) { std::string pos = positional; pos = opt->_validate(pos, 0); @@ -2528,7 +2542,7 @@ class App { for(const Option_p &opt : options_) { // Eat options, one by one, until done if(opt->get_positional() && - (static_cast(opt->count()) < opt->get_items_expected() || opt->get_items_expected() < 0)) { + (static_cast(opt->count()) < opt->get_items_expected_min() || opt->get_allow_extra_args())) { if(validate_positionals_) { std::string pos = positional; pos = opt->_validate(pos, 0); @@ -2714,13 +2728,14 @@ class App { // Get a reference to the pointer to make syntax bearable Option_p &op = *op_ptr; - int num = op->get_items_expected(); + int min_num = (std::min)(op->get_type_size_min(), op->get_items_expected_min()); + int max_num = op->get_items_expected_max(); // Make sure we always eat the minimum for unlimited vectors - int collected = 0; - int result_count = 0; - // deal with flag like things - if(num == 0) { + int collected = 0; // total number of arguments collected + int result_count = 0; // local variable for number of results in a single arg string + // deal with purely flag like things + if(max_num == 0) { auto res = op->get_flag_value(arg_name, value); op->add_result(res); parse_order_.push_back(op.get()); @@ -2730,31 +2745,37 @@ class App { op->add_result(value, result_count); parse_order_.push_back(op.get()); collected += result_count; - // If exact number expected - if(num > 0) - num = (num >= result_count) ? num - result_count : 0; - // -Trest } else if(!rest.empty()) { op->add_result(rest, result_count); parse_order_.push_back(op.get()); rest = ""; collected += result_count; - // If exact number expected - if(num > 0) - num = (num >= result_count) ? num - result_count : 0; } - // Unlimited vector parser - if(num < 0) { - while(!args.empty() && _recognize(args.back(), false) == detail::Classifier::NONE) { - if(collected >= -num) { - // We could break here for allow extras, but we don't + // gather the minimum number of arguments + while(min_num > collected && !args.empty()) { + std::string current_ = args.back(); + args.pop_back(); + op->add_result(current_, result_count); + parse_order_.push_back(op.get()); + collected += result_count; + } - // If any positionals remain, don't keep eating - if(_count_remaining_positionals() > 0) - break; + if(min_num > collected) { // if we have run out of arguments and the minimum was not met + throw ArgumentMismatch::TypedAtLeast(op->get_name(), min_num, op->get_type_name()); + } + + if(max_num > collected || op->get_allow_extra_args()) { // we allow optional arguments + auto remreqpos = _count_remaining_positionals(true); + // we have met the minimum now optionally check up to the maximum + while((collected < max_num || op->get_allow_extra_args()) && !args.empty() && + _recognize(args.back(), false) == detail::Classifier::NONE) { + // If any required positionals remain, don't keep eating + if(remreqpos >= args.size()) { + break; } + op->add_result(args.back(), result_count); parse_order_.push_back(op.get()); args.pop_back(); @@ -2764,19 +2785,17 @@ class App { // Allow -- to end an unlimited list and "eat" it if(!args.empty() && _recognize(args.back()) == detail::Classifier::POSITIONAL_MARK) args.pop_back(); - - } else { - while(num > 0 && !args.empty()) { - std::string current_ = args.back(); - args.pop_back(); - op->add_result(current_, result_count); + // optional flag that didn't receive anything now get the default value + if(min_num == 0 && max_num > 0 && collected == 0) { + auto res = op->get_flag_value(arg_name, std::string{}); + op->add_result(res); parse_order_.push_back(op.get()); - num -= result_count; } + } - if(num > 0) { - throw ArgumentMismatch::TypedAtLeast(op->get_name(), num, op->get_type_name()); - } + // if we only partially completed a type then add an empty string for later processing + if(min_num > 0 && op->get_type_size_max() != min_num && collected % op->get_type_size_max() != 0) { + op->add_result(std::string{}); } if(!rest.empty()) { diff --git a/include/CLI/Config.hpp b/include/CLI/Config.hpp index d3e0f2b9..011cbe9a 100644 --- a/include/CLI/Config.hpp +++ b/include/CLI/Config.hpp @@ -25,7 +25,7 @@ ConfigINI::to_config(const App *app, bool default_also, bool write_description, std::string value; // Non-flags - if(opt->get_type_size() != 0) { + if(opt->get_expected_min() != 0) { // If the option was found on command line if(opt->count() > 0) @@ -56,7 +56,7 @@ ConfigINI::to_config(const App *app, bool default_also, bool write_description, } // Don't try to quote anything that is not size 1 - if(opt->get_items_expected() != 1) + if(opt->get_items_expected_max() != 1) out << name << "=" << value << std::endl; else out << name << "=" << detail::add_quotes_if_needed(value) << std::endl; diff --git a/include/CLI/Error.hpp b/include/CLI/Error.hpp index 115bd66d..f5596656 100644 --- a/include/CLI/Error.hpp +++ b/include/CLI/Error.hpp @@ -246,8 +246,13 @@ class ArgumentMismatch : public ParseError { ", got " + std::to_string(recieved)), ExitCodes::ArgumentMismatch) {} - static ArgumentMismatch AtLeast(std::string name, int num) { - return ArgumentMismatch(name + ": At least " + std::to_string(num) + " required"); + static ArgumentMismatch AtLeast(std::string name, int num, size_t received) { + return ArgumentMismatch(name + ": At least " + std::to_string(num) + " required but received " + + std::to_string(received)); + } + static ArgumentMismatch AtMost(std::string name, int num, size_t received) { + return ArgumentMismatch(name + ": At Most " + std::to_string(num) + " required but received " + + std::to_string(received)); } static ArgumentMismatch TypedAtLeast(std::string name, int num, std::string type) { return ArgumentMismatch(name + ": " + std::to_string(num) + " required " + type + " missing"); diff --git a/include/CLI/Formatter.hpp b/include/CLI/Formatter.hpp index a5bdd709..02a2d76b 100644 --- a/include/CLI/Formatter.hpp +++ b/include/CLI/Formatter.hpp @@ -234,10 +234,11 @@ inline std::string Formatter::make_option_opts(const Option *opt) const { out << " " << get_label(opt->get_type_name()); if(!opt->get_default_str().empty()) out << "=" << opt->get_default_str(); - if(opt->get_expected() > 1) - out << " x " << opt->get_expected(); - if(opt->get_expected() == -1) + if(opt->get_expected_max() == detail::expected_max_vector_size) out << " ..."; + else if(opt->get_expected_min() > 1) + out << " x " << opt->get_expected(); + if(opt->get_required()) out << " " << get_label("REQUIRED"); } @@ -262,11 +263,11 @@ inline std::string Formatter::make_option_usage(const Option *opt) const { // Note that these are positionals usages std::stringstream out; out << make_option_name(opt, true); - - if(opt->get_expected() > 1) - out << "(" << std::to_string(opt->get_expected()) << "x)"; - else if(opt->get_expected() < 0) + if(opt->get_expected_max() >= detail::expected_max_vector_size) out << "..."; + else if(opt->get_expected_max() > 1) + out << "(" << opt->get_expected() << "x)"; + return opt->get_required() ? out.str() : "[" + out.str() + "]"; } diff --git a/include/CLI/Option.hpp b/include/CLI/Option.hpp index e2123abb..4a3871b8 100644 --- a/include/CLI/Option.hpp +++ b/include/CLI/Option.hpp @@ -21,14 +21,21 @@ namespace CLI { using results_t = std::vector; -using callback_t = std::function; +/// callback function definition +using callback_t = std::function; class Option; class App; using Option_p = std::unique_ptr