1
0
mirror of https://github.com/CLIUtils/CLI11.git synced 2025-04-29 12:13:52 +00:00

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<pair> working and all tests passing

* change callback to use reference to remove allocation and copy operation

* add support and test for vector<vector<X>>

* 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<X> as the cross conversion type so optional<std::vector<X>> 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<X>` 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
This commit is contained in:
Philip Top 2019-11-09 11:06:16 -08:00 committed by Henry Schreiner
parent 41d3c967d7
commit 418b7175f5
17 changed files with 1183 additions and 455 deletions

View File

@ -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<float> 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 <CLI/CLI.hpp>
...
boost::optional<int> 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<int, std::string> 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<int> 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<std::tuple<int, double, std::string>> 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.

View File

@ -554,27 +554,29 @@ class App {
// LCOV_EXCL_END
}
/// Add option for non-vectors (duplicate copy needed without defaulted to avoid `iostream << value`)
template <typename T, typename XC = T, enable_if_t<!std::is_const<XC>::value, detail::enabler> = detail::dummy>
/// Add option for assigning to a variable
template <typename AssignTo,
typename ConvertTo = AssignTo,
enable_if_t<!std::is_const<ConvertTo>::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<T, XC>(res, variable);
auto fun = [&variable](const CLI::results_t &res) { // comment for spacing
return detail::lexical_conversion<AssignTo, ConvertTo>(res, variable);
};
Option *opt = add_option(option_name, fun, option_description, defaulted, [&variable]() {
return CLI::detail::checked_to_string<T, XC>(variable);
return CLI::detail::checked_to_string<AssignTo, ConvertTo>(variable);
});
opt->type_name(detail::type_name<XC>());
// these must be actual variable since (std::max) sometimes is defined in terms of references and references
opt->type_name(detail::type_name<ConvertTo>());
// 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<T>::value;
auto XCcount = detail::type_count<XC>::value;
auto Tcount = detail::type_count<AssignTo>::value;
auto XCcount = detail::type_count<ConvertTo>::value;
opt->type_size((std::max)(Tcount, XCcount));
opt->expected(detail::expected_count<ConvertTo>::value);
return opt;
}
@ -584,7 +586,7 @@ class App {
const std::function<void(const T &)> &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<T, T>(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<T>());
opt->type_size(detail::type_count<T>::value);
opt->expected(detail::expected_count<T>::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<T> &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<void(void)> 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<void(int64_t)> 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 <typename T>
template <typename T, typename XC = double>
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<T, T>(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<size_t>(-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<int>(opt->count()) < opt->get_items_expected()) {
retval += static_cast<size_t>(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<int>(opt->count()) < opt->get_items_expected_min()) {
retval += static_cast<size_t>(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<int>(opt->count()) < opt->get_items_expected()))))
if(opt->get_positional() && ((static_cast<int>(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<int>(opt->count()) < opt->get_items_expected() ||
(opt->get_items_expected() < 0 && opt->count() == 0lu)) {
if(static_cast<int>(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<int>(opt->count()) < opt->get_items_expected() || opt->get_items_expected() < 0)) {
(static_cast<int>(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()) {

View File

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

View File

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

View File

@ -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() + "]";
}

View File

@ -21,14 +21,21 @@
namespace CLI {
using results_t = std::vector<std::string>;
using callback_t = std::function<bool(results_t)>;
/// callback function definition
using callback_t = std::function<bool(const results_t &)>;
class Option;
class App;
using Option_p = std::unique_ptr<Option>;
enum class MultiOptionPolicy : char { Throw, TakeLast, TakeFirst, Join };
/// Enumeration of the multiOption Policy selection
enum class MultiOptionPolicy : char {
Throw, //!< Throw an error if any extra arguments were given
TakeLast, //!< take only the last Expected number of arguments
TakeFirst, //!< take only the first Expected number of arguments
Join, //!< merge all the arguments together into a single string via the delimiter character default('\n')
TakeAll //!< just get all the passed argument regardless
};
/// This is the CRTP base class for Option and OptionDefaults. It was designed this way
/// to share parts of the class; an OptionDefaults can copy to an Option.
@ -60,7 +67,7 @@ template <typename CRTP> class OptionBase {
/// Automatically capture default value
bool always_capture_default_{false};
/// Policy for multiple arguments when `expected_ == 1` (can be set on bool flags, too)
/// Policy for handling multiple arguments beyond the expected Max
MultiOptionPolicy multi_option_policy_{MultiOptionPolicy::Throw};
/// Copy the contents to another similar class (one based on OptionBase)
@ -119,7 +126,7 @@ template <typename CRTP> class OptionBase {
/// The status of configurable
bool get_disable_flag_override() const { return disable_flag_override_; }
/// Get the current delimeter char
/// Get the current delimiter char
char get_delimiter() const { return delimiter_; }
/// Return true if this will automatically capture the default value for help printing
@ -144,13 +151,28 @@ template <typename CRTP> class OptionBase {
return self;
}
/// Set the multi option policy to take last
/// Set the multi option policy to take all arguments
CRTP *take_all() {
auto self = static_cast<CRTP *>(this);
self->multi_option_policy(MultiOptionPolicy::TakeAll);
return self;
}
/// Set the multi option policy to join
CRTP *join() {
auto self = static_cast<CRTP *>(this);
self->multi_option_policy(MultiOptionPolicy::Join);
return self;
}
/// Set the multi option policy to join with a specific delimiter
CRTP *join(char delim) {
auto self = static_cast<CRTP *>(this);
self->delimiter_ = delim;
self->multi_option_policy(MultiOptionPolicy::Join);
return self;
}
/// Allow in a configuration file
CRTP *configurable(bool value = true) {
configurable_ = value;
@ -213,7 +235,7 @@ class Option : public OptionBase<Option> {
/// A list of the short names (`-a`) without the leading dashes
std::vector<std::string> snames_;
/// A list of the long names (`--a`) without the leading dashes
/// A list of the long names (`--long`) without the leading dashes
std::vector<std::string> lnames_;
/// A list of the flag names with the appropriate default value, the first part of the pair should be duplicates of
@ -251,15 +273,18 @@ class Option : public OptionBase<Option> {
/// @name Configuration
///@{
/// The number of arguments that make up one option. -1=unlimited (vector-like), 0=flag, 1=normal option,
/// 2=complex/pair, etc. Set only when the option is created; this is intrinsic to the type. Eventually, -2 may mean
/// vector of pairs.
int type_size_{1};
/// The number of arguments that make up one option. max is the nominal type size, min is the minimum number of
/// strings
int type_size_max_{1};
/// The minimum number of arguments an option should be expecting
int type_size_min_{1};
/// The number of expected values, type_size_ must be < 0. Ignored for flag. N < 0 means at least -N values.
int expected_{1};
/// The minimum number of expected values
int expected_min_{1};
/// The maximum number of expected values
int expected_max_{1};
/// A list of validators to run on each value parsed
/// A list of Validators to run on each value parsed
std::vector<Validator> validators_;
/// A list of options that are required with this option
@ -282,19 +307,27 @@ class Option : public OptionBase<Option> {
/// @name Parsing results
///@{
/// Results of parsing
/// complete Results of parsing
results_t results_;
/// results after reduction
results_t proc_results_;
/// enumeration for the option state machine
enum class option_state {
parsing = 0, //!< The option is currently collecting parsed results
validated = 2, //!< the results have been validated
reduced = 4, //!< a subset of results has been generated
callback_run = 6, //!< the callback has been executed
};
/// Whether the callback has run (needed for INI parsing)
bool callback_run_{false};
option_state current_option_state_{option_state::parsing};
/// Specify that extra args beyond type_size_max should be allowed
bool allow_extra_args_{false};
/// Specify that the option should act like a flag vs regular option
bool flag_like_{false};
///@}
/// Making an option by hand is not defined, it must be made by the App class
Option(std::string option_name,
std::string option_description,
std::function<bool(results_t)> callback,
App *parent)
Option(std::string option_name, std::string option_description, callback_t callback, App *parent)
: description_(std::move(option_description)), parent_(parent), callback_(std::move(callback)) {
std::tie(snames_, lnames_, pname_) = detail::get_names(detail::split_names(option_name));
}
@ -313,39 +346,65 @@ class Option : public OptionBase<Option> {
operator bool() const { return !empty(); }
/// Clear the parsed results (mostly for testing)
void clear() { results_.clear(); }
void clear() {
results_.clear();
current_option_state_ = option_state::parsing;
}
///@}
/// @name Setting options
///@{
/// Set the number of expected arguments (Flags don't use this)
/// Set the number of expected arguments
Option *expected(int value) {
// Break if this is a flag
if(type_size_ == 0)
throw IncorrectConstruction::SetFlag(get_name(true, true));
// Setting 0 is not allowed
if(value == 0)
throw IncorrectConstruction::Set0Opt(get_name());
// No change is okay, quit now
if(expected_ == value)
return this;
// Type must be a vector
if(type_size_ >= 0)
throw IncorrectConstruction::ChangeNotVector(get_name());
// TODO: Can support multioption for non-1 values (except for join)
if(value != 1 && multi_option_policy_ != MultiOptionPolicy::Throw)
throw IncorrectConstruction::AfterMultiOpt(get_name());
expected_ = value;
if(value < 0) {
expected_min_ = -value;
if(expected_max_ < expected_min_) {
expected_max_ = expected_min_;
}
allow_extra_args_ = true;
flag_like_ = false;
} else if(value == detail::expected_max_vector_size) {
expected_min_ = 1;
expected_max_ = detail::expected_max_vector_size;
allow_extra_args_ = true;
flag_like_ = false;
} else {
expected_min_ = value;
expected_max_ = value;
flag_like_ = (expected_min_ == 0);
}
return this;
}
/// Set the range of expected arguments
Option *expected(int value_min, int value_max) {
if(value_min < 0) {
value_min = -value_min;
}
if(value_max < 0) {
value_max = detail::expected_max_vector_size;
}
if(value_max < value_min) {
expected_min_ = value_max;
expected_max_ = value_min;
} else {
expected_max_ = value_max;
expected_min_ = value_min;
}
return this;
}
/// Set the value of allow_extra_args which allows extra value arguments on the flag or option to be included
/// with each instance
Option *allow_extra_args(bool value = true) {
allow_extra_args_ = value;
return this;
}
/// Get the current value of allow extra args
bool get_allow_extra_args() const { return allow_extra_args_; }
/// Adds a Validator with a built in type name
Option *check(Validator validator, std::string validator_name = "") {
validator.non_modifying();
@ -356,23 +415,23 @@ class Option : public OptionBase<Option> {
}
/// Adds a Validator. Takes a const string& and returns an error message (empty if conversion/check is okay).
Option *check(std::function<std::string(const std::string &)> validator,
std::string validator_description = "",
std::string validator_name = "") {
validators_.emplace_back(validator, std::move(validator_description), std::move(validator_name));
Option *check(std::function<std::string(const std::string &)> Validator,
std::string Validator_description = "",
std::string Validator_name = "") {
validators_.emplace_back(Validator, std::move(Validator_description), std::move(Validator_name));
validators_.back().non_modifying();
return this;
}
/// Adds a transforming validator with a built in type name
Option *transform(Validator validator, std::string validator_name = "") {
validators_.insert(validators_.begin(), std::move(validator));
if(!validator_name.empty())
validators_.front().name(validator_name);
/// Adds a transforming Validator with a built in type name
Option *transform(Validator Validator, std::string Validator_name = "") {
validators_.insert(validators_.begin(), std::move(Validator));
if(!Validator_name.empty())
validators_.front().name(Validator_name);
return this;
}
/// Adds a validator-like function that can change result
/// Adds a Validator-like function that can change result
Option *transform(std::function<std::string(std::string)> func,
std::string transform_description = "",
std::string transform_name = "") {
@ -399,16 +458,16 @@ class Option : public OptionBase<Option> {
return this;
}
/// Get a named Validator
Validator *get_validator(const std::string &validator_name = "") {
for(auto &validator : validators_) {
if(validator_name == validator.get_name()) {
return &validator;
Validator *get_validator(const std::string &Validator_name = "") {
for(auto &Validator : validators_) {
if(Validator_name == Validator.get_name()) {
return &Validator;
}
}
if((validator_name.empty()) && (!validators_.empty())) {
if((Validator_name.empty()) && (!validators_.empty())) {
return &(validators_.front());
}
throw OptionNotFound(std::string{"Validator "} + validator_name + " Not Found");
throw OptionNotFound(std::string{"Validator "} + Validator_name + " Not Found");
}
/// Get a Validator by index NOTE: this may not be the order of definition
@ -552,10 +611,15 @@ class Option : public OptionBase<Option> {
/// Take the last argument if given multiple times (or another policy)
Option *multi_option_policy(MultiOptionPolicy value = MultiOptionPolicy::Throw) {
if(get_items_expected() < 0)
throw IncorrectConstruction::MultiOptionPolicy(get_name());
multi_option_policy_ = value;
if(value != multi_option_policy_) {
if(multi_option_policy_ == MultiOptionPolicy::Throw && expected_max_ == detail::expected_max_vector_size &&
expected_min_ > 1) { // this bizarre condition is to maintain backwards compatibility
// with the previous behavior of expected_ with vectors
expected_max_ = expected_min_;
}
multi_option_policy_ = value;
current_option_state_ = option_state::parsing;
}
return this;
}
@ -569,7 +633,12 @@ class Option : public OptionBase<Option> {
///@{
/// The number of arguments the option expects
int get_type_size() const { return type_size_; }
int get_type_size() const { return type_size_min_; }
/// The minimum number of arguments the option expects
int get_type_size_min() const { return type_size_min_; }
/// The maximum number of arguments the option expects
int get_type_size_max() const { return type_size_max_; }
/// The environment variable associated to this value
std::string get_envname() const { return envname_; }
@ -600,28 +669,23 @@ class Option : public OptionBase<Option> {
const std::vector<std::string> get_fnames() const { return fnames_; }
/// The number of times the option expects to be included
int get_expected() const { return expected_; }
int get_expected() const { return expected_min_; }
/// \brief The total number of expected values (including the type)
/// This is positive if exactly this number is expected, and negative for at least N values
///
/// v = fabs(size_type*expected)
/// !MultiOptionPolicy::Throw
/// | Expected < 0 | Expected == 0 | Expected > 0
/// Size < 0 | -v | 0 | -v
/// Size == 0 | 0 | 0 | 0
/// Size > 0 | -v | 0 | -v // Expected must be 1
///
/// MultiOptionPolicy::Throw
/// | Expected < 0 | Expected == 0 | Expected > 0
/// Size < 0 | -v | 0 | v
/// Size == 0 | 0 | 0 | 0
/// Size > 0 | v | 0 | v // Expected must be 1
///
int get_items_expected() const {
return std::abs(type_size_ * expected_) *
((multi_option_policy_ != MultiOptionPolicy::Throw || (expected_ < 0 && type_size_ < 0) ? -1 : 1));
/// The number of times the option expects to be included
int get_expected_min() const { return expected_min_; }
/// The max number of times the option expects to be included
int get_expected_max() const { return expected_max_; }
/// The total min number of expected string values to be used
int get_items_expected_min() const { return type_size_min_ * expected_min_; }
/// Get the maximum number of items expected to be returned and used for the callback
int get_items_expected_max() const {
int t = type_size_max_;
return detail::checked_multiply(t, expected_max_) ? t : detail::expected_max_vector_size;
}
/// The total min number of expected string values to be used
int get_items_expected() const { return get_items_expected_min(); }
/// True if the argument can be given directly
bool get_positional() const { return pname_.length() > 0; }
@ -711,66 +775,26 @@ class Option : public OptionBase<Option> {
/// Process the callback
void run_callback() {
callback_run_ = true;
// Run the validators (can change the string)
if(!validators_.empty()) {
int index = 0;
// this is not available until multi_option_policy with type_size_>0 is enabled and functional
// if(type_size_ > 0 && multi_option_policy_ == CLI::MultiOptionPolicy::TakeLast) {
// index = type_size_ - static_cast<int>(results_.size());
//}
if(type_size_ < 0 && multi_option_policy_ == CLI::MultiOptionPolicy::TakeLast) { // for vector operations
index = expected_ - static_cast<int>(results_.size());
}
for(std::string &result : results_) {
auto err_msg = _validate(result, index);
++index;
if(!err_msg.empty())
throw ValidationError(get_name(), err_msg);
}
}
if(!(callback_)) {
return;
}
bool local_result;
// Num items expected or length of vector, always at least 1
// Only valid for a trimming policy
int trim_size =
std::min<int>(std::max<int>(std::abs(get_items_expected()), 1), static_cast<int>(results_.size()));
// Operation depends on the policy setting
if(multi_option_policy_ == MultiOptionPolicy::TakeLast) {
// Allow multi-option sizes (including 0)
results_t partial_result{results_.end() - trim_size, results_.end()};
local_result = !callback_(partial_result);
} else if(multi_option_policy_ == MultiOptionPolicy::TakeFirst) {
results_t partial_result{results_.begin(), results_.begin() + trim_size};
local_result = !callback_(partial_result);
} else if(multi_option_policy_ == MultiOptionPolicy::Join) {
results_t partial_result = {detail::join(results_, "\n")};
local_result = !callback_(partial_result);
} else {
// Exact number required
if(get_items_expected() > 0) {
if(results_.size() != static_cast<size_t>(get_items_expected()))
throw ArgumentMismatch(get_name(), get_items_expected(), results_.size());
// Variable length list
} else if(get_items_expected() < 0) {
// Require that this be a multiple of expected size and at least as many as expected
if(results_.size() < static_cast<size_t>(-get_items_expected()) ||
results_.size() % static_cast<size_t>(std::abs(get_type_size())) != 0u)
throw ArgumentMismatch(get_name(), get_items_expected(), results_.size());
}
local_result = !callback_(results_);
if(current_option_state_ == option_state::parsing) {
_validate_results(results_);
current_option_state_ = option_state::validated;
}
if(local_result)
throw ConversionError(get_name(), results_);
if(current_option_state_ < option_state::reduced) {
_reduce_results(proc_results_, results_);
current_option_state_ = option_state::reduced;
}
if(current_option_state_ >= option_state::reduced) {
current_option_state_ = option_state::callback_run;
if(!(callback_)) {
return;
}
const results_t &send_results = proc_results_.empty() ? results_ : proc_results_;
bool local_result = callback_(send_results);
if(!local_result)
throw ConversionError(get_name(), results_);
}
}
/// If options share any of the same names, find it
@ -833,7 +857,8 @@ class Option : public OptionBase<Option> {
return (detail::find_member(name, fnames_, ignore_case_, ignore_underscore_) >= 0);
}
/// Get the value that goes for a flag, nominally gets the default value but allows for overrides if not disabled
/// Get the value that goes for a flag, nominally gets the default value but allows for overrides if not
/// disabled
std::string get_flag_value(std::string name, std::string input_value) const {
static const std::string trueString{"true"};
static const std::string falseString{"false"};
@ -856,7 +881,11 @@ class Option : public OptionBase<Option> {
}
auto ind = detail::find_member(name, fnames_, ignore_case_, ignore_underscore_);
if((input_value.empty()) || (input_value == emptyString)) {
return (ind < 0) ? trueString : default_flag_values_[static_cast<size_t>(ind)].second;
if(flag_like_) {
return (ind < 0) ? trueString : default_flag_values_[static_cast<size_t>(ind)].second;
} else {
return (ind < 0) ? default_str_ : default_flag_values_[static_cast<size_t>(ind)].second;
}
}
if(ind < 0) {
return input_value;
@ -875,69 +904,74 @@ class Option : public OptionBase<Option> {
/// Puts a result at the end
Option *add_result(std::string s) {
_add_result(std::move(s));
callback_run_ = false;
_add_result(std::move(s), results_);
current_option_state_ = option_state::parsing;
return this;
}
/// Puts a result at the end and get a count of the number of arguments actually added
Option *add_result(std::string s, int &results_added) {
results_added = _add_result(std::move(s));
callback_run_ = false;
results_added = _add_result(std::move(s), results_);
current_option_state_ = option_state::parsing;
return this;
}
/// Puts a result at the end
Option *add_result(std::vector<std::string> s) {
for(auto &str : s) {
_add_result(std::move(str));
_add_result(std::move(str), results_);
}
callback_run_ = false;
current_option_state_ = option_state::parsing;
return this;
}
/// Get a copy of the results
std::vector<std::string> results() const { return results_; }
results_t results() const { return results_; }
/// Get the results as a specified type
template <typename T,
enable_if_t<!is_vector<T>::value && !std::is_const<T>::value, detail::enabler> = detail::dummy>
void results(T &output) const {
bool retval;
if(results_.empty()) {
retval = detail::lexical_cast(default_str_, output);
} else if(results_.size() == 1) {
retval = detail::lexical_cast(results_[0], output);
} else {
switch(multi_option_policy_) {
case MultiOptionPolicy::TakeFirst:
retval = detail::lexical_cast(results_.front(), output);
break;
case MultiOptionPolicy::TakeLast:
default:
retval = detail::lexical_cast(results_.back(), output);
break;
case MultiOptionPolicy::Throw:
throw ConversionError(get_name(), results_);
case MultiOptionPolicy::Join:
retval = detail::lexical_cast(detail::join(results_), output);
break;
/// Get a copy of the results
results_t reduced_results() const {
results_t res = proc_results_.empty() ? results_ : proc_results_;
if(current_option_state_ < option_state::reduced) {
if(current_option_state_ == option_state::parsing) {
res = results_;
_validate_results(res);
}
results_t extra;
_reduce_results(extra, res);
if(!extra.empty()) {
res = std::move(extra);
}
}
if(!retval) {
throw ConversionError(get_name(), results_);
}
return res;
}
/// Get the results as a vector of the specified type
template <typename T> void results(std::vector<T> &output) const {
output.clear();
bool retval = true;
for(const auto &elem : results_) {
output.emplace_back();
retval &= detail::lexical_cast(elem, output.back());
/// Get the results as a specified type
template <typename T, enable_if_t<!std::is_const<T>::value, detail::enabler> = detail::dummy>
void results(T &output) const {
bool retval;
if(current_option_state_ >= option_state::reduced || (results_.size() == 1 && validators_.empty())) {
const results_t &res = (proc_results_.empty()) ? results_ : proc_results_;
retval = detail::lexical_conversion<T, T>(res, output);
} else {
results_t res;
if(results_.empty()) {
if(!default_str_.empty()) {
//_add_results takes an rvalue only
_add_result(std::string(default_str_), res);
_validate_results(res);
results_t extra;
_reduce_results(extra, res);
if(!extra.empty()) {
res = std::move(extra);
}
} else {
res.emplace_back();
}
} else {
res = reduced_results();
}
retval = detail::lexical_conversion<T, T>(res, output);
}
if(!retval) {
throw ConversionError(get_name(), results_);
}
@ -951,7 +985,7 @@ class Option : public OptionBase<Option> {
}
/// See if the callback has been run already
bool get_callback_run() const { return callback_run_; }
bool get_callback_run() const { return (current_option_state_ == option_state::callback_run); }
///@}
/// @name Custom options
@ -971,11 +1005,40 @@ class Option : public OptionBase<Option> {
/// Set a custom option size
Option *type_size(int option_type_size) {
type_size_ = option_type_size;
if(type_size_ == 0)
if(option_type_size < 0) {
// this section is included for backwards compatibility
type_size_max_ = -option_type_size;
type_size_min_ = -option_type_size;
expected_max_ = detail::expected_max_vector_size;
} else {
type_size_max_ = option_type_size;
if(type_size_max_ < detail::expected_max_vector_size) {
type_size_min_ = option_type_size;
}
if(type_size_max_ == 0)
required_ = false;
}
return this;
}
/// Set a custom option type size range
Option *type_size(int option_type_size_min, int option_type_size_max) {
if(option_type_size_min < 0 || option_type_size_max < 0) {
// this section is included for backwards compatibility
expected_max_ = detail::expected_max_vector_size;
option_type_size_min = (std::abs)(option_type_size_min);
option_type_size_max = (std::abs)(option_type_size_max);
}
if(option_type_size_min > option_type_size_max) {
type_size_max_ = option_type_size_min;
type_size_min_ = option_type_size_max;
} else {
type_size_min_ = option_type_size_min;
type_size_max_ = option_type_size_max;
}
if(type_size_max_ == 0) {
required_ = false;
if(option_type_size < 0)
expected_ = -1;
}
return this;
}
@ -1003,7 +1066,8 @@ class Option : public OptionBase<Option> {
Option *default_val(std::string val) {
default_str(val);
auto old_results = results_;
results_ = {val};
results_.clear();
add_result(val);
run_callback();
results_ = std::move(old_results);
return this;
@ -1013,8 +1077,8 @@ class Option : public OptionBase<Option> {
std::string get_type_name() const {
std::string full_type_name = type_name_();
if(!validators_.empty()) {
for(auto &validator : validators_) {
std::string vtype = validator.get_description();
for(auto &Validator : validators_) {
std::string vtype = Validator.get_description();
if(!vtype.empty()) {
full_type_name += ":" + vtype;
}
@ -1024,9 +1088,106 @@ class Option : public OptionBase<Option> {
}
private:
// Run a result through the validators
std::string _validate(std::string &result, int index) {
/// Run the results through the Validators
void _validate_results(results_t &res) const {
// Run the Validators (can change the string)
if(!validators_.empty()) {
if(type_size_max_ > 1) { // in this context index refers to the index in the type
int index = 0;
if(get_items_expected_max() < static_cast<int>(res.size()) &&
multi_option_policy_ == CLI::MultiOptionPolicy::TakeLast) {
// create a negative index for the earliest ones
index = get_items_expected_max() - static_cast<int>(res.size());
}
for(std::string &result : res) {
if(result.empty() && type_size_max_ != type_size_min_ && index >= 0) {
index = 0; // reset index for variable size chunks
continue;
}
auto err_msg = _validate(result, (index >= 0) ? (index % type_size_max_) : index);
if(!err_msg.empty())
throw ValidationError(get_name(), err_msg);
++index;
}
} else {
int index = 0;
if(expected_max_ < static_cast<int>(res.size()) &&
multi_option_policy_ == CLI::MultiOptionPolicy::TakeLast) {
// create a negative index for the earliest ones
index = expected_max_ - static_cast<int>(res.size());
}
for(std::string &result : res) {
auto err_msg = _validate(result, index);
++index;
if(!err_msg.empty())
throw ValidationError(get_name(), err_msg);
}
}
}
}
/** reduce the results in accordance with the MultiOptionPolicy
@param[out] res results are assigned to res if there if they are different
*/
void _reduce_results(results_t &res, const results_t &original) const {
// max num items expected or length of vector, always at least 1
// Only valid for a trimming policy
res.clear();
// Operation depends on the policy setting
switch(multi_option_policy_) {
case MultiOptionPolicy::TakeAll:
break;
case MultiOptionPolicy::TakeLast:
// Allow multi-option sizes (including 0)
{
size_t trim_size = std::min<size_t>(std::max<size_t>(get_items_expected_max(), 1), original.size());
if(original.size() != trim_size) {
res.assign(original.end() - trim_size, original.end());
}
}
break;
case MultiOptionPolicy::TakeFirst: {
size_t trim_size = std::min<size_t>(std::max<size_t>(get_items_expected_max(), 1), original.size());
if(original.size() != trim_size) {
res.assign(original.begin(), original.begin() + trim_size);
}
} break;
case MultiOptionPolicy::Join:
if(results_.size() > 1) {
res.push_back(detail::join(original, std::string(1, (delimiter_ == '\0') ? '\n' : delimiter_)));
}
break;
case MultiOptionPolicy::Throw:
default: {
auto num_min = static_cast<size_t>(get_items_expected_min());
auto num_max = static_cast<size_t>(get_items_expected_max());
if(num_min == 0) {
num_min = 1;
}
if(num_max == 0) {
num_max = 1;
}
if(original.size() < num_min) {
throw ArgumentMismatch::AtLeast(get_name(), static_cast<int>(num_min), original.size());
}
if(original.size() > num_max) {
throw ArgumentMismatch::AtMost(get_name(), static_cast<int>(num_max), original.size());
}
break;
}
}
}
// Run a result through the Validators
std::string _validate(std::string &result, int index) const {
std::string err_msg;
if(result.empty() && expected_min_ == 0) {
// an empty with nothing expected is allowed
return err_msg;
}
for(const auto &vali : validators_) {
auto v = vali.get_application_index();
if(v == -1 || v == index) {
@ -1039,25 +1200,37 @@ class Option : public OptionBase<Option> {
break;
}
}
return err_msg;
}
/// Add a single result to the result set, taking into account delimiters
int _add_result(std::string &&result) {
int _add_result(std::string &&result, std::vector<std::string> &res) const {
int result_count = 0;
if(allow_extra_args_ && !result.empty() && result.front() == '[' &&
result.back() == ']') { // this is now a vector string likely from the default or user entry
result.pop_back();
for(auto &var : CLI::detail::split(result.substr(1), ',')) {
if(!var.empty()) {
result_count += _add_result(std::move(var), res);
}
}
return result_count;
}
if(delimiter_ == '\0') {
results_.push_back(std::move(result));
res.push_back(std::move(result));
++result_count;
} else {
if((result.find_first_of(delimiter_) != std::string::npos)) {
for(const auto &var : CLI::detail::split(result, delimiter_)) {
if(!var.empty()) {
results_.push_back(var);
res.push_back(var);
++result_count;
}
}
} else {
results_.push_back(std::move(result));
res.push_back(std::move(result));
++result_count;
}
}

View File

@ -31,7 +31,9 @@ std::ostream &operator<<(std::ostream &in, const T &item) {
using namespace enums;
namespace detail {
/// a constant defining an expected max vector size defined to be a big number that could be multiplied by 4 and not
/// produce overflow for some expected uses
constexpr int expected_max_vector_size{1 << 29};
// Based on http://stackoverflow.com/questions/236129/split-a-string-in-c
/// Split a string by a delim
inline std::vector<std::string> split(const std::string &s, char delim) {

View File

@ -274,9 +274,24 @@ struct type_count<
typename std::enable_if<!is_vector<T>::value && !is_tuple_like<T>::value && !std::is_void<T>::value>::type> {
static constexpr int value{1};
};
/// Type size of types that look like a vector
template <typename T> struct type_count<T, typename std::enable_if<is_vector<T>::value>::type> {
static constexpr int value{-1};
static constexpr int value{is_vector<typename T::value_type>::value ? expected_max_vector_size
: type_count<typename T::value_type>::value};
};
/// This will only trigger for actual void type
template <typename T, typename Enable = void> struct expected_count { static const int value{0}; };
/// For most types the number of expected items is 1
template <typename T>
struct expected_count<T, typename std::enable_if<!is_vector<T>::value && !std::is_void<T>::value>::type> {
static constexpr int value{1};
};
/// number of expected items in a vector
template <typename T> struct expected_count<T, typename std::enable_if<is_vector<T>::value>::type> {
static constexpr int value{expected_max_vector_size};
};
// Enumeration of the different supported categorizations of objects
@ -392,11 +407,11 @@ struct classify_object<T,
/// Tuple type
template <typename T>
struct classify_object<
T,
typename std::enable_if<type_count<T>::value >= 2 || (is_tuple_like<T>::value && uncommon_type<T>::value &&
!is_direct_constructible<T, double>::value &&
!is_direct_constructible<T, int>::value)>::type> {
struct classify_object<T,
typename std::enable_if<(type_count<T>::value >= 2 && !is_vector<T>::value) ||
(is_tuple_like<T>::value && uncommon_type<T>::value &&
!is_direct_constructible<T, double>::value &&
!is_direct_constructible<T, int>::value)>::type> {
static constexpr objCategory value{tuple_value};
};
@ -450,17 +465,11 @@ constexpr const char *type_name() {
return "TEXT";
}
/// This one should not be used normally, since vector types print the internal type
template <typename T, enable_if_t<classify_object<T>::value == vector_value, detail::enabler> = detail::dummy>
constexpr const char *type_name() {
return type_name<typename T::value_type>();
}
/// Print name for single element tuple types
template <
typename T,
enable_if_t<classify_object<T>::value == tuple_value && type_count<T>::value == 1, detail::enabler> = detail::dummy>
std::string type_name() {
inline std::string type_name() {
return type_name<typename std::tuple_element<0, T>::type>();
}
@ -489,6 +498,12 @@ std::string type_name() {
return tname;
}
/// This one should not be used normally, since vector types print the internal type
template <typename T, enable_if_t<classify_object<T>::value == vector_value, detail::enabler> = detail::dummy>
inline std::string type_name() {
return type_name<typename T::value_type>();
}
// Lexical cast
/// Convert a flag into an integer value typically binary flags
@ -673,19 +688,37 @@ bool lexical_cast(const std::string &input, T &output) {
}
/// Assign a value through lexical cast operations
template <typename T, typename XC, enable_if_t<std::is_same<T, XC>::value, detail::enabler> = detail::dummy>
template <typename T,
typename XC,
enable_if_t<std::is_same<T, XC>::value && (classify_object<T>::value == string_assignable ||
classify_object<T>::value == string_constructible),
detail::enabler> = detail::dummy>
bool lexical_assign(const std::string &input, T &output) {
return lexical_cast(input, output);
}
/// Assign a value through lexical cast operations
template <typename T,
typename XC,
enable_if_t<std::is_same<T, XC>::value && classify_object<T>::value != string_assignable &&
classify_object<T>::value != string_constructible,
detail::enabler> = detail::dummy>
bool lexical_assign(const std::string &input, T &output) {
if(input.empty()) {
output = T{};
return true;
}
return lexical_cast(input, output);
}
/// Assign a value converted from a string in lexical cast to the output value directly
template <
typename T,
typename XC,
enable_if_t<!std::is_same<T, XC>::value && std::is_assignable<T &, XC &>::value, detail::enabler> = detail::dummy>
bool lexical_assign(const std::string &input, T &output) {
XC val;
bool parse_result = lexical_cast<XC>(input, val);
XC val{};
bool parse_result = (!input.empty()) ? lexical_cast<XC>(input, val) : true;
if(parse_result) {
output = val;
}
@ -699,18 +732,19 @@ template <typename T,
std::is_move_assignable<T>::value,
detail::enabler> = detail::dummy>
bool lexical_assign(const std::string &input, T &output) {
XC val;
bool parse_result = lexical_cast<XC>(input, val);
XC val{};
bool parse_result = input.empty() ? true : lexical_cast<XC>(input, val);
if(parse_result) {
output = T(val); // use () form of constructor to allow some implicit conversions
}
return parse_result;
}
/// Lexical conversion if there is only one element
template <typename T,
typename XC,
enable_if_t<!is_tuple_like<T>::value && !is_tuple_like<XC>::value && !is_vector<T>::value, detail::enabler> =
detail::dummy>
template <
typename T,
typename XC,
enable_if_t<!is_tuple_like<T>::value && !is_tuple_like<XC>::value && !is_vector<T>::value && !is_vector<XC>::value,
detail::enabler> = detail::dummy>
bool lexical_conversion(const std::vector<std ::string> &strings, T &output) {
return lexical_assign<T, XC>(strings[0], output);
}
@ -722,9 +756,9 @@ template <typename T,
bool lexical_conversion(const std::vector<std ::string> &strings, T &output) {
typename std::tuple_element<0, XC>::type v1;
typename std::tuple_element<1, XC>::type v2;
bool retval = lexical_cast(strings[0], v1);
bool retval = lexical_assign<decltype(v1), decltype(v1)>(strings[0], v1);
if(strings.size() > 1) {
retval &= lexical_cast(strings[1], v2);
retval &= lexical_assign<decltype(v2), decltype(v2)>(strings[1], v2);
}
if(retval) {
output = T{v1, v2};
@ -735,7 +769,9 @@ bool lexical_conversion(const std::vector<std ::string> &strings, T &output) {
/// Lexical conversion of a vector types
template <class T,
class XC,
enable_if_t<type_count<T>::value == -1 && type_count<XC>::value == -1, detail::enabler> = detail::dummy>
enable_if_t<expected_count<T>::value == expected_max_vector_size &&
expected_count<XC>::value == expected_max_vector_size && type_count<XC>::value == 1,
detail::enabler> = detail::dummy>
bool lexical_conversion(const std::vector<std ::string> &strings, T &output) {
bool retval = true;
output.clear();
@ -748,10 +784,38 @@ bool lexical_conversion(const std::vector<std ::string> &strings, T &output) {
return (!output.empty()) && retval;
}
/// Lexical conversion of a vector types with type size of two
template <class T,
class XC,
enable_if_t<expected_count<T>::value == expected_max_vector_size &&
expected_count<XC>::value == expected_max_vector_size && type_count<XC>::value == 2,
detail::enabler> = detail::dummy>
bool lexical_conversion(const std::vector<std ::string> &strings, T &output) {
bool retval = true;
output.clear();
for(size_t ii = 0; ii < strings.size(); ii += 2) {
typename std::tuple_element<0, typename XC::value_type>::type v1;
typename std::tuple_element<1, typename XC::value_type>::type v2;
retval = lexical_assign<decltype(v1), decltype(v1)>(strings[ii], v1);
if(strings.size() > ii + 1) {
retval &= lexical_assign<decltype(v2), decltype(v2)>(strings[ii + 1], v2);
}
if(retval) {
output.emplace_back(v1, v2);
} else {
return false;
}
}
return (!output.empty()) && retval;
}
/// Conversion to a vector type using a particular single type as the conversion type
template <class T,
class XC,
enable_if_t<(type_count<T>::value == -1) && (type_count<XC>::value == 1), detail::enabler> = detail::dummy>
enable_if_t<(expected_count<T>::value == expected_max_vector_size) && (expected_count<XC>::value == 1) &&
(type_count<XC>::value == 1),
detail::enabler> = detail::dummy>
bool lexical_conversion(const std::vector<std ::string> &strings, T &output) {
bool retval = true;
output.clear();
@ -763,6 +827,23 @@ bool lexical_conversion(const std::vector<std ::string> &strings, T &output) {
}
return (!output.empty()) && retval;
}
// This one is last since it can call other lexical_conversion functions
/// Lexical conversion if there is only one element but the conversion type is a vector
template <typename T,
typename XC,
enable_if_t<!is_tuple_like<T>::value && !is_vector<T>::value && is_vector<XC>::value, detail::enabler> =
detail::dummy>
bool lexical_conversion(const std::vector<std ::string> &strings, T &output) {
if(strings.size() > 1 || (!strings.empty() && !(strings.front().empty()))) {
XC val;
auto retval = lexical_conversion<XC, XC>(strings, val);
output = T{val};
return retval;
}
output = T{};
return true;
}
/// function template for converting tuples if the static Index is greater than the tuple size
template <class T, class XC, std::size_t I>
@ -794,6 +875,38 @@ bool lexical_conversion(const std::vector<std ::string> &strings, T &output) {
return tuple_conversion<T, XC, 0>(strings, output);
}
/// Lexical conversion of a vector types with type_size >2
template <class T,
class XC,
enable_if_t<expected_count<T>::value == expected_max_vector_size &&
expected_count<XC>::value == expected_max_vector_size && (type_count<XC>::value > 2),
detail::enabler> = detail::dummy>
bool lexical_conversion(const std::vector<std ::string> &strings, T &output) {
bool retval = true;
output.clear();
std::vector<std::string> temp;
size_t ii = 0;
size_t icount = 0;
size_t xcm = type_count<XC>::value;
while(ii < strings.size()) {
temp.push_back(strings[ii]);
++ii;
++icount;
if(icount == xcm || temp.back().empty()) {
if(static_cast<int>(xcm) == expected_max_vector_size) {
temp.pop_back();
}
output.emplace_back();
retval = retval && lexical_conversion<typename T::value_type, typename XC::value_type>(temp, output.back());
temp.clear();
if(!retval) {
return false;
}
icount = 0;
}
}
return retval;
}
/// Sum a vector of flag representations
/// The flag vector produces a series of strings in a vector, simple true is represented by a "1", simple false is
/// by

View File

@ -325,14 +325,14 @@ class IPV4Validator : public Validator {
func_ = [](std::string &ip_addr) {
auto result = CLI::detail::split(ip_addr, '.');
if(result.size() != 4) {
return "Invalid IPV4 address must have four parts " + ip_addr;
return "Invalid IPV4 address must have four parts (" + ip_addr + ')';
}
int num;
bool retval = true;
for(const auto &var : result) {
retval &= detail::lexical_cast(var, num);
if(!retval) {
return "Failed parsing number " + var;
return "Failed parsing number (" + var + ')';
}
if(num < 0 || num > 255) {
return "Each IP number must be between 0 and 255 " + var;
@ -350,10 +350,10 @@ class PositiveNumber : public Validator {
func_ = [](std::string &number_str) {
int number;
if(!detail::lexical_cast(number_str, number)) {
return "Failed parsing number " + number_str;
return "Failed parsing number: (" + number_str + ')';
}
if(number < 0) {
return "Number less then 0 " + number_str;
return "Number less then 0: (" + number_str + ')';
}
return std::string();
};
@ -367,7 +367,7 @@ class Number : public Validator {
func_ = [](std::string &number_str) {
double number;
if(!detail::lexical_cast(number_str, number)) {
return "Failed parsing as a number " + number_str;
return "Failed parsing as a number (" + number_str + ')';
}
return std::string();
};

View File

@ -40,12 +40,14 @@ TEST_F(TApp, OneFlagShortValuesAs) {
flg->take_last();
EXPECT_EQ(opt->as<int>(), 2);
flg->multi_option_policy(CLI::MultiOptionPolicy::Throw);
EXPECT_THROW(opt->as<int>(), CLI::ConversionError);
EXPECT_THROW(opt->as<int>(), CLI::ArgumentMismatch);
flg->multi_option_policy(CLI::MultiOptionPolicy::TakeAll);
auto vec = opt->as<std::vector<int>>();
EXPECT_EQ(vec[0], 1);
EXPECT_EQ(vec[1], 2);
flg->multi_option_policy(CLI::MultiOptionPolicy::Join);
EXPECT_EQ(opt->as<std::string>(), "1\n2");
flg->delimiter(',');
EXPECT_EQ(opt->as<std::string>(), "1,2");
}
@ -403,6 +405,27 @@ TEST_F(TApp, OneStringEqualVersionSingleStringQuotedMultipleWithEqualAndProgram)
EXPECT_EQ(str4, "Unquoted");
}
TEST_F(TApp, OneStringFlagLike) {
std::string str{"something"};
app.add_option("-s,--string", str)->expected(0, 1);
args = {"--string"};
run();
EXPECT_EQ(1u, app.count("-s"));
EXPECT_EQ(1u, app.count("--string"));
EXPECT_TRUE(str.empty());
}
TEST_F(TApp, OneIntFlagLike) {
int val;
auto opt = app.add_option("-i", val)->expected(0, 1);
args = {"-i"};
run();
EXPECT_EQ(1u, app.count("-i"));
opt->default_str("7");
run();
EXPECT_EQ(val, 7);
}
TEST_F(TApp, TogetherInt) {
int i;
app.add_option("-i,--int", i);
@ -620,6 +643,42 @@ TEST_F(TApp, BoolAndIntFlags) {
EXPECT_EQ((unsigned int)2, uflag);
}
TEST_F(TApp, FlagLikeOption) {
bool val = false;
auto opt = app.add_option("--flag", val)->type_size(0)->default_str("true");
args = {"--flag"};
run();
EXPECT_EQ(1u, app.count("--flag"));
EXPECT_TRUE(val);
val = false;
opt->type_size(0, 0); // should be the same as above
EXPECT_EQ(opt->get_type_size_min(), 0);
EXPECT_EQ(opt->get_type_size_max(), 0);
run();
EXPECT_EQ(1u, app.count("--flag"));
EXPECT_TRUE(val);
}
TEST_F(TApp, FlagLikeIntOption) {
int val = -47;
auto opt = app.add_option("--flag", val)->expected(0, 1);
// normally some default value should be set, but this test is for some paths in the validators checks to skip
// validation on empty string if nothing is expected
opt->check(CLI::PositiveNumber);
args = {"--flag"};
EXPECT_TRUE(opt->as<std::string>().empty());
run();
EXPECT_EQ(1u, app.count("--flag"));
EXPECT_NE(val, -47);
args = {"--flag", "12"};
run();
EXPECT_EQ(val, 12);
args.clear();
run();
EXPECT_TRUE(opt->as<std::string>().empty());
}
TEST_F(TApp, BoolOnlyFlag) {
bool bflag;
app.add_flag("-b", bflag)->multi_option_policy(CLI::MultiOptionPolicy::Throw);
@ -629,7 +688,7 @@ TEST_F(TApp, BoolOnlyFlag) {
EXPECT_TRUE(bflag);
args = {"-b", "-b"};
EXPECT_THROW(run(), CLI::ConversionError);
EXPECT_THROW(run(), CLI::ArgumentMismatch);
}
TEST_F(TApp, BoolOption) {
@ -800,9 +859,51 @@ TEST_F(TApp, TakeLastOptMulti) {
EXPECT_EQ(vals, std::vector<int>({2, 3}));
}
TEST_F(TApp, vectorDefaults) {
std::vector<int> vals{4, 5};
auto opt = app.add_option("--long", vals, "", true);
args = {"--long", "[1,2,3]"};
run();
EXPECT_EQ(vals, std::vector<int>({1, 2, 3}));
args.clear();
run();
auto res = app["--long"]->as<std::vector<int>>();
EXPECT_EQ(res, std::vector<int>({4, 5}));
app.clear();
opt->expected(1)->take_last();
res = app["--long"]->as<std::vector<int>>();
EXPECT_EQ(res, std::vector<int>({5}));
opt->take_first();
res = app["--long"]->as<std::vector<int>>();
EXPECT_EQ(res, std::vector<int>({4}));
opt->expected(0, 1)->take_last();
run();
EXPECT_EQ(res, std::vector<int>({4}));
res = app["--long"]->as<std::vector<int>>();
EXPECT_EQ(res, std::vector<int>({5}));
}
TEST_F(TApp, TakeLastOptMulti_alternative_path) {
std::vector<int> vals;
app.add_option("--long", vals)->expected(2, -1)->take_last();
args = {"--long", "1", "2", "3"};
run();
EXPECT_EQ(vals, std::vector<int>({2, 3}));
}
TEST_F(TApp, TakeLastOptMultiCheck) {
std::vector<int> vals;
auto opt = app.add_option("--long", vals)->expected(2)->take_last();
auto opt = app.add_option("--long", vals)->expected(-2)->take_last();
opt->check(CLI::Validator(CLI::PositiveNumber).application_index(0));
opt->check((!CLI::PositiveNumber).application_index(1));
@ -826,7 +927,7 @@ TEST_F(TApp, TakeFirstOptMulti) {
TEST_F(TApp, ComplexOptMulti) {
std::complex<double> val;
app.add_complex("--long", val)->take_first();
app.add_complex("--long", val)->take_first()->allow_extra_args();
args = {"--long", "1", "2", "3", "4"};
@ -1141,7 +1242,7 @@ TEST_F(TApp, RequiredOptsUnlimited) {
app.allow_extras(false);
std::vector<std::string> remain;
app.add_option("positional", remain);
auto popt = app.add_option("positional", remain);
run();
EXPECT_EQ(strs, std::vector<std::string>({"one", "two"}));
EXPECT_EQ(remain, std::vector<std::string>());
@ -1157,6 +1258,12 @@ TEST_F(TApp, RequiredOptsUnlimited) {
run();
EXPECT_EQ(strs, std::vector<std::string>({"two"}));
EXPECT_EQ(remain, std::vector<std::string>({"one"}));
args = {"--str", "one", "two"};
popt->required();
run();
EXPECT_EQ(strs, std::vector<std::string>({"one"}));
EXPECT_EQ(remain, std::vector<std::string>({"two"}));
}
TEST_F(TApp, RequiredOptsUnlimitedShort) {
@ -1217,9 +1324,9 @@ TEST_F(TApp, OptsUnlimitedEnd) {
TEST_F(TApp, RequireOptPriority) {
std::vector<std::string> strs;
app.add_option("--str", strs)->required();
app.add_option("--str", strs);
std::vector<std::string> remain;
app.add_option("positional", remain)->expected(2);
app.add_option("positional", remain)->expected(2)->required();
args = {"--str", "one", "two", "three"};
run();
@ -1239,7 +1346,7 @@ TEST_F(TApp, RequireOptPriorityShort) {
std::vector<std::string> strs;
app.add_option("-s", strs)->required();
std::vector<std::string> remain;
app.add_option("positional", remain)->expected(2);
app.add_option("positional", remain)->expected(2)->required();
args = {"-s", "one", "two", "three"};
run();
@ -1330,7 +1437,7 @@ TEST_F(TApp, CallbackBoolFlags) {
EXPECT_THROW(app.add_flag_callback("hi", func), CLI::IncorrectConstruction);
cback->multi_option_policy(CLI::MultiOptionPolicy::Throw);
args = {"--val", "--val=false"};
EXPECT_THROW(run(), CLI::ConversionError);
EXPECT_THROW(run(), CLI::ArgumentMismatch);
}
TEST_F(TApp, CallbackFlagsFalse) {
@ -1638,7 +1745,7 @@ TEST_F(TApp, pair_check) {
}
// this will require that modifying the multi-option policy for tuples be allowed which it isn't at present
/*
TEST_F(TApp, pair_check_take_first) {
std::string myfile{"pair_check_file2.txt"};
bool ok = static_cast<bool>(std::ofstream(myfile.c_str()).put('a')); // create file
@ -1663,7 +1770,7 @@ TEST_F(TApp, pair_check_take_first) {
EXPECT_THROW(run(), CLI::ValidationError);
}
*/
TEST_F(TApp, VectorFixedString) {
std::vector<std::string> strvec;
std::vector<std::string> answer{"mystring", "mystring2", "mystring3"};
@ -1720,7 +1827,7 @@ TEST_F(TApp, DefaultedResult) {
opts->results(nString);
EXPECT_EQ(nString, "NA");
int newIval;
EXPECT_THROW(optv->results(newIval), CLI::ConversionError);
// EXPECT_THROW(optv->results(newIval), CLI::ConversionError);
optv->default_str("442");
optv->results(newIval);
EXPECT_EQ(newIval, 442);
@ -1731,7 +1838,8 @@ TEST_F(TApp, VectorUnlimString) {
std::vector<std::string> answer{"mystring", "mystring2", "mystring3"};
CLI::Option *opt = app.add_option("-s,--string", strvec);
EXPECT_EQ(-1, opt->get_expected());
EXPECT_EQ(1, opt->get_expected());
EXPECT_EQ(CLI::detail::expected_max_vector_size, opt->get_expected_max());
args = {"--string", "mystring", "mystring2", "mystring3"};
run();
@ -1744,6 +1852,35 @@ TEST_F(TApp, VectorUnlimString) {
EXPECT_EQ(answer, strvec);
}
TEST_F(TApp, VectorExpectedRange) {
std::vector<std::string> strvec;
CLI::Option *opt = app.add_option("--string", strvec);
opt->expected(2, 4)->multi_option_policy(CLI::MultiOptionPolicy::Throw);
args = {"--string", "mystring", "mystring2", "mystring3"};
run();
EXPECT_EQ(3u, app.count("--string"));
args = {"--string", "mystring"};
EXPECT_THROW(run(), CLI::ArgumentMismatch);
args = {"--string", "mystring", "mystring2", "string2", "--string", "string4", "string5"};
EXPECT_THROW(run(), CLI::ArgumentMismatch);
EXPECT_EQ(opt->get_expected_max(), 4);
EXPECT_EQ(opt->get_expected_min(), 2);
opt->expected(4, 2); // just test the handling of reversed arguments
EXPECT_EQ(opt->get_expected_max(), 4);
EXPECT_EQ(opt->get_expected_min(), 2);
opt->expected(-5);
EXPECT_EQ(opt->get_expected_max(), 5);
EXPECT_EQ(opt->get_expected_min(), 5);
opt->expected(-5, 7);
EXPECT_EQ(opt->get_expected_max(), 7);
EXPECT_EQ(opt->get_expected_min(), 5);
}
TEST_F(TApp, VectorFancyOpts) {
std::vector<std::string> strvec;
std::vector<std::string> answer{"mystring", "mystring2", "mystring3"};
@ -2227,6 +2364,138 @@ TEST_F(TApp, CustomDoubleOptionAlt) {
EXPECT_DOUBLE_EQ(custom_opt.second, 1.5);
}
// now with independent type sizes and expected this is possible
TEST_F(TApp, vectorPair) {
std::vector<std::pair<int, std::string>> custom_opt;
auto opt = app.add_option("--dict", custom_opt);
args = {"--dict", "1", "str1", "--dict", "3", "str3"};
run();
EXPECT_EQ(custom_opt.size(), 2u);
EXPECT_EQ(custom_opt[0].first, 1);
EXPECT_EQ(custom_opt[1].second, "str3");
args = {"--dict", "1", "str1", "--dict", "3", "str3", "--dict", "-1", "str4"};
run();
EXPECT_EQ(custom_opt.size(), 3u);
EXPECT_EQ(custom_opt[2].first, -1);
EXPECT_EQ(custom_opt[2].second, "str4");
opt->check(CLI::PositiveNumber.application_index(0));
EXPECT_THROW(run(), CLI::ValidationError);
}
TEST_F(TApp, vectorPairFail) {
std::vector<std::pair<int, std::string>> custom_opt;
app.add_option("--dict", custom_opt);
args = {"--dict", "1", "str1", "--dict", "str3", "1"};
EXPECT_THROW(run(), CLI::ConversionError);
}
TEST_F(TApp, vectorPairTypeRange) {
std::vector<std::pair<int, std::string>> custom_opt;
auto opt = app.add_option("--dict", custom_opt);
opt->type_size(2, 1); // just test switched arguments
EXPECT_EQ(opt->get_type_size_min(), 1);
EXPECT_EQ(opt->get_type_size_max(), 2);
args = {"--dict", "1", "str1", "--dict", "3", "str3"};
run();
EXPECT_EQ(custom_opt.size(), 2u);
EXPECT_EQ(custom_opt[0].first, 1);
EXPECT_EQ(custom_opt[1].second, "str3");
args = {"--dict", "1", "str1", "--dict", "3", "--dict", "-1", "str4"};
run();
EXPECT_EQ(custom_opt.size(), 3u);
EXPECT_TRUE(custom_opt[1].second.empty());
EXPECT_EQ(custom_opt[2].first, -1);
EXPECT_EQ(custom_opt[2].second, "str4");
opt->type_size(-2, -1); // test negative arguments
EXPECT_EQ(opt->get_type_size_min(), 1);
EXPECT_EQ(opt->get_type_size_max(), 2);
// this type size spec should run exactly as before
run();
EXPECT_EQ(custom_opt.size(), 3u);
EXPECT_TRUE(custom_opt[1].second.empty());
EXPECT_EQ(custom_opt[2].first, -1);
EXPECT_EQ(custom_opt[2].second, "str4");
}
// now with independent type sizes and expected this is possible
TEST_F(TApp, vectorTuple) {
std::vector<std::tuple<int, std::string, double>> custom_opt;
auto opt = app.add_option("--dict", custom_opt);
args = {"--dict", "1", "str1", "4.3", "--dict", "3", "str3", "2.7"};
run();
EXPECT_EQ(custom_opt.size(), 2u);
EXPECT_EQ(std::get<0>(custom_opt[0]), 1);
EXPECT_EQ(std::get<1>(custom_opt[1]), "str3");
EXPECT_EQ(std::get<2>(custom_opt[1]), 2.7);
args = {"--dict", "1", "str1", "4.3", "--dict", "3", "str3", "2.7", "--dict", "-1", "str4", "-1.87"};
run();
EXPECT_EQ(custom_opt.size(), 3u);
EXPECT_EQ(std::get<0>(custom_opt[2]), -1);
EXPECT_EQ(std::get<1>(custom_opt[2]), "str4");
EXPECT_EQ(std::get<2>(custom_opt[2]), -1.87);
opt->check(CLI::PositiveNumber.application_index(0));
EXPECT_THROW(run(), CLI::ValidationError);
args.back() = "haha";
args[9] = "45";
EXPECT_THROW(run(), CLI::ConversionError);
}
// now with independent type sizes and expected this is possible
TEST_F(TApp, vectorVector) {
std::vector<std::vector<int>> custom_opt;
auto opt = app.add_option("--dict", custom_opt);
args = {"--dict", "1", "2", "4", "--dict", "3", "1"};
run();
EXPECT_EQ(custom_opt.size(), 2u);
EXPECT_EQ(custom_opt[0].size(), 3u);
EXPECT_EQ(custom_opt[1].size(), 2u);
args = {"--dict", "1", "2", "4", "--dict", "3", "1", "--dict", "3", "--dict",
"3", "3", "3", "3", "3", "3", "3", "3", "3", "-3"};
run();
EXPECT_EQ(custom_opt.size(), 4u);
EXPECT_EQ(custom_opt[0].size(), 3u);
EXPECT_EQ(custom_opt[1].size(), 2u);
EXPECT_EQ(custom_opt[2].size(), 1u);
EXPECT_EQ(custom_opt[3].size(), 10u);
opt->check(CLI::PositiveNumber.application_index(9));
EXPECT_THROW(run(), CLI::ValidationError);
args.pop_back();
EXPECT_NO_THROW(run());
args.back() = "haha";
EXPECT_THROW(run(), CLI::ConversionError);
}
// #128
TEST_F(TApp, RepeatingMultiArgumentOptions) {
std::vector<std::string> entries;

View File

@ -173,46 +173,6 @@ TEST_F(TApp, IncorrectConstructionFlagPositional3) {
EXPECT_THROW(app.add_flag("cat", x), CLI::IncorrectConstruction);
}
TEST_F(TApp, IncorrectConstructionFlagExpected) {
auto cat = app.add_flag("--cat");
EXPECT_THROW(cat->expected(0), CLI::IncorrectConstruction);
EXPECT_THROW(cat->expected(1), CLI::IncorrectConstruction);
}
TEST_F(TApp, IncorrectConstructionOptionAsFlag) {
int x;
auto cat = app.add_option("--cat", x);
EXPECT_NO_THROW(cat->expected(1));
EXPECT_THROW(cat->expected(0), CLI::IncorrectConstruction);
EXPECT_THROW(cat->expected(2), CLI::IncorrectConstruction);
}
TEST_F(TApp, IncorrectConstructionOptionAsVector) {
int x;
auto cat = app.add_option("--cat", x);
EXPECT_THROW(cat->expected(2), CLI::IncorrectConstruction);
}
TEST_F(TApp, IncorrectConstructionVectorAsFlag) {
std::vector<int> x;
auto cat = app.add_option("--cat", x);
EXPECT_THROW(cat->expected(0), CLI::IncorrectConstruction);
}
TEST_F(TApp, IncorrectConstructionVectorTakeLast) {
std::vector<int> vec;
auto cat = app.add_option("--vec", vec);
EXPECT_THROW(cat->multi_option_policy(CLI::MultiOptionPolicy::TakeLast), CLI::IncorrectConstruction);
}
TEST_F(TApp, IncorrectConstructionTakeLastExpected) {
std::vector<int> vec;
auto cat = app.add_option("--vec", vec);
cat->expected(1);
ASSERT_NO_THROW(cat->multi_option_policy(CLI::MultiOptionPolicy::TakeLast));
EXPECT_THROW(cat->expected(2), CLI::IncorrectConstruction);
}
TEST_F(TApp, IncorrectConstructionNeedsCannotFind) {
auto cat = app.add_flag("--cat");
EXPECT_THROW(cat->needs("--nothing"), CLI::IncorrectConstruction);

View File

@ -458,7 +458,7 @@ TEST_F(TApp, DefaultedResult) {
opts->results(nString);
EXPECT_EQ(nString, "NA");
int newIval;
EXPECT_THROW(optv->results(newIval), CLI::ConversionError);
// EXPECT_THROW(optv->results(newIval), CLI::ConversionError);
optv->default_str("442");
optv->results(newIval);
EXPECT_EQ(newIval, 442);

View File

@ -8,6 +8,7 @@
#include <fstream>
#include <string>
#include <tuple>
#include <utility>
class NotStreamable {};
@ -42,13 +43,32 @@ TEST(TypeTools, type_size) {
V = CLI::detail::type_count<void>::value;
EXPECT_EQ(V, 0);
V = CLI::detail::type_count<std::vector<double>>::value;
EXPECT_EQ(V, -1);
EXPECT_EQ(V, 1);
V = CLI::detail::type_count<std::tuple<double, int>>::value;
EXPECT_EQ(V, 2);
V = CLI::detail::type_count<std::tuple<std::string, double, int>>::value;
EXPECT_EQ(V, 3);
V = CLI::detail::type_count<std::array<std::string, 5>>::value;
EXPECT_EQ(V, 5);
V = CLI::detail::type_count<std::vector<std::pair<std::string, double>>>::value;
EXPECT_EQ(V, 2);
}
TEST(TypeTools, expected_count) {
auto V = CLI::detail::expected_count<int>::value;
EXPECT_EQ(V, 1);
V = CLI::detail::expected_count<void>::value;
EXPECT_EQ(V, 0);
V = CLI::detail::expected_count<std::vector<double>>::value;
EXPECT_EQ(V, CLI::detail::expected_max_vector_size);
V = CLI::detail::expected_count<std::tuple<double, int>>::value;
EXPECT_EQ(V, 1);
V = CLI::detail::expected_count<std::tuple<std::string, double, int>>::value;
EXPECT_EQ(V, 1);
V = CLI::detail::expected_count<std::array<std::string, 5>>::value;
EXPECT_EQ(V, 1);
V = CLI::detail::expected_count<std::vector<std::pair<std::string, double>>>::value;
EXPECT_EQ(V, CLI::detail::expected_max_vector_size);
}
TEST(Split, SimpleByToken) {
@ -277,6 +297,8 @@ TEST(Validators, IPValidate1) {
EXPECT_FALSE(CLI::ValidIPV4(ip).empty());
ip = "aaa";
EXPECT_FALSE(CLI::ValidIPV4(ip).empty());
ip = "1.2.3.abc";
EXPECT_FALSE(CLI::ValidIPV4(ip).empty());
ip = "11.22";
EXPECT_FALSE(CLI::ValidIPV4(ip).empty());
}
@ -813,10 +835,21 @@ TEST(Types, TypeName) {
vector_name = CLI::detail::type_name<std::vector<double>>();
EXPECT_EQ("FLOAT", vector_name);
static_assert(CLI::detail::classify_object<std::pair<int, std::string>>::value ==
CLI::detail::objCategory::tuple_value,
"pair<int,string> does not read like a tuple");
std::string pair_name = CLI::detail::type_name<std::vector<std::pair<int, std::string>>>();
EXPECT_EQ("[INT,TEXT]", pair_name);
vector_name = CLI::detail::type_name<std::vector<std::vector<unsigned char>>>();
EXPECT_EQ("UINT", vector_name);
auto vclass = CLI::detail::classify_object<std::tuple<double>>::value;
EXPECT_EQ(vclass, CLI::detail::objCategory::number_constructible);
auto vclass = CLI::detail::classify_object<std::vector<std::vector<unsigned char>>>::value;
EXPECT_EQ(vclass, CLI::detail::objCategory::vector_value);
auto tclass = CLI::detail::classify_object<std::tuple<double>>::value;
EXPECT_EQ(tclass, CLI::detail::objCategory::number_constructible);
std::string tuple_name = CLI::detail::type_name<std::tuple<double>>();
EXPECT_EQ("FLOAT", tuple_name);

View File

@ -77,6 +77,26 @@ TEST_F(TApp, BuiltinComplex) {
EXPECT_DOUBLE_EQ(3, comp.imag());
}
TEST_F(TApp, BuiltinComplexFloat) {
std::complex<float> comp{1, 2};
app.add_complex<std::complex<float>, float>("-c,--complex", comp, "", true);
args = {"-c", "4", "3"};
std::string help = app.help();
EXPECT_THAT(help, HasSubstr("1"));
EXPECT_THAT(help, HasSubstr("2"));
EXPECT_THAT(help, HasSubstr("COMPLEX"));
EXPECT_FLOAT_EQ(1, comp.real());
EXPECT_FLOAT_EQ(2, comp.imag());
run();
EXPECT_FLOAT_EQ(4, comp.real());
EXPECT_FLOAT_EQ(3, comp.imag());
}
TEST_F(TApp, BuiltinComplexWithDelimiter) {
cx comp{1, 2};
app.add_complex("-c,--complex", comp, "", true)->delimiter('+');
@ -121,13 +141,61 @@ TEST_F(TApp, BuiltinComplexIgnoreI) {
EXPECT_DOUBLE_EQ(3, comp.imag());
}
TEST_F(TApp, BuiltinComplexFail) {
TEST_F(TApp, BuiltinComplexSingleArg) {
cx comp{1, 2};
app.add_complex("-c,--complex", comp);
args = {"-c", "4"};
run();
EXPECT_DOUBLE_EQ(4, comp.real());
EXPECT_DOUBLE_EQ(0, comp.imag());
EXPECT_THROW(run(), CLI::ArgumentMismatch);
args = {"-c", "4-2i"};
run();
EXPECT_DOUBLE_EQ(4, comp.real());
EXPECT_DOUBLE_EQ(-2, comp.imag());
args = {"-c", "4+2i"};
run();
EXPECT_DOUBLE_EQ(4, comp.real());
EXPECT_DOUBLE_EQ(2, comp.imag());
args = {"-c", "-4+2j"};
run();
EXPECT_DOUBLE_EQ(-4, comp.real());
EXPECT_DOUBLE_EQ(2, comp.imag());
args = {"-c", "-4.2-2j"};
run();
EXPECT_DOUBLE_EQ(-4.2, comp.real());
EXPECT_DOUBLE_EQ(-2, comp.imag());
args = {"-c", "-4.2-2.7i"};
run();
EXPECT_DOUBLE_EQ(-4.2, comp.real());
EXPECT_DOUBLE_EQ(-2.7, comp.imag());
}
TEST_F(TApp, BuiltinComplexSingleImag) {
cx comp{1, 2};
app.add_complex("-c,--complex", comp);
args = {"-c", "4j"};
run();
EXPECT_DOUBLE_EQ(0, comp.real());
EXPECT_DOUBLE_EQ(4, comp.imag());
args = {"-c", "-4j"};
run();
EXPECT_DOUBLE_EQ(0, comp.real());
EXPECT_DOUBLE_EQ(-4, comp.imag());
args = {"-c", "-4"};
run();
EXPECT_DOUBLE_EQ(-4, comp.real());
EXPECT_DOUBLE_EQ(0, comp.imag());
args = {"-c", "+4"};
run();
EXPECT_DOUBLE_EQ(4, comp.real());
EXPECT_DOUBLE_EQ(0, comp.imag());
}
class spair {

View File

@ -104,6 +104,22 @@ TEST_F(TApp, BoostOptionalTest) {
EXPECT_EQ(*opt, 3);
}
TEST_F(TApp, BoostOptionalTestZarg) {
boost::optional<int> opt;
app.add_option("-c,--count", opt)->expected(0, 1);
run();
EXPECT_FALSE(opt);
args = {"-c", "1"};
run();
EXPECT_TRUE(opt);
EXPECT_EQ(*opt, 1);
opt = {};
args = {"--count"};
run();
EXPECT_FALSE(opt);
}
TEST_F(TApp, BoostOptionalint64Test) {
boost::optional<int64_t> opt;
app.add_option("-c,--count", opt);
@ -175,6 +191,22 @@ TEST_F(TApp, BoostOptionalVector) {
EXPECT_EQ(*opt, expV);
}
TEST_F(TApp, BoostOptionalVectorEmpty) {
boost::optional<std::vector<int>> opt;
app.add_option<decltype(opt), std::vector<int>>("-v,--vec", opt)->expected(0, 3)->allow_extra_args();
run();
EXPECT_FALSE(opt);
args = {"-v"};
opt = std::vector<int>{4, 3};
run();
EXPECT_FALSE(opt);
args = {"-v", "1", "4", "5"};
run();
EXPECT_TRUE(opt);
std::vector<int> expV{1, 4, 5};
EXPECT_EQ(*opt, expV);
}
#endif
#if !CLI11_OPTIONAL

View File

@ -101,6 +101,24 @@ TEST_F(TApp, EnumCheckedTransform) {
EXPECT_THROW(run(), CLI::ValidationError);
}
// from jzakrzewski Issue #330
TEST_F(TApp, EnumCheckedDefualtTransform) {
enum class existing : int16_t { abort, overwrite, remove };
app.add_option("--existing", "What to do if file already exists in the destination")
->transform(
CLI::CheckedTransformer(std::unordered_map<std::string, existing>{{"abort", existing::abort},
{"overwrite", existing ::overwrite},
{"delete", existing::remove},
{"remove", existing::remove}}))
->default_val("abort");
args = {"--existing", "overwrite"};
run();
EXPECT_EQ(app.get_option("--existing")->as<existing>(), existing::overwrite);
args.clear();
run();
EXPECT_EQ(app.get_option("--existing")->as<existing>(), existing::abort);
}
TEST_F(TApp, SimpleTransformFn) {
int value;
auto opt = app.add_option("-s", value)->transform(CLI::Transformer({{"one", "1"}}, CLI::ignore_case));

View File

@ -9,7 +9,7 @@
#include "gtest/gtest.h"
#include <iostream>
typedef std::vector<std::string> input_t;
using input_t = std::vector<std::string>;
struct TApp : public ::testing::Test {
CLI::App app{"My Test Program"};
@ -27,7 +27,7 @@ class TempFile {
std::string _name;
public:
explicit TempFile(std::string name) : _name(name) {
explicit TempFile(std::string name) : _name(std::move(name)) {
if(!CLI::NonexistentPath(_name).empty())
throw std::runtime_error(_name);
}