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

Option groups (#227)

* change the move function to _move_option and add an additional test

add a validation check on min options to make sure it is even possible to succeed.

add some additional tests to cover code paths and potential errors.

add a number of additional tests and checks and fix some issues with the add function in option_groups

clean up example and help formatting

add option_groups example to play with

move create_option_group to a member function using a dummy template

add some optionGroup tests

add min and max options calls and an associated Error call

* add ranges example,  add excludes to app for options and subcommands.

* add some tests on ranges, and some subcommand tests with exclusion

* add tests in optionGroups for some invalid inputs

* add required option to subcommands and option_groups

* add disabled flag

* add disable option to subcommands and some more tests

* start work on ReadMe modifications

* update the readme with descriptions of function and methods added for option_groups

* clear up gcc 4.7 warnings

* some update to the Readme and a few more warnings fixed

* Minor readme touchup
This commit is contained in:
Philip Top 2019-03-01 08:43:08 -08:00 committed by Henry Schreiner
parent 4a4608102d
commit 0631189b4d
14 changed files with 1158 additions and 59 deletions

View File

@ -32,8 +32,10 @@ CLI11 is a command line parser for C++11 and beyond that provides a rich feature
- [Adding options](#adding-options)
- [Option types](#option-types)
- [Option options](#option-options)
- [Getting Results](#getting-results) 🚧
- [Subcommands](#subcommands)
- [Subcommand options](#subcommand-options)
- [Option Groups](#option-groups) 🚧
- [Configuration file](#configuration-file)
- [Inheriting defaults](#inheriting-defaults)
- [Formatting](#formatting)
@ -45,6 +47,8 @@ CLI11 is a command line parser for C++11 and beyond that provides a rich feature
- [Contribute](#contribute)
- [License](#license)
Features that were added in the last released major version are marked with "🆕". Features only available in master are marked with "🚧".
## Background
### Introduction
@ -53,7 +57,7 @@ CLI11 provides all the features you expect in a powerful command line parser, wi
It is tested on [Travis][], [AppVeyor][], and [Azure][], and is being included in the [GooFit GPU fitting framework][goofit]. It was inspired by [`plumbum.cli`][plumbum] for Python. CLI11 has a user friendly introduction in this README, a more in-depth tutorial [GitBook][], as well as [API documentation][api-docs] generated by Travis.
See the [changelog](./CHANGELOG.md) or [GitHub Releases][] for details for current and past releases. Also see the [Version 1.0 post][], [Version 1.3 post][], or [Version 1.6 post][] for more information.
You can be notified when new releases are made by subscribing to <https://github.com/CLIUtils/CLI11/releases.atom> on an RSS reader, like Feedly.
You can be notified when new releases are made by subscribing to <https://github.com/CLIUtils/CLI11/releases.atom> on an RSS reader, like Feedly, or use the releases mode of the github watching tool.
### Why write another CLI parser?
@ -61,7 +65,7 @@ An acceptable CLI parser library should be all of the following:
- Easy to include (i.e., header only, one file if possible, **no external requirements**).
- Short, simple syntax: This is one of the main reasons to use a CLI parser, it should make variables from the command line nearly as easy to define as any other variables. If most of your program is hidden in CLI parsing, this is a problem for readability.
- C++11 or better: Should work with GCC 4.7+ (such as GCC 4.8 on CentOS 7), Clang 3.5+, AppleClang 7+, NVCC 7.0+, or MSVC 2015+.
- C++11 or better: Should work with GCC 4.8+ (default on CentOS/RHEL 7), Clang 3.5+, AppleClang 7+, NVCC 7.0+, or MSVC 2015+.
- Work on Linux, macOS, and Windows.
- Well tested using [Travis][] (Linux and macOS) and [AppVeyor][] (Windows) or [Azure][] (all three). "Well" is defined as having good coverage measured by [CodeCov][].
- Clear help printing.
@ -69,7 +73,7 @@ An acceptable CLI parser library should be all of the following:
- Standard shell idioms supported naturally, like grouping flags, a positional separator, etc.
- Easy to execute, with help, parse errors, etc. providing correct exit and details.
- Easy to extend as part of a framework that provides "applications" to users.
- Usable subcommand syntax, with support for multiple subcommands, nested subcommands, and optional fallthrough (explained later).
- Usable subcommand syntax, with support for multiple subcommands, nested subcommands, option groups, and optional fallthrough (explained later).
- Ability to add a configuration file (`ini` format), and produce it as well.
- Produce real values that can be used directly in code, not something you have pay compute time to look up, for HPC applications.
- Work with standard types, simple custom types, and extensible to exotic types.
@ -208,6 +212,8 @@ app.add_flag_callback(option_name,function<void(void)>,help_string="") // 🚧
// Add subcommands
App* subcom = app.add_subcommand(name, description);
Option_group *app.add_option_group(name,description); // 🚧
// 🚧 All add_*set* methods deprecated in CLI11 1.8 - use ->transform(CLI::IsMember) instead
-app.add_set(option_name,
- variable_to_bind_to, // Same type as stored by set
@ -223,7 +229,7 @@ App* subcom = app.add_subcommand(name, description);
-app.add_mutable_set_ignore_case_underscore(... // 🆕 String only
```
An option name must start with a alphabetic character, underscore, or a number. For long options, anything but an equals sign or a comma is valid after that, though for the `add_flag*` functions '{' has special meaning. Names are given as a comma separated string, with the dash or dashes. An option or flag can have as many names as you want, and afterward, using `count`, you can use any of the names, with dashes as needed, to count the options. One of the names is allowed to be given without proceeding dash(es); if present the option is a positional option, and that name will be used on help line for its positional form. If you want the default value to print in the help description, pass in `true` for the final parameter for `add_option`.
An option name must start with a alphabetic character, underscore, or a number. For long options, after the first character '.', and '-' are also valid. For the `add_flag*` functions '{' has special meaning. Names are given as a comma separated string, with the dash or dashes. An option or flag can have as many names as you want, and afterward, using `count`, you can use any of the names, with dashes as needed, to count the options. One of the names is allowed to be given without proceeding dash(es); if present the option is a positional option, and that name will be used on help line for its positional form. If you want the default value to print in the help description, pass in `true` for the final parameter for `add_option`.
The `add_option_function<type>(...` function will typically require the template parameter be given unless a `std::function` object with an exact match is passed. The type can be any type supported by the `add_option` function.
@ -246,7 +252,7 @@ app.add_flag("-1{1},-2{2},-3{3}",result,"numerical flag") // 🚧
using any of those flags on the command line will result in the specified number in the output. Similar things can be done for string values, and enumerations, as long as the default value can be converted to the given type.
On a C++14 compiler, you can pass a callback function directly to `.add_flag`, while in C++11 mode you'll need to use `.add_flag_function` if you want a callback function. The function will be given the number of times the flag was passed. You can throw a relevant `CLI::ParseError` to signal a failure.
On a `C++14` compiler, you can pass a callback function directly to `.add_flag`, while in C++11 mode you'll need to use `.add_flag_function` if you want a callback function. The function will be given the number of times the flag was passed. You can throw a relevant `CLI::ParseError` to signal a failure.
On a compiler that supports C++17's `__has_include`, you can also use `std::optional`, `std::experimental::optional`, and `boost::optional` directly in an `add_option` call. If you don't have `__has_include`, you can define `CLI11_BOOST_OPTIONAL 1` before including CLI11 to manually add support (or 0 to remove) for `boost::optional`. See [CLI11 Internals][] for information on how this was done and how you can add your own converters.
@ -334,12 +340,13 @@ You can access a vector of pointers to the parsed options in the original order
If `--` is present in the command line that does not end an unlimited option, then
everything after that is positional only.
#### Getting results 🚧
#### Getting results {#getting-results} 🚧
In most cases the fastest and easiest way is to return the results through a callback or variable specified in one of the `add_*` functions. But there are situations where this is not possible or desired. For these cases the results may be obtained through one of the following functions. Please note that these functions will do any type conversions and processing during the call so should not used in performance critical code:
- `results()`: retrieves a vector of strings with all the results in the order they were given.
- `results(variable_to_bind_to)`: 🚧 gets the results according to the MultiOptionPolicy and converts them just like the `add_option_function` with a variable.
- `Value=as<type>()`: 🚧 returns the result or default value directly as the specified type if possible, can be vector to return all results, and a non-vector to get the result according to the MultiOptionPolicy in place.
- `results()`: Retrieves a vector of strings with all the results in the order they were given.
- `results(variable_to_bind_to)`: 🚧 Gets the results according to the MultiOptionPolicy and converts them just like the `add_option_function` with a variable.
- `Value=as<type>()`: 🚧 Returns the result or default value directly as the specified type if possible, can be vector to return all results, and a non-vector to get the result according to the MultiOptionPolicy in place.
### Subcommands
@ -362,19 +369,25 @@ Nameless subcommands function a similarly to groups in the main `App`. If an op
#### Subcommand options
There are several options that are supported on the main app and subcommands. These are:
There are several options that are supported on the main app and subcommands and option_groups. These are:
- `.ignore_case()`: Ignore the case of this subcommand. Inherited by added subcommands, so is usually used on the main `App`.
- `.ignore_underscore()`: 🆕 Ignore any underscores in the subcommand name. Inherited by added subcommands, so is usually used on the main `App`.
- `.allow_windows_style_options()`: 🆕 Allow command line options to be parsed in the form of `/s /long /file:file_name.ext` This option does not change how options are specified in the `add_option` calls or the ability to process options in the form of `-s --long --file=file_name.ext`
- `.fallthrough()`: Allow extra unmatched options and positionals to "fall through" and be matched on a parent command. Subcommands always are allowed to fall through.
- `.disable()`: 🚧 Specify that the subcommand is disabled, if given with a bool value it will enable or disable the subcommand or option group.
- `.exludes(option_or_subcommand)`: 🚧 If given an option pointer or pointer to another subcommand, these subcommands cannot be given together. In the case of options, if the option is passed the subcommand cannot be used and will generate an error.
- `.require_option()`: 🚧 Require 1 or more options or option groups be used.
- `.require_option(N)`: 🚧 Require `N` options or option groups if `N>0`, or up to `N` if `N<0`. `N=0` resets to the default 0 or more.
- `.require_option(min, max)`: 🚧 Explicitly set min and max allowed options or option groups. Setting `max` to 0 is unlimited.
- `.require_subcommand()`: Require 1 or more subcommands.
- `.require_subcommand(N)`: Require `N` subcommands if `N>0`, or up to `N` if `N<0`. `N=0` resets to the default 0 or more.
- `.require_subcommand(min, max)`: Explicitly set min and max allowed subcommands. Setting `max` to 0 is unlimited.
- `.add_subcommand(name="", description="")` Add a subcommand, returns a pointer to the internally stored subcommand.
- `.add_subcommand(shared_ptr<App>)` 🚧 Add a subcommand by shared_ptr, returns a pointer to the internally stored subcommand.
- `.add_subcommand(name="", description="")`: Add a subcommand, returns a pointer to the internally stored subcommand.
- `.add_subcommand(shared_ptr<App>)`: 🚧 Add a subcommand by shared_ptr, returns a pointer to the internally stored subcommand.
- `.got_subcommand(App_or_name)`: Check to see if a subcommand was received on the command line.
- `.get_subcommands(filter)`: The list of subcommands given on the command line.
- `.get_subcommands(filter)`: The list of subcommands that match a particular filter function.
- `.add_option_group(name="", description="")`: 🚧 Add an option group to an App, an option group is specialized subcommand intended for containing groups of options or other groups for controlling how options interact.
- `.get_parent()`: Get the parent App or nullptr if called on master App.
- `.get_option(name)`: Get an option pointer by option name will throw if the specified option is not available, nameless subcommands are also searched
- `.get_option_no_throw(name)`: 🚧 Get an option pointer by option name. This function will return a `nullptr` instead of throwing if the option is not available.
@ -384,6 +397,9 @@ There are several options that are supported on the main app and subcommands. Th
- `.description(str)`: 🆕 Set/change the description.
- `.get_description()`: Access the description.
- `.parsed()`: True if this subcommand was given on the command line.
- `.count()`: Returns the number of times the subcommand was called
- `.count(option_name)`: Returns the number of times a particular option was called
- `.count_all()`: 🚧 Returns the total number of arguments a particular subcommand had, on the master App it returns the total number of processed commands
- `.name(name)`: Add or change the name.
- `.callback(void() function)`: Set the callback that runs at the end of parsing. The options have already run at this point.
- `.allow_extras()`: Do not throw an error if extra arguments are left over.
@ -398,6 +414,27 @@ There are several options that are supported on the main app and subcommands. Th
> Note: if you have a fixed number of required positional options, that will match before subcommand names. `{}` is an empty filter function.
#### Option Groups 🚧 {#option-groups}
The method
```cpp
.add_option_group(name,description)
```
Will create an option Group, and return a pointer to it. An option group allows creation of a collection of options, similar to the groups function on options, but with additional controls and requirements. They allow specific sets of options to be composed and controlled as a collective. For an example see [range test](./tests/ranges.cpp). Option groups are a specialization of an App so all [functions](#subcommand-options) that work with an App also work on option groups. Options can be created as part of an option group using the add functions just like a subcommand, or previously created options can be added through
```cpp
ogroup->add_option(option_pointer)
```
```cpp
ogroup->add_options(option_pointer)
```
```cpp
ogroup->add_options(option1,option2,option3,...)
```
The option pointers used in this function must be options defined in the parent application of the option group otherwise an error will be generated.
Options in an option group are searched for a command line match after any options in the main app, so any positionals in the main app would be matched first. So care must be taken to make sure of the order when using positional arguments and option groups.
Option groups work well with `excludes` and `require_options` methods, as an Application will treat an option group as a single option for the purpose of counting and requirements. Option groups allow specifying requirements such as requiring 1 of 3 options in one group and 1 of 3 options in a different group. Option groups can contain other groups as well. Disabling an option group will turn off all options within the group.
### Configuration file
```cpp

View File

@ -69,6 +69,7 @@ set_property(TEST subcom_partitioned_none PROPERTY PASS_REGULAR_EXPRESSION
"This is a timer:"
"--file is required"
"Run with --help for more information.")
add_test(NAME subcom_partitioned_all COMMAND subcom_partitioned --file this --count --count -d 1.2)
set_property(TEST subcom_partitioned_all PROPERTY PASS_REGULAR_EXPRESSION
"This is a timer:"
@ -81,6 +82,30 @@ set_property(TEST subcom_partitioned_help PROPERTY PASS_REGULAR_EXPRESSION
"-f,--file TEXT REQUIRED"
"-d,--double FLOAT")
add_cli_exe(option_groups option_groups.cpp)
add_test(NAME option_groups_missing COMMAND option_groups )
set_property(TEST option_groups_missing PROPERTY PASS_REGULAR_EXPRESSION
"Exactly 1 option from"
"is required")
add_test(NAME option_groups_extra COMMAND option_groups --csv --binary)
set_property(TEST option_groups_extra PROPERTY PASS_REGULAR_EXPRESSION
"and 2 were given")
add_test(NAME option_groups_extra2 COMMAND option_groups --csv --address "192.168.1.1" -o "test.out")
set_property(TEST option_groups_extra2 PROPERTY PASS_REGULAR_EXPRESSION
"at most 1")
add_cli_exe(ranges ranges.cpp)
add_test(NAME ranges_range COMMAND ranges --range 1 2 3)
set_property(TEST ranges_range PROPERTY PASS_REGULAR_EXPRESSION
"[2:1:3]")
add_test(NAME ranges_minmax COMMAND ranges --min 2 --max 3)
set_property(TEST ranges_minmax PROPERTY PASS_REGULAR_EXPRESSION
"[2:1:3]")
add_test(NAME ranges_error COMMAND ranges --min 2 --max 3 --step 1 --range 1 2 3)
set_property(TEST ranges_error PROPERTY PASS_REGULAR_EXPRESSION
"Exactly 1 option from")
add_cli_exe(validators validators.cpp)
add_test(NAME validators_help COMMAND validators --help)
set_property(TEST validators_help PROPERTY PASS_REGULAR_EXPRESSION

View File

@ -0,0 +1,38 @@
#include "CLI/CLI.hpp"
int main(int argc, char **argv) {
CLI::App app("data output specification");
app.set_help_all_flag("--help-all", "Expand all help");
auto format = app.add_option_group("output_format", "formatting type for output");
auto target = app.add_option_group("output target", "target location for the output");
bool csv = false;
bool human = false;
bool binary = false;
format->add_flag("--csv", csv, "specify the output in csv format");
format->add_flag("--human", human, "specify the output in human readable text format");
format->add_flag("--binary", binary, "specify the output in binary format");
// require one of the options to be selected
format->require_option(1);
std::string fileLoc;
std::string networkAddress;
target->add_option("-o,--file", fileLoc, "specify the file location of the output");
target->add_option("--address", networkAddress, "specify a network address to send the file");
// require at most one of the target options
target->require_option(0, 1);
CLI11_PARSE(app, argc, argv);
std::string format_type = (csv) ? std::string("CSV") : ((human) ? "human readable" : "binary");
std::cout << "Selected " << format_type << "format" << std::endl;
if(fileLoc.empty()) {
std::cout << " sent to file " << fileLoc << std::endl;
} else if(networkAddress.empty()) {
std::cout << " sent over network to " << networkAddress << std::endl;
} else {
std::cout << " sent to std::cout" << std::endl;
}
return 0;
}

33
examples/ranges.cpp Normal file
View File

@ -0,0 +1,33 @@
#include "CLI/CLI.hpp"
int main(int argc, char **argv) {
CLI::App app{"App to demonstrate exclusionary option groups."};
std::vector<int> range;
app.add_option("--range,-R", range, "A range")->expected(-2);
auto ogroup = app.add_option_group("min_max_step", "set the min max and step");
int min, max, step = 1;
ogroup->add_option("--min,-m", min, "The minimum")->required();
ogroup->add_option("--max,-M", max, "The maximum")->required();
ogroup->add_option("--step,-s", step, "The step", true);
app.require_option(1);
CLI11_PARSE(app, argc, argv);
if(!range.empty()) {
if(range.size() == 2) {
min = range[0];
max = range[1];
}
if(range.size() >= 3) {
step = range[0];
min = range[1];
max = range[2];
}
}
std::cout << "range is [" << min << ':' << step << ':' << max << "]\n";
return 0;
}

View File

@ -51,6 +51,7 @@ class App;
using App_p = std::shared_ptr<App>;
class Option_group;
/// Creates a command line program, with very few defaults.
/** To use, create a new `Program()` instance with `argc`, `argv`, and a help description. The templated
* add_option methods make it easy to prepare options. Remember to call `.start` before starting your
@ -80,9 +81,15 @@ class App {
/// If true, return immediately on an unrecognized option (implies allow_extras) INHERITABLE
bool prefix_command_{false};
/// if set to true the name was automatically generated from the command line vs a user set name
/// If set to true the name was automatically generated from the command line vs a user set name
bool has_automatic_name_{false};
/// If set to true the subcommand is required to be processed and used, ignored for main app
bool required_{false};
/// If set to true the subcommand is disabled and cannot be used, ignored for main app
bool disabled_{false};
/// This is a function that runs when complete. Great for subcommands. Can throw.
std::function<void()> callback_;
@ -132,6 +139,13 @@ class App {
/// This is a list of the subcommands collected, in order
std::vector<App *> parsed_subcommands_;
/// this is a list of subcommands that are exclusionary to this one
std::set<App *> exclude_subcommands_;
/// This is a list of options which are exclusionary to this App, if the options were used this subcommand should
/// not be
std::set<Option *> exclude_options_;
///@}
/// @name Subcommands
///@{
@ -171,6 +185,12 @@ class App {
/// Max number of subcommands allowed (parsing stops after this number). 0 is unlimited INHERITABLE
size_t require_subcommand_max_ = 0;
/// Minimum required options (not inheritable!)
size_t require_option_min_ = 0;
/// Max number of options allowed. 0 is unlimited (not inheritable)
size_t require_option_max_ = 0;
/// The group membership INHERITABLE
std::string group_{"Subcommands"};
@ -260,6 +280,18 @@ class App {
return this;
}
/// Remove the error when extras are left over on the command line.
App *required(bool require = true) {
required_ = require;
return this;
}
/// Disable the subcommand or option group
App *disabled(bool disable = true) {
disabled_ = disable;
return this;
}
/// Remove the error when extras are left over on the command line.
/// Will also call App::allow_extras().
App *allow_config_extras(bool allow = true) {
@ -926,7 +958,7 @@ class App {
Option *set_config(std::string option_name = "",
std::string default_filename = "",
std::string help_message = "Read an ini file",
bool required = false) {
bool config_required = false) {
// Remove existing config if present
if(config_ptr_ != nullptr)
@ -935,7 +967,7 @@ class App {
// Only add config if option passed
if(!option_name.empty()) {
config_name_ = default_filename;
config_required_ = required;
config_required_ = config_required;
config_ptr_ = add_option(option_name, config_name_, help_message, !default_filename.empty());
config_ptr_->configurable(false);
}
@ -965,6 +997,17 @@ class App {
return false;
}
/// creates an option group as part of the given app
template <typename T = Option_group>
T *add_option_group(std::string group_name, std::string group_description = "") {
auto option_group = std::make_shared<T>(std::move(group_description), group_name, nullptr);
auto ptr = option_group.get();
// move to App_p for overload resolution on older gcc versions
App_p app_ptr = std::dynamic_pointer_cast<App>(option_group);
add_subcommand(std::move(app_ptr));
return ptr;
}
///@}
/// @name Subcommmands
///@{
@ -988,6 +1031,7 @@ class App {
subcommands_.push_back(std::move(subcom));
return subcommands_.back().get();
}
/// Check to see if a subcommand is part of this command (doesn't have to be in command line)
/// returns the first subcommand if passed a nullptr
App *get_subcommand(App *subcom) const {
@ -1043,6 +1087,22 @@ class App {
/// otherwise modified in a callback
size_t count() const { return parsed_; }
/// Get a count of all the arguments processed in options and subcommands, this excludes arguments which were
/// treated as extras.
size_t count_all() const {
size_t cnt{0};
for(auto &opt : options_) {
cnt += opt->count();
}
for(auto &sub : subcommands_) {
cnt += sub->count_all();
}
if(!get_name().empty()) { // for named subcommands add the number of times the subcommand was called
cnt += parsed_;
}
return cnt;
}
/// Changes the group membership
App *group(std::string group_name) {
group_ = group_name;
@ -1078,6 +1138,35 @@ class App {
return this;
}
/// The argumentless form of require option requires 1 or more options be used
App *require_option() {
require_option_min_ = 1;
require_option_max_ = 0;
return this;
}
/// Require an option to be given (does not affect help call)
/// The number required can be given. Negative values indicate maximum
/// number allowed (0 for any number).
App *require_option(int value) {
if(value < 0) {
require_option_min_ = 0;
require_option_max_ = static_cast<size_t>(-value);
} else {
require_option_min_ = static_cast<size_t>(value);
require_option_max_ = static_cast<size_t>(value);
}
return this;
}
/// Explicitly control the number of options required. Setting 0
/// for the max means unlimited number allowed. Max number inheritable.
App *require_option(size_t min, size_t max) {
require_option_min_ = min;
require_option_max_ = max;
return this;
}
/// Stop subcommand fallthrough, so that parent commands cannot collect commands after subcommand.
/// Default from parent, usually set on parent.
App *fallthrough(bool value = true) {
@ -1219,7 +1308,7 @@ class App {
/// Counts the number of times the given option was passed.
size_t count(std::string option_name) const { return get_option(option_name)->count(); }
/// Get a subcommand pointer list to the currently selected subcommands (after parsing by by default, in command
/// Get a subcommand pointer list to the currently selected subcommands (after parsing by default, in command
/// line order; use parsed = false to get the original definition list.)
std::vector<App *> get_subcommands() const { return parsed_subcommands_; }
@ -1267,6 +1356,50 @@ class App {
/// Check with name instead of pointer to see if subcommand was selected
bool got_subcommand(std::string subcommand_name) const { return get_subcommand(subcommand_name)->parsed_ > 0; }
/// Sets excluded options for the subcommand
App *excludes(Option *opt) {
if(opt == nullptr) {
throw OptionNotFound("nullptr passed");
}
exclude_options_.insert(opt);
return this;
}
/// Sets excluded subcommands for the subcommand
App *excludes(App *app) {
if((app == this) || (app == nullptr)) {
throw OptionNotFound("nullptr passed");
}
auto res = exclude_subcommands_.insert(app);
// subcommand exclusion should be symmetric
if(res.second) {
app->exclude_subcommands_.insert(this);
}
return this;
}
/// Removes an option from the excludes list of this subcommand
bool remove_excludes(Option *opt) {
auto iterator = std::find(std::begin(exclude_options_), std::end(exclude_options_), opt);
if(iterator != std::end(exclude_options_)) {
exclude_options_.erase(iterator);
return true;
} else {
return false;
}
}
/// Removes a subcommand from this excludes list of this subcommand
bool remove_excludes(App *app) {
auto iterator = std::find(std::begin(exclude_subcommands_), std::end(exclude_subcommands_), app);
if(iterator != std::end(exclude_subcommands_)) {
exclude_subcommands_.erase(iterator);
return true;
} else {
return false;
}
}
///@}
/// @name Help
///@{
@ -1424,12 +1557,24 @@ class App {
/// Get the required max subcommand value
size_t get_require_subcommand_max() const { return require_subcommand_max_; }
/// Get the required min option value
size_t get_require_option_min() const { return require_option_min_; }
/// Get the required max option value
size_t get_require_option_max() const { return require_option_max_; }
/// Get the prefix command status
bool get_prefix_command() const { return prefix_command_; }
/// Get the status of allow extras
bool get_allow_extras() const { return allow_extras_; }
/// Get the status of required
bool get_required() const { return required_; }
/// Get the status of required
bool get_disabled() const { return disabled_; }
/// Get the status of allow extras
bool get_allow_config_extras() const { return allow_config_extras_; }
@ -1457,6 +1602,9 @@ class App {
/// Get the name of the current app
std::string get_name() const { return name_; }
/// Get a display name for an app
std::string get_display_name() const { return (!name_.empty()) ? name_ : "[Option Group: " + get_group() + "]"; }
/// Check the name, case insensitive and underscore insensitive if set
bool check_name(std::string name_to_check) const {
std::string local_name = name_;
@ -1525,15 +1673,33 @@ class App {
protected:
/// Check the options to make sure there are no conflicts.
///
/// Currently checks to see if multiple positionals exist with -1 args
/// Currently checks to see if multiple positionals exist with -1 args and checks if the min and max options are
/// feasible
void _validate() const {
auto pcount = std::count_if(std::begin(options_), std::end(options_), [](const Option_p &opt) {
return opt->get_items_expected() < 0 && opt->get_positional();
});
if(pcount > 1)
throw InvalidError(name_);
size_t nameless_subs{0};
for(const App_p &app : subcommands_) {
app->_validate();
if(app->get_name().empty())
++nameless_subs;
}
if(require_option_min_ > 0) {
if(require_option_max_ > 0) {
if(require_option_max_ < require_option_min_) {
throw(InvalidError("Required min options greater than required max options",
ExitCodes::InvalidError));
}
}
if(require_option_min_ > (options_.size() + nameless_subs)) {
throw(InvalidError("Required min options greater than number of available options",
ExitCodes::InvalidError));
}
}
}
@ -1572,7 +1738,7 @@ class App {
}
for(const App_p &com : subcommands_)
if(com->check_name(current) && !*com)
if(!com->disabled_ && com->check_name(current) && !*com)
return true;
// Check parent if exists, else return false
@ -1690,8 +1856,34 @@ class App {
/// Verify required options and cross requirements. Subcommands too (only if selected).
void _process_requirements() {
// check excludes
bool excluded{false};
std::string excluder;
for(auto &opt : exclude_options_) {
if(opt->count() > 0) {
excluded = true;
excluder = opt->get_name();
}
}
for(auto &subc : exclude_subcommands_) {
if(subc->count_all() > 0) {
excluded = true;
excluder = subc->get_display_name();
}
}
if(excluded) {
if(count_all() > 0) {
throw ExcludesError(get_display_name(), excluder);
}
// if we are excluded but didn't receive anything, just return
return;
}
size_t used_options = 0;
for(const Option_p &opt : options_) {
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)
@ -1711,16 +1903,60 @@ class App {
if(opt->count() > 0 && opt_ex->count() != 0)
throw ExcludesError(opt->get_name(), opt_ex->get_name());
}
// check for the required number of subcommands
if(require_subcommand_min_ > 0) {
auto selected_subcommands = get_subcommands();
if(require_subcommand_min_ > selected_subcommands.size())
throw RequiredError::Subcommand(require_subcommand_min_);
}
// Max error cannot occur, the extra subcommand will parse as an ExtrasError or a remaining item.
// run this loop to check how many unnamed subcommands were actually used since they are considered options from
// the perspective of an App
for(App_p &sub : subcommands_) {
if((sub->count() > 0) || (sub->name_.empty()))
if(sub->disabled_)
continue;
if((sub->name_.empty()) && (sub->count_all() > 0)) {
++used_options;
}
}
if((require_option_min_ > used_options) ||
((require_option_max_ > 0) && (require_option_max_ < used_options))) {
auto option_list = detail::join(options_, [](const Option_p &ptr) { return ptr->get_name(false, true); });
if(option_list.compare(0, 10, "-h,--help,") == 0) {
option_list.erase(0, 10);
}
auto subc_list = get_subcommands([](App *app) { return ((app->get_name().empty()) && (!app->disabled_)); });
if(!subc_list.empty()) {
option_list += "," + detail::join(subc_list, [](const App *app) { return app->get_display_name(); });
}
throw RequiredError::Option(require_option_min_, require_option_max_, used_options, option_list);
}
// now process the requirements for subcommands if needed
for(App_p &sub : subcommands_) {
if(sub->disabled_)
continue;
if((sub->name_.empty()) && (sub->required_ == false)) {
if(sub->count_all() == 0) {
if((require_option_min_ > 0) && (require_option_min_ <= used_options)) {
continue;
// if we have met the requirement and there is nothing in this option group skip checking
// requirements
}
if((require_option_max_ > 0) && (used_options >= require_option_min_)) {
continue;
// if we have met the requirement and there is nothing in this option group skip checking
// requirements
}
}
}
sub->_process_requirements();
if((sub->required_) && (sub->count_all() == 0)) {
throw(CLI::RequiredError(sub->get_display_name()));
}
}
}
@ -1859,10 +2095,10 @@ class App {
}
/// Count the required remaining positional arguments
size_t _count_remaining_positionals(bool required = false) const {
size_t _count_remaining_positionals(bool required_only = false) const {
size_t retval = 0;
for(const Option_p &opt : options_)
if(opt->get_positional() && (!required || opt->get_required()) && opt->get_items_expected() > 0 &&
if(opt->get_positional() && (!required_only || opt->get_required()) && 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();
@ -1886,7 +2122,7 @@ class App {
}
for(auto &subc : subcommands_) {
if(subc->name_.empty()) {
if((subc->name_.empty()) && (!subc->disabled_)) {
subc->_parse_positional(args);
if(subc->missing_.empty()) { // check if it was used and is not in the missing category
return;
@ -1922,6 +2158,8 @@ class App {
if(_count_remaining_positionals(/* required */ true) > 0)
return _parse_positional(args);
for(const App_p &com : subcommands_) {
if(com->disabled_)
continue;
if(com->check_name(args.back())) {
args.pop_back();
if(std::find(std::begin(parsed_subcommands_), std::end(parsed_subcommands_), com.get()) ==
@ -1976,11 +2214,12 @@ class App {
// Option not found
if(op_ptr == std::end(options_)) {
for(auto &subc : subcommands_) {
if(subc->name_.empty()) {
if((subc->name_.empty()) && (!(subc->disabled_))) {
subc->_parse_arg(args, current_type);
if(subc->missing_.empty()) { // check if it was used and is not in the missing category
return;
} else {
// for unnamed subs they shouldn't trigger a missing argument
args.push_back(std::move(subc->missing_.front().second));
subc->missing_.clear();
}
@ -2006,8 +2245,8 @@ class App {
// Make sure we always eat the minimum for unlimited vectors
int collected = 0;
int result_count = 0;
// deal with flag like things
int count = 0;
if(num == 0) {
auto res = op->get_flag_value(arg_name, value);
op->add_result(res);
@ -2015,22 +2254,22 @@ class App {
}
// --this=value
else if(!value.empty()) {
op->add_result(value, count);
op->add_result(value, result_count);
parse_order_.push_back(op.get());
collected += count;
collected += result_count;
// If exact number expected
if(num > 0)
num = (num >= count) ? num - count : 0;
num = (num >= result_count) ? num - result_count : 0;
// -Trest
} else if(!rest.empty()) {
op->add_result(rest, count);
op->add_result(rest, result_count);
parse_order_.push_back(op.get());
rest = "";
collected += count;
collected += result_count;
// If exact number expected
if(num > 0)
num = (num >= count) ? num - count : 0;
num = (num >= result_count) ? num - result_count : 0;
}
// Unlimited vector parser
@ -2043,10 +2282,10 @@ class App {
if(_count_remaining_positionals() > 0)
break;
}
op->add_result(args.back(), count);
op->add_result(args.back(), result_count);
parse_order_.push_back(op.get());
args.pop_back();
collected += count;
collected += result_count;
}
// Allow -- to end an unlimited list and "eat" it
@ -2057,9 +2296,9 @@ class App {
while(num > 0 && !args.empty()) {
std::string current_ = args.back();
args.pop_back();
op->add_result(current_, count);
op->add_result(current_, result_count);
parse_order_.push_back(op.get());
num -= count;
num -= result_count;
}
if(num > 0) {
@ -2072,6 +2311,72 @@ class App {
args.push_back(rest);
}
}
public:
/// function that could be used by subclasses of App to shift options around into subcommands
void _move_option(Option *opt, App *app) {
if(opt == nullptr) {
throw OptionNotFound("the option is NULL");
}
// verify that the give app is actually a subcommand
bool found = false;
for(auto &subc : subcommands_) {
if(app == subc.get()) {
found = true;
}
}
if(!found) {
throw OptionNotFound("The Given app is not a subcommand");
}
if((help_ptr_ == opt) || (help_all_ptr_ == opt))
throw OptionAlreadyAdded("cannot move help options");
if((config_ptr_ == opt))
throw OptionAlreadyAdded("cannot move config file options");
auto iterator =
std::find_if(std::begin(options_), std::end(options_), [opt](const Option_p &v) { return v.get() == opt; });
if(iterator != std::end(options_)) {
const auto &opt_p = *iterator;
if(std::find_if(std::begin(app->options_), std::end(app->options_), [&opt_p](const Option_p &v) {
return (*v == *opt_p);
}) == std::end(app->options_)) {
// only erase after the insertion was successful
app->options_.push_back(std::move(*iterator));
options_.erase(iterator);
} else {
throw OptionAlreadyAdded(opt->get_name());
}
} else {
throw OptionNotFound("could not locate the given App");
}
}
};
/// Extension of App to better manage groups of options
class Option_group : public App {
public:
Option_group(std::string group_description, std::string group_name, App *parent)
: App(std::move(group_description), "", parent) {
group(group_name);
}
using App::add_option;
/// add an existing option to the Option_group
Option *add_option(Option *opt) {
if(get_parent() == nullptr) {
throw OptionNotFound("Unable to locate the specified option");
}
get_parent()->_move_option(opt, this);
return opt;
}
/// add an existing option to the Option_group
void add_options(Option *opt) { add_option(opt); }
/// add a bunch of options to the group
template <typename... Args> void add_options(Option *opt, Args... args) {
add_option(opt);
add_options(args...);
}
};
namespace FailureMessage {

View File

@ -212,6 +212,27 @@ class RequiredError : public ParseError {
return RequiredError("Requires at least " + std::to_string(min_subcom) + " subcommands",
ExitCodes::RequiredError);
}
static RequiredError Option(size_t min_option, size_t max_option, size_t used, const std::string &option_list) {
if((min_option == 1) && (max_option == 1) && (used == 0))
return RequiredError("Exactly 1 option from [" + option_list + "]");
else if((min_option == 1) && (max_option == 1) && (used > 1))
return RequiredError("Exactly 1 option from [" + option_list + "] is required and " + std::to_string(used) +
" were given",
ExitCodes::RequiredError);
else if((min_option == 1) && (used == 0))
return RequiredError("At least 1 option from [" + option_list + "]");
else if(used < min_option)
return RequiredError("Requires at least " + std::to_string(min_option) + " options used and only " +
std::to_string(used) + "were given from [" + option_list + "]",
ExitCodes::RequiredError);
else if(max_option == 1)
return RequiredError("Requires at most 1 options be given from [" + option_list + "]",
ExitCodes::RequiredError);
else
return RequiredError("Requires at most " + std::to_string(max_option) + " options be used and " +
std::to_string(used) + "were given from [" + option_list + "]",
ExitCodes::RequiredError);
}
};
/// Thrown when the wrong number of arguments has been received

View File

@ -58,7 +58,27 @@ inline std::string Formatter::make_groups(const App *app, AppFormatMode mode) co
inline std::string Formatter::make_description(const App *app) const {
std::string desc = app->get_description();
auto min_options = app->get_require_option_min();
auto max_options = app->get_require_option_max();
if(app->get_required()) {
desc += " REQUIRED ";
}
if((max_options == min_options) && (min_options > 0)) {
if(min_options == 1) {
desc += " \n[Exactly 1 of the following options is required]";
} else {
desc += " \n[Exactly " + std::to_string(min_options) + "options from the following list are required]";
}
} else if(max_options > 0) {
if(min_options > 0) {
desc += " \n[Between " + std::to_string(min_options) + " and " + std::to_string(max_options) +
" of the follow options are required]";
} else {
desc += " \n[At most " + std::to_string(max_options) + " of the following options are allowed]";
}
} else if(min_options > 0) {
desc += " \n[At least " + std::to_string(min_options) + " of the following options are required]";
}
return (!desc.empty()) ? desc + "\n" : std::string{};
}
@ -90,7 +110,9 @@ inline std::string Formatter::make_usage(const App *app, std::string name) const
}
// Add a marker if subcommands are expected or optional
if(!app->get_subcommands({}).empty()) {
if(!app->get_subcommands(
[](const CLI::App *subc) { return ((!subc->get_disabled()) && (!subc->get_name().empty())); })
.empty()) {
out << " " << (app->get_require_subcommand_min() == 0 ? "[" : "")
<< get_label(app->get_require_subcommand_max() < 2 || app->get_require_subcommand_min() > 1 ? "SUBCOMMAND"
: "SUBCOMMANDS")
@ -118,6 +140,11 @@ inline std::string Formatter::make_help(const App *app, std::string name, AppFor
return make_expanded(app);
std::stringstream out;
if((app->get_name().empty()) && (app->get_parent() != nullptr)) {
if(app->get_group() != "Subcommands") {
out << app->get_group() << ':';
}
}
out << make_description(app);
out << make_usage(app, name);
@ -177,7 +204,7 @@ inline std::string Formatter::make_subcommand(const App *sub) const {
inline std::string Formatter::make_expanded(const App *sub) const {
std::stringstream out;
out << sub->get_name() << "\n";
out << sub->get_display_name() << "\n";
out << make_description(sub);
out << make_positionals(sub);

View File

@ -721,13 +721,16 @@ class Option : public OptionBase<Option> {
for(const std::string &lname : lnames_)
if(other.check_lname(lname))
return true;
// We need to do the inverse, just in case we are ignore_case or ignore underscore
if(ignore_case_ ||
ignore_underscore_) { // We need to do the inverse, in case we are ignore_case or ignore underscore
for(const std::string &sname : other.snames_)
if(check_sname(sname))
return true;
for(const std::string &lname : other.lnames_)
if(check_lname(lname))
return true;
}
return false;
}
@ -814,8 +817,8 @@ class Option : public OptionBase<Option> {
}
/// Puts a result at the end and get a count of the number of arguments actually added
Option *add_result(std::string s, int &count) {
count = _add_result(std::move(s));
Option *add_result(std::string s, int &results_added) {
results_added = _add_result(std::move(s));
callback_run_ = false;
return this;
}
@ -940,24 +943,24 @@ class Option : public OptionBase<Option> {
private:
int _add_result(std::string &&result) {
int count = 0;
int result_count = 0;
if(delimiter_ == '\0') {
results_.push_back(std::move(result));
++count;
++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);
++count;
++result_count;
}
}
} else {
results_.push_back(std::move(result));
++count;
++result_count;
}
}
return count;
return result_count;
}
};

View File

@ -2,6 +2,8 @@
#include <complex>
#include <cstdlib>
#include "gmock/gmock.h"
TEST_F(TApp, OneFlagShort) {
app.add_flag("-c,--count");
args = {"-c"};
@ -118,6 +120,23 @@ TEST_F(TApp, DashedOptionsSingleString) {
EXPECT_EQ(2u, app.count("--that"));
}
TEST_F(TApp, RequireOptionsError) {
using ::testing::HasSubstr;
using ::testing::Not;
app.add_flag("-c");
app.add_flag("--q");
app.add_flag("--this,--that");
app.require_option(1, 2);
try {
app.parse("-c --q --this --that");
} catch(const CLI::RequiredError &re) {
EXPECT_THAT(re.what(), Not(HasSubstr("-h,--help")));
}
EXPECT_NO_THROW(app.parse("-c --q"));
EXPECT_NO_THROW(app.parse("-c --this --that"));
}
TEST_F(TApp, BoolFlagOverride) {
bool val;
auto flg = app.add_flag("--this,--that", val);
@ -527,6 +546,7 @@ TEST_F(TApp, LotsOfFlags) {
EXPECT_EQ(2u, app.count("-a"));
EXPECT_EQ(1u, app.count("-b"));
EXPECT_EQ(1u, app.count("-A"));
EXPECT_EQ(app.count_all(), 4u);
}
TEST_F(TApp, NumberFlags) {
@ -657,6 +677,7 @@ TEST_F(TApp, ShortOpts) {
EXPECT_EQ(1u, app.count("-y"));
EXPECT_EQ((unsigned long long)2, funnyint);
EXPECT_EQ("zyz", someopt);
EXPECT_EQ(app.count_all(), 3u);
}
TEST_F(TApp, DefaultOpts) {

View File

@ -36,6 +36,7 @@ set(CLI11_TESTS
DeprecatedTest
StringParseTest
TrueFalseTest
OptionGroupTest
)
if(WIN32)

View File

@ -154,6 +154,17 @@ TEST(Formatter, AllSub) {
EXPECT_THAT(help, HasSubstr("subcom"));
}
TEST(Formatter, AllSubRequired) {
CLI::App app{"My prog"};
CLI::App *sub = app.add_subcommand("subcom", "This");
sub->add_flag("--insub", "MyFlag");
sub->required();
std::string help = app.help("", CLI::AppFormatMode::All);
EXPECT_THAT(help, HasSubstr("--insub"));
EXPECT_THAT(help, HasSubstr("subcom"));
EXPECT_THAT(help, HasSubstr("REQUIRED"));
}
TEST(Formatter, NamelessSub) {
CLI::App app{"My prog"};
CLI::App *sub = app.add_subcommand("", "This subcommand");

503
tests/OptionGroupTest.cpp Normal file
View File

@ -0,0 +1,503 @@
#include "app_helper.hpp"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
using ::testing::HasSubstr;
using ::testing::Not;
using vs_t = std::vector<std::string>;
TEST_F(TApp, BasicOptionGroup) {
auto ogroup = app.add_option_group("clusters");
int res;
ogroup->add_option("--test1", res);
ogroup->add_option("--test2", res);
ogroup->add_option("--test3", res);
args = {"--test1", "5"};
run();
EXPECT_EQ(res, 5);
EXPECT_EQ(app.count_all(), 1u);
}
TEST_F(TApp, BasicOptionGroupExact) {
auto ogroup = app.add_option_group("clusters");
int res;
ogroup->add_option("--test1", res);
ogroup->add_option("--test2", res);
ogroup->add_option("--test3", res);
int val2;
app.add_option("--option", val2);
ogroup->require_option(1);
args = {"--test1", "5"};
run();
EXPECT_EQ(res, 5);
args = {"--test1", "5", "--test2", "4"};
EXPECT_THROW(run(), CLI::RequiredError);
args = {"--option", "9"};
EXPECT_THROW(run(), CLI::RequiredError);
std::string help = ogroup->help();
auto exactloc = help.find("[Exactly 1");
EXPECT_NE(exactloc, std::string::npos);
}
TEST_F(TApp, BasicOptionGroupExactTooMany) {
auto ogroup = app.add_option_group("clusters");
int res;
ogroup->add_option("--test1", res);
ogroup->add_option("--test2", res);
ogroup->add_option("--test3", res);
int val2;
app.add_option("--option", val2);
ogroup->require_option(10);
args = {"--test1", "5"};
EXPECT_THROW(run(), CLI::InvalidError);
}
TEST_F(TApp, BasicOptionGroupMinMax) {
auto ogroup = app.add_option_group("clusters");
int res;
ogroup->add_option("--test1", res);
ogroup->add_option("--test2", res);
ogroup->add_option("--test3", res);
int val2;
app.add_option("--option", val2);
ogroup->require_option(1, 1);
args = {"--test1", "5"};
run();
EXPECT_EQ(res, 5);
args = {"--test1", "5", "--test2", "4"};
EXPECT_THROW(run(), CLI::RequiredError);
args = {"--option", "9"};
EXPECT_THROW(run(), CLI::RequiredError);
std::string help = ogroup->help();
auto exactloc = help.find("[Exactly 1");
EXPECT_NE(exactloc, std::string::npos);
}
TEST_F(TApp, BasicOptionGroupMinMaxDifferent) {
auto ogroup = app.add_option_group("clusters");
int res;
ogroup->add_option("--test1", res);
ogroup->add_option("--test2", res);
ogroup->add_option("--test3", res);
int val2;
app.add_option("--option", val2);
ogroup->require_option(1, 2);
args = {"--test1", "5"};
run();
EXPECT_EQ(res, 5);
args = {"--test1", "5", "--test2", "4"};
EXPECT_NO_THROW(run());
EXPECT_EQ(app.count_all(), 2);
args = {"--option", "9"};
EXPECT_THROW(run(), CLI::RequiredError);
args = {"--test1", "5", "--test2", "4", "--test3=5"};
EXPECT_THROW(run(), CLI::RequiredError);
std::string help = ogroup->help();
auto exactloc = help.find("[Between 1 and 2");
EXPECT_NE(exactloc, std::string::npos);
}
TEST_F(TApp, BasicOptionGroupMinMaxDifferentReversed) {
auto ogroup = app.add_option_group("clusters");
int res;
ogroup->add_option("--test1", res);
ogroup->add_option("--test2", res);
ogroup->add_option("--test3", res);
int val2;
app.add_option("--option", val2);
ogroup->require_option(2, 1);
EXPECT_EQ(ogroup->get_require_option_min(), 2);
EXPECT_EQ(ogroup->get_require_option_max(), 1);
args = {"--test1", "5"};
EXPECT_THROW(run(), CLI::InvalidError);
ogroup->require_option(1, 2);
EXPECT_NO_THROW(run());
EXPECT_EQ(res, 5);
EXPECT_EQ(ogroup->get_require_option_min(), 1);
EXPECT_EQ(ogroup->get_require_option_max(), 2);
args = {"--test1", "5", "--test2", "4"};
EXPECT_NO_THROW(run());
args = {"--option", "9"};
EXPECT_THROW(run(), CLI::RequiredError);
args = {"--test1", "5", "--test2", "4", "--test3=5"};
EXPECT_THROW(run(), CLI::RequiredError);
std::string help = ogroup->help();
auto exactloc = help.find("[Between 1 and 2");
EXPECT_NE(exactloc, std::string::npos);
}
TEST_F(TApp, BasicOptionGroupMax) {
auto ogroup = app.add_option_group("clusters");
int res;
ogroup->add_option("--test1", res);
ogroup->add_option("--test2", res);
ogroup->add_option("--test3", res);
int val2;
app.add_option("--option", val2);
ogroup->require_option(-2);
args = {"--test1", "5"};
run();
EXPECT_EQ(res, 5);
args = {"--option", "9"};
EXPECT_NO_THROW(run());
args = {"--test1", "5", "--test2", "4", "--test3=5"};
EXPECT_THROW(run(), CLI::RequiredError);
std::string help = ogroup->help();
auto exactloc = help.find("[At most 2");
EXPECT_NE(exactloc, std::string::npos);
}
TEST_F(TApp, BasicOptionGroupMax1) {
auto ogroup = app.add_option_group("clusters");
int res;
ogroup->add_option("--test1", res);
ogroup->add_option("--test2", res);
ogroup->add_option("--test3", res);
int val2;
app.add_option("--option", val2);
ogroup->require_option(-1);
args = {"--test1", "5"};
run();
EXPECT_EQ(res, 5);
args = {"--option", "9"};
EXPECT_NO_THROW(run());
args = {"--test1", "5", "--test2", "4"};
EXPECT_THROW(run(), CLI::RequiredError);
std::string help = ogroup->help();
auto exactloc = help.find("[At most 1");
EXPECT_NE(exactloc, std::string::npos);
}
TEST_F(TApp, BasicOptionGroupMin) {
auto ogroup = app.add_option_group("clusters");
int res;
ogroup->add_option("--test1", res);
ogroup->add_option("--test2", res);
ogroup->add_option("--test3", res);
int val2;
app.add_option("--option", val2);
ogroup->require_option();
args = {"--option", "9"};
EXPECT_THROW(run(), CLI::RequiredError);
args = {"--test1", "5", "--test2", "4", "--test3=5"};
EXPECT_NO_THROW(run());
std::string help = ogroup->help();
auto exactloc = help.find("[At least 1");
EXPECT_NE(exactloc, std::string::npos);
}
TEST_F(TApp, BasicOptionGroupExact2) {
auto ogroup = app.add_option_group("clusters");
int res;
ogroup->add_option("--test1", res);
ogroup->add_option("--test2", res);
ogroup->add_option("--test3", res);
int val2;
app.add_option("--option", val2);
ogroup->require_option(2);
args = {"--option", "9"};
EXPECT_THROW(run(), CLI::RequiredError);
args = {"--test1", "5", "--test2", "4", "--test3=5"};
EXPECT_THROW(run(), CLI::RequiredError);
args = {"--test1", "5", "--test3=5"};
EXPECT_NO_THROW(run());
std::string help = ogroup->help();
auto exactloc = help.find("[Exactly 2");
EXPECT_NE(exactloc, std::string::npos);
}
TEST_F(TApp, BasicOptionGroupMin2) {
auto ogroup = app.add_option_group("clusters");
int res;
ogroup->add_option("--test1", res);
ogroup->add_option("--test2", res);
ogroup->add_option("--test3", res);
int val2;
app.add_option("--option", val2);
ogroup->require_option(2, 0);
args = {"--option", "9"};
EXPECT_THROW(run(), CLI::RequiredError);
args = {"--test1", "5", "--test2", "4", "--test3=5"};
EXPECT_NO_THROW(run());
std::string help = ogroup->help();
auto exactloc = help.find("[At least 2");
EXPECT_NE(exactloc, std::string::npos);
}
TEST_F(TApp, BasicOptionGroupMinMoved) {
int res;
auto opt1 = app.add_option("--test1", res);
auto opt2 = app.add_option("--test2", res);
auto opt3 = app.add_option("--test3", res);
int val2;
app.add_option("--option", val2);
auto ogroup = app.add_option_group("clusters");
ogroup->require_option();
ogroup->add_option(opt1);
ogroup->add_option(opt2);
ogroup->add_option(opt3);
args = {"--option", "9"};
EXPECT_THROW(run(), CLI::RequiredError);
args = {"--test1", "5", "--test2", "4", "--test3=5"};
EXPECT_NO_THROW(run());
std::string help = app.help();
auto exactloc = help.find("[At least 1");
auto oloc = help.find("--test1");
EXPECT_NE(exactloc, std::string::npos);
EXPECT_NE(oloc, std::string::npos);
EXPECT_LT(exactloc, oloc);
}
TEST_F(TApp, BasicOptionGroupMinMovedAsGroup) {
int res;
auto opt1 = app.add_option("--test1", res);
auto opt2 = app.add_option("--test2", res);
auto opt3 = app.add_option("--test3", res);
int val2;
app.add_option("--option", val2);
auto ogroup = app.add_option_group("clusters");
ogroup->require_option();
ogroup->add_options(opt1, opt2, opt3);
EXPECT_THROW(ogroup->add_options(opt1), CLI::OptionNotFound);
args = {"--option", "9"};
EXPECT_THROW(run(), CLI::RequiredError);
args = {"--test1", "5", "--test2", "4", "--test3=5"};
EXPECT_NO_THROW(run());
std::string help = app.help();
auto exactloc = help.find("[At least 1");
auto oloc = help.find("--test1");
EXPECT_NE(exactloc, std::string::npos);
EXPECT_NE(oloc, std::string::npos);
EXPECT_LT(exactloc, oloc);
}
TEST_F(TApp, BasicOptionGroupAddFailures) {
int res;
auto opt1 = app.add_option("--test1", res);
app.set_config("--config");
int val2;
app.add_option("--option", val2);
auto ogroup = app.add_option_group("clusters");
EXPECT_THROW(ogroup->add_options(app.get_config_ptr()), CLI::OptionAlreadyAdded);
EXPECT_THROW(ogroup->add_options(app.get_help_ptr()), CLI::OptionAlreadyAdded);
auto sub = app.add_subcommand("sub", "subcommand");
auto opt2 = sub->add_option("--option2", val2);
EXPECT_THROW(ogroup->add_option(opt2), CLI::OptionNotFound);
EXPECT_THROW(ogroup->add_options(nullptr), CLI::OptionNotFound);
ogroup->add_option(opt1);
auto opt3 = app.add_option("--test1", res);
EXPECT_THROW(ogroup->add_option(opt3), CLI::OptionAlreadyAdded);
}
TEST_F(TApp, BasicOptionGroupScrewedUpMove) {
int res;
auto opt1 = app.add_option("--test1", res);
auto opt2 = app.add_option("--test2", res);
int val2;
app.add_option("--option", val2);
auto ogroup = app.add_option_group("clusters");
ogroup->require_option();
auto ogroup2 = ogroup->add_option_group("clusters2");
EXPECT_THROW(ogroup2->add_options(opt1, opt2), CLI::OptionNotFound);
CLI::Option_group EmptyGroup("description", "new group", nullptr);
EXPECT_THROW(EmptyGroup.add_option(opt2), CLI::OptionNotFound);
EXPECT_THROW(app._move_option(opt2, ogroup2), CLI::OptionNotFound);
}
TEST_F(TApp, InvalidOptions) {
auto ogroup = app.add_option_group("clusters");
CLI::Option *opt = nullptr;
EXPECT_THROW(ogroup->excludes(opt), CLI::OptionNotFound);
CLI::App *app_p = nullptr;
EXPECT_THROW(ogroup->excludes(app_p), CLI::OptionNotFound);
EXPECT_THROW(ogroup->excludes(ogroup), CLI::OptionNotFound);
EXPECT_THROW(ogroup->add_option(opt), CLI::OptionNotFound);
}
struct ManyGroups : public TApp {
CLI::Option_group *main;
CLI::Option_group *g1;
CLI::Option_group *g2;
CLI::Option_group *g3;
std::string name1;
std::string name2;
std::string name3;
std::string val1;
std::string val2;
std::string val3;
ManyGroups() {
main = app.add_option_group("main", "the main outer group");
g1 = main->add_option_group("g1", "group1 description");
g2 = main->add_option_group("g2", "group2 description");
g3 = main->add_option_group("g3", "group3 description");
g1->add_option("--name1", name1)->required();
g1->add_option("--val1", val1);
g2->add_option("--name2", name2)->required();
g2->add_option("--val2", val2);
g3->add_option("--name3", name3)->required();
g3->add_option("--val3", val3);
}
void remove_required() {
g1->get_option("--name1")->required(false);
g2->get_option("--name2")->required(false);
g3->get_option("--name3")->required(false);
g1->required(false);
g2->required(false);
g3->required(false);
}
};
TEST_F(ManyGroups, SingleGroup) {
// only 1 group can be used
main->require_option(1);
args = {"--name1", "test"};
run();
EXPECT_EQ(name1, "test");
args = {"--name2", "test", "--val2", "tval"};
run();
EXPECT_EQ(val2, "tval");
args = {"--name1", "test", "--val2", "tval"};
EXPECT_THROW(run(), CLI::RequiredError);
}
TEST_F(ManyGroups, SingleGroupError) {
// only 1 group can be used
main->require_option(1);
args = {"--name1", "test", "--name2", "test3"};
EXPECT_THROW(run(), CLI::RequiredError);
}
TEST_F(ManyGroups, AtMostOneGroup) {
// only 1 group can be used
main->require_option(0, 1);
args = {"--name1", "test", "--name2", "test3"};
EXPECT_THROW(run(), CLI::RequiredError);
args = {};
EXPECT_NO_THROW(run());
}
TEST_F(ManyGroups, AtLeastTwoGroups) {
// only 1 group can be used
main->require_option(2, 0);
args = {"--name1", "test", "--name2", "test3"};
run();
args = {"--name1", "test"};
EXPECT_THROW(run(), CLI::RequiredError);
}
TEST_F(ManyGroups, BetweenOneAndTwoGroups) {
// only 1 group can be used
main->require_option(1, 2);
args = {"--name1", "test", "--name2", "test3"};
run();
args = {"--name1", "test"};
run();
args = {};
EXPECT_THROW(run(), CLI::RequiredError);
args = {"--name1", "test", "--name2", "test3", "--name3=test3"};
EXPECT_THROW(run(), CLI::RequiredError);
}
TEST_F(ManyGroups, RequiredFirst) {
// only 1 group can be used
remove_required();
g1->required();
EXPECT_TRUE(g1->get_required());
EXPECT_FALSE(g2->get_required());
args = {"--name1", "test", "--name2", "test3"};
run();
args = {"--name2", "test"};
try {
run();
} catch(const CLI::RequiredError &re) {
EXPECT_THAT(re.what(), HasSubstr("g1"));
}
args = {"--name1", "test", "--name2", "test3", "--name3=test3"};
EXPECT_NO_THROW(run());
}
TEST_F(ManyGroups, DisableFirst) {
// only 1 group can be used
remove_required();
g1->disabled();
EXPECT_TRUE(g1->get_disabled());
EXPECT_FALSE(g2->get_disabled());
args = {"--name2", "test"};
run();
args = {"--name1", "test", "--name2", "test3"};
EXPECT_THROW(run(), CLI::ExtrasError);
g1->disabled(false);
args = {"--name1", "test", "--name2", "test3", "--name3=test3"};
EXPECT_NO_THROW(run());
}

View File

@ -837,6 +837,7 @@ TEST_F(ManySubcommands, MaxCommands) {
args = {"sub1", "sub2", "sub3"};
EXPECT_NO_THROW(run());
EXPECT_EQ(sub2->remaining().size(), 1u);
EXPECT_EQ(app.count_all(), 2u);
// Currently, setting sub2 to throw causes an extras error
// In the future, would passing on up to app's extras be better?
@ -853,6 +854,78 @@ TEST_F(ManySubcommands, MaxCommands) {
EXPECT_THROW(run(), CLI::ExtrasError);
}
TEST_F(ManySubcommands, SubcommandExclusion) {
sub1->excludes(sub3);
sub2->excludes(sub3);
args = {"sub1", "sub2"};
EXPECT_NO_THROW(run());
args = {"sub1", "sub2", "sub3"};
EXPECT_THROW(run(), CLI::ExcludesError);
args = {"sub1", "sub2", "sub4"};
EXPECT_NO_THROW(run());
EXPECT_EQ(app.count_all(), 3u);
args = {"sub3", "sub4"};
EXPECT_NO_THROW(run());
}
TEST_F(ManySubcommands, SubcommandOptionExclusion) {
auto excluder_flag = app.add_flag("--exclude");
sub1->excludes(excluder_flag)->fallthrough();
sub2->excludes(excluder_flag)->fallthrough();
sub3->fallthrough();
sub4->fallthrough();
args = {"sub3", "sub4", "--exclude"};
EXPECT_NO_THROW(run());
// the option comes later so doesn't exclude
args = {"sub1", "sub3", "--exclude"};
EXPECT_THROW(run(), CLI::ExcludesError);
args = {"--exclude", "sub2", "sub4"};
EXPECT_THROW(run(), CLI::ExcludesError);
args = {"sub1", "--exclude", "sub2", "sub4"};
try {
run();
} catch(const CLI::ExcludesError &ee) {
EXPECT_NE(std::string(ee.what()).find("sub1"), std::string::npos);
}
}
TEST_F(ManySubcommands, SubcommandRequired) {
sub1->required();
args = {"sub1", "sub2"};
EXPECT_NO_THROW(run());
args = {"sub1", "sub2", "sub3"};
EXPECT_NO_THROW(run());
args = {"sub3", "sub4"};
EXPECT_THROW(run(), CLI::RequiredError);
}
TEST_F(ManySubcommands, SubcommandDisabled) {
sub3->disabled();
args = {"sub1", "sub2"};
EXPECT_NO_THROW(run());
args = {"sub1", "sub2", "sub3"};
app.allow_extras(false);
sub2->allow_extras(false);
EXPECT_THROW(run(), CLI::ExtrasError);
args = {"sub3", "sub4"};
EXPECT_THROW(run(), CLI::ExtrasError);
sub3->disabled(false);
args = {"sub3", "sub4"};
EXPECT_NO_THROW(run());
}
TEST_F(TApp, UnnamedSub) {
double val;
auto sub = app.add_subcommand("", "empty name");
@ -886,6 +959,7 @@ TEST_F(TApp, UnnamedSubMix) {
EXPECT_EQ(val, -3.0);
EXPECT_EQ(val2, 5.93);
EXPECT_EQ(val3, 4.56);
EXPECT_EQ(app.count_all(), 3u);
}
TEST_F(TApp, UnnamedSubMixExtras) {

View File

@ -41,7 +41,7 @@ class TempFile {
};
inline void put_env(std::string name, std::string value) {
#ifdef _MSC_VER
#ifdef _WIN32
_putenv_s(name.c_str(), value.c_str());
#else
setenv(name.c_str(), value.c_str(), 1);
@ -49,7 +49,7 @@ inline void put_env(std::string name, std::string value) {
}
inline void unset_env(std::string name) {
#ifdef _MSC_VER
#ifdef _WIN32
_putenv_s(name.c_str(), "");
#else
unsetenv(name.c_str());