Skip to content

Commit 2cd58ef

Browse files
phlptphenryiii
authored andcommitted
add a test of duplicate named subcommands in different option_groups and make sure that executes them over running the same one twice. (#247)
make duplicate subcommands work
1 parent b8ebce7 commit 2cd58ef

File tree

4 files changed

+87
-20
lines changed

4 files changed

+87
-20
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ Before parsing, you can set the following options:
283283
- `->envname(name)`: Gets the value from the environment if present and not passed on the command line.
284284
- `->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).
285285
- `->ignore_case()`: Ignore the case on the command line (also works on subcommands, does not affect arguments).
286-
- `->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
286+
- `->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
287287
- `->disable_flag_override()`: 🚧 from the command line long form flag option can be assigned a value on the command line using the `=` notation `--flag=value`. If this behavior is not desired, the `disable_flag_override()` disables it and will generate an exception if it is done on the command line. The `=` does not work with short form flag options.
288288
- `->delimiter(char)`: 🚧 allows specification of a custom delimiter for separating single arguments into vector arguments, for example specifying `->delimiter(',')` on an option would result in `--opt=1,2,3` producing 3 elements of a vector and the equivalent of --opt 1 2 3 assuming opt is a vector value
289289
- `->description(str)`: 🆕 Set/change the description.
@@ -379,7 +379,7 @@ of `IsMember`:
379379
* `auto p = std::make_shared<std::vector<std::string>>(std::initializer_list<std::string>("one", "two")); CLI::IsMember(p)`: You can modify `p` later.
380380
* 🚧 The `Transformer` and `CheckedTransformer` Validators transform one value into another. Any container or copyable pointer (including `std::shared_ptr`) to a container that generates pairs of values can be passed to these `Validator's`; the container just needs to be iterable and have a `::value_type` that consists of pairs. The key type should be convertible from a string, and the value type should be convertible to a string You can use an initializer list directly if you like. If you need to modify the map later, the pointer form lets you do that; the description message will correctly refer to the current version of the map. `Transformer` does not do any checking so values not in the map are ignored. `CheckedTransformer` takes an extra step of verifying that the value is either one of the map key values, or one of the expected output values, and if not will generate a ValidationError. A Transformer placed using `check` will not do anything.
381381
After specifying a map of options, you can also specify "filter" just like in CLI::IsMember.
382-
Here are some examples (`Transfomer` and `CheckedTransformer` are interchangeable in the examples)
382+
Here are some examples (`Transformer` and `CheckedTransformer` are interchangeable in the examples)
383383
of `Transformer`:
384384

385385
* `CLI::Transformer({{"key1", "map1"},{"key2","map2"}})`: Select from key values and produce map values.
@@ -453,7 +453,7 @@ All `App`s have a `get_subcommands()` method, which returns a list of pointers t
453453
For many cases, however, using an app's callback may be easier. Every app executes a callback function after it parses; just use a lambda function (with capture to get parsed values) to `.callback`. If you throw `CLI::Success` or `CLI::RuntimeError(return_value)`, you can
454454
even exit the program through the callback. The main `App` has a callback slot, as well, but it is generally not as useful.
455455
You are allowed to throw `CLI::Success` in the callbacks.
456-
Multiple subcommands are allowed, to allow [`Click`][click] like series of commands (order is preserved).
456+
Multiple subcommands are allowed, to allow [`Click`][click] like series of commands (order is preserved). The same subcommand can be triggered multiple times but all positional arguments will take precedence over the second and future calls of the subcommand. `->count()` on the subcommand will return the number of times the subcommand was called. The subcommand callback will only be triggered once.
457457
458458
🚧 Subcommands may also have an empty name either by calling `add_subcommand` with an empty string for the name or with no arguments.
459459
Nameless subcommands function a similarly to groups in the main `App`. See [Option groups](#option-groups) to see how this might work. 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.

include/CLI/App.hpp

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1045,7 +1045,7 @@ class App {
10451045

10461046
/// Check to see if a subcommand is part of this command (text version)
10471047
App *get_subcommand(std::string subcom) const {
1048-
auto subc = _find_subcommand(subcom, false);
1048+
auto subc = _find_subcommand(subcom, false, false);
10491049
if(subc == nullptr)
10501050
throw OptionNotFound(subcom);
10511051
return subc;
@@ -1572,7 +1572,7 @@ class App {
15721572
/// Get the status of required
15731573
bool get_required() const { return required_; }
15741574

1575-
/// Get the status of required
1575+
/// Get the status of disabled
15761576
bool get_disabled() const { return disabled_; }
15771577

15781578
/// Get the status of allow extras
@@ -1736,8 +1736,8 @@ class App {
17361736
if(require_subcommand_max_ != 0 && parsed_subcommands_.size() >= require_subcommand_max_) {
17371737
return parent_ != nullptr && parent_->_valid_subcommand(current);
17381738
}
1739-
auto com = _find_subcommand(current, true);
1740-
if((com != nullptr) && !*com) {
1739+
auto com = _find_subcommand(current, true, true);
1740+
if(com != nullptr) {
17411741
return true;
17421742
}
17431743
// Check parent if exists, else return false
@@ -2135,33 +2135,49 @@ class App {
21352135
if(parent_ != nullptr && fallthrough_)
21362136
return parent_->_parse_positional(args);
21372137
else {
2138-
if(positionals_at_end_) {
2139-
throw CLI::ExtrasError(args);
2138+
/// now try one last gasp at subcommands that have been executed before, go to root app and try to find a
2139+
/// subcommand in a broader way
2140+
auto parent_app = this;
2141+
while(parent_app->parent_ != nullptr) {
2142+
parent_app = parent_app->parent_;
21402143
}
2141-
args.pop_back();
2142-
missing_.emplace_back(detail::Classifier::NONE, positional);
2144+
auto com = parent_app->_find_subcommand(args.back(), true, false);
2145+
if((com != nullptr) &&
2146+
((com->parent_->require_subcommand_max_ == 0) ||
2147+
(com->parent_->require_subcommand_max_ > com->parent_->parsed_subcommands_.size()))) {
2148+
args.pop_back();
2149+
com->_parse(args);
2150+
} else {
2151+
if(positionals_at_end_) {
2152+
throw CLI::ExtrasError(args);
2153+
}
2154+
args.pop_back();
2155+
missing_.emplace_back(detail::Classifier::NONE, positional);
21432156

2144-
if(prefix_command_) {
2145-
while(!args.empty()) {
2146-
missing_.emplace_back(detail::Classifier::NONE, args.back());
2147-
args.pop_back();
2157+
if(prefix_command_) {
2158+
while(!args.empty()) {
2159+
missing_.emplace_back(detail::Classifier::NONE, args.back());
2160+
args.pop_back();
2161+
}
21482162
}
21492163
}
21502164
}
21512165
}
21522166

2153-
/// Locate a subcommand by name
2154-
App *_find_subcommand(const std::string &subc_name, bool ignore_disabled) const noexcept {
2167+
/// Locate a subcommand by name with two conditions, should disabled subcommands be ignored, and should used
2168+
/// subcommands be ignored
2169+
App *_find_subcommand(const std::string &subc_name, bool ignore_disabled, bool ignore_used) const noexcept {
21552170
for(const App_p &com : subcommands_) {
21562171
if((com->disabled_) && (ignore_disabled))
21572172
continue;
21582173
if(com->get_name().empty()) {
2159-
auto subc = com->_find_subcommand(subc_name, ignore_disabled);
2174+
auto subc = com->_find_subcommand(subc_name, ignore_disabled, ignore_used);
21602175
if(subc != nullptr) {
21612176
return subc;
21622177
}
21632178
} else if(com->check_name(subc_name)) {
2164-
return com.get();
2179+
if((!*com) || (!ignore_used))
2180+
return com.get();
21652181
}
21662182
}
21672183
return nullptr;
@@ -2173,7 +2189,7 @@ class App {
21732189
void _parse_subcommand(std::vector<std::string> &args) {
21742190
if(_count_remaining_positionals(/* required */ true) > 0)
21752191
return _parse_positional(args);
2176-
auto com = _find_subcommand(args.back(), true);
2192+
auto com = _find_subcommand(args.back(), true, true);
21772193
if(com != nullptr) {
21782194
args.pop_back();
21792195
if(std::find(std::begin(parsed_subcommands_), std::end(parsed_subcommands_), com) ==

tests/OptionGroupTest.cpp

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -501,3 +501,39 @@ TEST_F(ManyGroups, DisableFirst) {
501501
args = {"--name1", "test", "--name2", "test3", "--name3=test3"};
502502
EXPECT_NO_THROW(run());
503503
}
504+
505+
TEST_F(ManyGroups, SameSubcommand) {
506+
// only 1 group can be used
507+
remove_required();
508+
auto sub1 = g1->add_subcommand("sub1");
509+
auto sub2 = g2->add_subcommand("sub1");
510+
auto sub3 = g3->add_subcommand("sub1");
511+
512+
args = {"sub1", "sub1", "sub1"};
513+
514+
run();
515+
516+
EXPECT_TRUE(*sub1);
517+
EXPECT_TRUE(*sub2);
518+
EXPECT_TRUE(*sub3);
519+
/// This should be made to work at some point
520+
/*auto subs = app.get_subcommands();
521+
EXPECT_EQ(subs.size(), 3u);
522+
EXPECT_EQ(subs[0], sub1);
523+
EXPECT_EQ(subs[1], sub2);
524+
EXPECT_EQ(subs[2], sub3);
525+
*/
526+
args = {"sub1", "sub1", "sub1", "sub1"};
527+
// for the 4th and future ones they will route to the first one
528+
run();
529+
EXPECT_EQ(sub1->count(), 2u);
530+
EXPECT_EQ(sub2->count(), 1u);
531+
EXPECT_EQ(sub3->count(), 1u);
532+
533+
// subs should remain the same since the duplicate would not be registered there
534+
/*subs = app.get_subcommands();
535+
EXPECT_EQ(subs.size(), 3u);
536+
EXPECT_EQ(subs[0], sub1);
537+
EXPECT_EQ(subs[1], sub2);
538+
EXPECT_EQ(subs[2], sub3);*/
539+
}

tests/SubcommandTest.cpp

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,21 @@ TEST_F(TApp, FooFooProblem) {
163163
EXPECT_EQ(other_str, "");
164164
}
165165

166+
TEST_F(TApp, DuplicateSubcommands) {
167+
168+
auto foo = app.add_subcommand("foo");
169+
170+
args = {"foo", "foo"};
171+
run();
172+
EXPECT_TRUE(*foo);
173+
EXPECT_EQ(foo->count(), 2);
174+
175+
args = {"foo", "foo", "foo"};
176+
run();
177+
EXPECT_TRUE(*foo);
178+
EXPECT_EQ(foo->count(), 3);
179+
}
180+
166181
TEST_F(TApp, Callbacks) {
167182
auto sub1 = app.add_subcommand("sub1");
168183
sub1->callback([]() { throw CLI::Success(); });

0 commit comments

Comments
 (0)