Skip to content

Commit

Permalink
Option groups (CLIUtils#227)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
phlptp authored and henryiii committed Mar 1, 2019
1 parent 4a46081 commit 0631189
Show file tree
Hide file tree
Showing 14 changed files with 1,158 additions and 59 deletions.
63 changes: 50 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -53,23 +57,23 @@ 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?

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.
- Nice error messages.
- 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.
Expand Down Expand Up @@ -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
Expand All @@ -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.

Expand All @@ -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.

Expand Down Expand Up @@ -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

Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -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
Expand Down
25 changes: 25 additions & 0 deletions examples/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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:"
Expand All @@ -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
Expand Down
38 changes: 38 additions & 0 deletions examples/option_groups.cpp
Original file line number Diff line number Diff line change
@@ -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 changes: 33 additions & 0 deletions examples/ranges.cpp
Original file line number Diff line number Diff line change
@@ -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;
}
Loading

0 comments on commit 0631189

Please sign in to comment.