Skip to content

Commit

Permalink
Add unnamed subcommand (CLIUtils#216)
Browse files Browse the repository at this point in the history
increment the parse_ variable on unnamed subcommands. 

update the readme, and add a formatter test for nameless subcommands in nondefault group with other named subcommands.

add a test of default arguments

add a formatter test

add tests for unnamed subcommands and an example of the partitioned subcommands.

change the app_p to be a shared_ptr so you can add an App later on and merge them together

add the ability to add unnamed subcommands that allow partitioning on options into multiple apps.
  • Loading branch information
phlptp authored and henryiii committed Feb 6, 2019
1 parent b4910df commit 598046c
Show file tree
Hide file tree
Showing 7 changed files with 408 additions and 26 deletions.
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ An acceptable CLI parser library should be all of the following:
- Usable subcommand syntax, with support for multiple subcommands, nested subcommands, 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 extendible to exotic types.
- Work with standard types, simple custom types, and extensible to exotic types.
- Permissively licensed.

### Other parsers
Expand All @@ -92,7 +92,7 @@ After I wrote this, I also found the following libraries:
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| [GFlags][] | The Google Commandline Flags library. Uses macros heavily, and is limited in scope, missing things like subcommands. It provides a simple syntax and supports config files/env vars. |
| [GetOpt][] | Very limited C solution with long, convoluted syntax. Does not support much of anything, like help generation. Always available on UNIX, though (but in different flavors). |
| [ProgramOptions.hxx][] | Intresting library, less powerful and no subcommands. Nice callback system. |
| [ProgramOptions.hxx][] | Interesting library, less powerful and no subcommands. Nice callback system. |
| [Args][] | Also interesting, and supports subcommands. I like the optional-like design, but CLI11 is cleaner and provides direct value access, and is less verbose. |
| [Argument Aggregator][] | I'm a big fan of the [fmt][] library, and the try-catch statement looks familiar. :thumbsup: Doesn't seem to support subcommands. |
| [Clara][] | Simple library built for the excellent [Catch][] testing framework. Unique syntax, limited scope. |
Expand Down Expand Up @@ -239,7 +239,7 @@ Before parsing, you can set the following options:
- `->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"`. `""` will not show up in the help print (hidden).
- `->ignore_case()`: Ignore the case on the command line (also works on subcommands, does not affect arguments).
- `->ignore_underscore()`: Ignore any underscores in the options names (also works on subcommands, does not affect arguments). For example "option_one" will match with optionone. This does not apply to short form options since they only have one character
- `->ignore_underscore()`: Ignore any underscores in the options names (also works on subcommands, does not affect arguments). For example "option_one" will match with "optionone". This does not apply to short form options since they only have one character
- `->description(str)`: Set/change the description.
- `->multi_option_policy(CLI::MultiOptionPolicy::Throw)`: Set the multi-option policy. Shortcuts available: `->take_last()`, `->take_first()`, and `->join()`. This will only affect options expecting 1 argument or bool flags (which always default to take last).
- `->check(CLI::ExistingFile)`: Requires that the file exists if given.
Expand Down Expand Up @@ -285,7 +285,7 @@ Subcommands are supported, and can be nested infinitely. To add a subcommand, ca
case).

If you want to require that at least one subcommand is given, use `.require_subcommand()` on the parent app. You can optionally give an exact number of subcommands to require, as well. If you give two arguments, that sets the min and max number allowed.
0 for the max number allowed will allow an unlimited number of subcommands. As a handy shortcut, a single negative value N will set "up to N" values. Limiting the maximimum number allows you to keep arguments that match a previous
0 for the max number allowed will allow an unlimited number of subcommands. As a handy shortcut, a single negative value N will set "up to N" values. Limiting the maximum number allows you to keep arguments that match a previous
subcommand name from matching.

If an `App` (main or subcommand) has been parsed on the command line, `->parsed` will be true (or convert directly to bool).
Expand All @@ -296,6 +296,9 @@ even exit the program through the callback. The main `App` has a callback slot,
You are allowed to throw `CLI::Success` in the callbacks.
Multiple subcommands are allowed, to allow [`Click`][click] like series of commands (order is preserved).

Subcommands may also have an empty name either by calling `add_subcommand` with an empty string for the name or with no arguments.
Nameless subcommands function a little like groups in the main `App`. If an option is not defined in the main App, all nameless subcommands are checked as well. This allows for the options to be defined in a composable group. The `add_subcommand` function has an overload for adding a `shared_ptr<App>` so the subcommand(s) could be defined in different components and merged into a main `App`, or possibly multiple `Apps`. Multiple nameless subcommands are allowed.

#### Subcommand options

There are several options that are supported on the main app and subcommands. These are:
Expand All @@ -307,7 +310,8 @@ There are several options that are supported on the main app and subcommands. Th
- `.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(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_parent()`: Get the parent App or nullptr if called on master App.
Expand Down
18 changes: 18 additions & 0 deletions examples/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,24 @@ set_property(TEST subcommands_all PROPERTY PASS_REGULAR_EXPRESSION
"Subcommand: start"
"Subcommand: stop")

add_cli_exe(subcom_partitioned subcom_partitioned.cpp)
add_test(NAME subcom_partitioned_none COMMAND subcom_partitioned)
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:"
"Working on file: this, direct count: 1, opt count: 1"
"Working on count: 2, direct count: 2, opt count: 2"
"Some value: 1.2")
# test shows that the help prints out for unnamed subcommands
add_test(NAME subcom_partitioned_help COMMAND subcom_partitioned --help)
set_property(TEST subcom_partitioned_help PROPERTY PASS_REGULAR_EXPRESSION
"-f,--file TEXT REQUIRED"
"-d,--double FLOAT")

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
37 changes: 37 additions & 0 deletions examples/subcom_partitioned.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#include "CLI/CLI.hpp"
#include "CLI/Timer.hpp"

int main(int argc, char **argv) {
CLI::AutoTimer("This is a timer");

CLI::App app("K3Pi goofit fitter");

CLI::App_p impOpt = std::make_shared<CLI::App>("Important");
std::string file;
CLI::Option *opt = impOpt->add_option("-f,--file,file", file, "File name")->required();

int count;
CLI::Option *copt = impOpt->add_flag("-c,--count", count, "Counter")->required();

CLI::App_p otherOpt = std::make_shared<CLI::App>("Other");
double value; // = 3.14;
otherOpt->add_option("-d,--double", value, "Some Value");

// add the subapps to the main one
app.add_subcommand(impOpt);
app.add_subcommand(otherOpt);

try {
app.parse(argc, argv);
} catch(const CLI::ParseError &e) {
return app.exit(e);
}

std::cout << "Working on file: " << file << ", direct count: " << impOpt->count("--file")
<< ", opt count: " << opt->count() << std::endl;
std::cout << "Working on count: " << count << ", direct count: " << impOpt->count("--count")
<< ", opt count: " << copt->count() << std::endl;
std::cout << "Some value: " << value << std::endl;

return 0;
}
143 changes: 122 additions & 21 deletions include/CLI/App.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ std::string help(const App *app, const Error &e);

class App;

using App_p = std::unique_ptr<App>;
using App_p = std::shared_ptr<App>;

/// 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
Expand Down Expand Up @@ -77,9 +77,12 @@ class App {
/// If true, allow extra arguments in the ini file (ie, don't throw an error). INHERITABLE
bool allow_config_extras_{false};

/// If true, return immediately on an unrecognised option (implies allow_extras) INHERITABLE
/// 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
bool has_automatic_name_{false};

/// This is a function that runs when complete. Great for subcommands. Can throw.
std::function<void()> callback_;

Expand Down Expand Up @@ -244,6 +247,7 @@ class App {
/// Set a name for the app (empty will use parser to set the name)
App *name(std::string app_name = "") {
name_ = app_name;
has_automatic_name_ = false;
return this;
}

Expand Down Expand Up @@ -1124,17 +1128,29 @@ class App {
///@{

/// Add a subcommand. Inherits INHERITABLE and OptionDefaults, and help flag
App *add_subcommand(std::string subcommand_name, std::string description = "") {
CLI::App_p subcom(new App(description, subcommand_name, this));
for(const auto &subc : subcommands_)
if(subc->check_name(subcommand_name) || subcom->check_name(subc->name_))
throw OptionAlreadyAdded(subc->name_);
App *add_subcommand(std::string subcommand_name = "", std::string description = "") {
CLI::App_p subcom = std::shared_ptr<App>(new App(description, subcommand_name, this));
return add_subcommand(std::move(subcom));
}

/// Add a previously created app as a subcommand
App *add_subcommand(CLI::App_p subcom) {
if(!subcom)
throw IncorrectConstruction("passed App is not valid");
if(!subcom->name_.empty()) {
for(const auto &subc : subcommands_)
if(subc->check_name(subcom->name_) || subcom->check_name(subc->name_))
throw OptionAlreadyAdded(subc->name_);
}
subcom->parent_ = this;
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 {
if(subcom == nullptr)
throw OptionNotFound("nullptr passed");
for(const App_p &subcomptr : subcommands_)
if(subcomptr.get() == subcom)
return subcom;
Expand All @@ -1148,9 +1164,41 @@ class App {
return subcomptr.get();
throw OptionNotFound(subcom);
}
/// Get a pointer to subcommand by index
App *get_subcommand(int index = 0) const {
if((index >= 0) && (index < subcommands_.size()))
return subcommands_[index].get();
throw OptionNotFound(std::to_string(index));
}

/// Check to see if a subcommand is part of this command and get a shared_ptr to it
CLI::App_p get_subcommand_ptr(App *subcom) const {
if(subcom == nullptr)
throw OptionNotFound("nullptr passed");
for(const App_p &subcomptr : subcommands_)
if(subcomptr.get() == subcom)
return subcomptr;
throw OptionNotFound(subcom->get_name());
}

/// Check to see if a subcommand is part of this command (text version)
CLI::App_p get_subcommand_ptr(std::string subcom) const {
for(const App_p &subcomptr : subcommands_)
if(subcomptr->check_name(subcom))
return subcomptr;
throw OptionNotFound(subcom);
}

/// Get an owning pointer to subcommand by index
CLI::App_p get_subcommand_ptr(int index = 0) const {
if((index >= 0) && (index < subcommands_.size()))
return subcommands_[index];
throw OptionNotFound(std::to_string(index));
}

/// No argument version of count counts the number of times this subcommand was
/// passed in. The main app will return 1.
/// passed in. The main app will return 1. Unnamed subcommands will also return 1 unless
/// otherwise modified in a callback
size_t count() const { return parsed_; }

/// Changes the group membership
Expand Down Expand Up @@ -1215,10 +1263,9 @@ class App {
/// Reset the parsed data
void clear() {

parsed_ = false;
parsed_ = 0;
missing_.clear();
parsed_subcommands_.clear();

for(const Option_p &opt : options_) {
opt->clear();
}
Expand All @@ -1231,8 +1278,10 @@ class App {
/// This must be called after the options are in but before the rest of the program.
void parse(int argc, const char *const *argv) {
// If the name is not set, read from command line
if(name_.empty())
if((name_.empty()) || (has_automatic_name_)) {
has_automatic_name_ = true;
name_ = argv[0];
}

std::vector<std::string> args;
for(int i = argc - 1; i > 0; i--)
Expand All @@ -1248,7 +1297,8 @@ class App {

if(program_name_included) {
auto nstr = detail::split_program_name(commandline);
if(name_.empty()) {
if((name_.empty()) || (has_automatic_name_)) {
has_automatic_name_ = true;
name_ = nstr.first;
}
commandline = std::move(nstr.second);
Expand Down Expand Up @@ -1276,11 +1326,14 @@ class App {
if(parsed_ > 0)
clear();

// _parse is incremented in commands/subcommands,
// parsed_ is incremented in commands/subcommands,
// but placed here to make sure this is cleared when
// running parse after an error is thrown, even by _validate.
// running parse after an error is thrown, even by _validate or _configure.
parsed_ = 1;
_validate();
_configure();
// set the parent as nullptr as this object should be the top now
parent_ = nullptr;
parsed_ = 0;

_parse(args);
Expand Down Expand Up @@ -1599,10 +1652,28 @@ class App {
});
if(pcount > 1)
throw InvalidError(name_);
for(const App_p &app : subcommands_)
for(const App_p &app : subcommands_) {
app->_validate();
}
}

/// configure subcommands to enable parsing through the current object
/// set the correct fallthrough and prefix for nameless subcommands and
/// makes sure parent is set correctly
void _configure() {
for(const App_p &app : subcommands_) {
if(app->has_automatic_name_) {
app->name_.clear();
}
if(app->name_.empty()) {
app->fallthrough_ = false; // make sure fallthrough_ is false to prevent infinite loop
app->prefix_command_ = false;
}
// make sure the parent is set to be this object in preparation for parse
app->parent_ = this;
app->_configure();
}
}
/// Internal function to run (App) callback, top down
void run_callback() {
pre_callback();
Expand Down Expand Up @@ -1768,7 +1839,7 @@ class App {
// Max error cannot occur, the extra subcommand will parse as an ExtrasError or a remaining item.

for(App_p &sub : subcommands_) {
if(sub->count() > 0)
if((sub->count() > 0) || (sub->name_.empty()))
sub->_process_requirements();
}
}
Expand Down Expand Up @@ -1799,9 +1870,17 @@ class App {
}
}

/// Internal function to recursively increment the parsed counter on the current app as well unnamed subcommands
void increment_parsed() {
++parsed_;
for(App_p &sub : subcommands_) {
if(sub->get_name().empty())
sub->increment_parsed();
}
}
/// Internal parse function
void _parse(std::vector<std::string> &args) {
parsed_++;
increment_parsed();
bool positional_only = false;

while(!args.empty()) {
Expand Down Expand Up @@ -1833,13 +1912,12 @@ class App {
/// Fill in a single config option
bool _parse_single_config(const ConfigItem &item, size_t level = 0) {
if(level < item.parents.size()) {
App *subcom;
try {
subcom = get_subcommand(item.parents.at(level));
auto subcom = get_subcommand(item.parents.at(level));
return subcom->_parse_single_config(item, level + 1);
} catch(const OptionNotFound &) {
return false;
}
return subcom->_parse_single_config(item, level + 1);
}

Option *op;
Expand Down Expand Up @@ -1922,6 +2000,18 @@ class App {
}
}

for(auto &subc : subcommands_) {
if(subc->name_.empty()) {
subc->_parse_positional(args);
if(subc->missing_.empty()) { // check if it was used and is not in the missing category
return;
} else {
args.push_back(std::move(subc->missing_.front().second));
subc->missing_.clear();
}
}
}

if(parent_ != nullptr && fallthrough_)
return parent_->_parse_positional(args);
else {
Expand Down Expand Up @@ -1997,6 +2087,17 @@ class App {

// Option not found
if(op_ptr == std::end(options_)) {
for(auto &subc : subcommands_) {
if(subc->name_.empty()) {
subc->_parse_arg(args, current_type);
if(subc->missing_.empty()) { // check if it was used and is not in the missing category
return;
} else {
args.push_back(std::move(subc->missing_.front().second));
subc->missing_.clear();
}
}
}
// If a subcommand, try the master command
if(parent_ != nullptr && fallthrough_)
return parent_->_parse_arg(args, current_type);
Expand Down
Loading

0 comments on commit 598046c

Please sign in to comment.