Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add unnamed subcommand #216

Merged
merged 1 commit into from
Feb 6, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
henryiii marked this conversation as resolved.
Show resolved Hide resolved
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 {
henryiii marked this conversation as resolved.
Show resolved Hide resolved
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;
henryiii marked this conversation as resolved.
Show resolved Hide resolved
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