Commit 2cd58ef8cf185c74a7263b7e0e3b37aadd396f6e

Authored by Philip Top
Committed by Henry Schreiner
1 parent b8ebce75

add a test of duplicate named subcommands in different option_groups and make su…

…re that executes them over running the same one twice. (#247)

make duplicate subcommands work
README.md
@@ -283,7 +283,7 @@ Before parsing, you can set the following options: @@ -283,7 +283,7 @@ Before parsing, you can set the following options:
283 - `->envname(name)`: Gets the value from the environment if present and not passed on the command line. 283 - `->envname(name)`: Gets the value from the environment if present and not passed on the command line.
284 - `->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). 284 - `->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).
285 - `->ignore_case()`: Ignore the case on the command line (also works on subcommands, does not affect arguments). 285 - `->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
287 - `->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. 287 - `->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.
288 - `->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 288 - `->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
289 - `->description(str)`: πŸ†• Set/change the description. 289 - `->description(str)`: πŸ†• Set/change the description.
@@ -379,7 +379,7 @@ of `IsMember`: @@ -379,7 +379,7 @@ of `IsMember`:
379 * `auto p = std::make_shared<std::vector<std::string>>(std::initializer_list<std::string>("one", "two")); CLI::IsMember(p)`: You can modify `p` later. 379 * `auto p = std::make_shared<std::vector<std::string>>(std::initializer_list<std::string>("one", "two")); CLI::IsMember(p)`: You can modify `p` later.
380 * 🚧 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. 380 * 🚧 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.
381 After specifying a map of options, you can also specify "filter" just like in CLI::IsMember. 381 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)
383 of `Transformer`: 383 of `Transformer`:
384 384
385 * `CLI::Transformer({{"key1", "map1"},{"key2","map2"}})`: Select from key values and produce map values. 385 * `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 @@ -453,7 +453,7 @@ All `App`s have a `get_subcommands()` method, which returns a list of pointers t
453 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 453 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
454 even exit the program through the callback. The main `App` has a callback slot, as well, but it is generally not as useful. 454 even exit the program through the callback. The main `App` has a callback slot, as well, but it is generally not as useful.
455 You are allowed to throw `CLI::Success` in the callbacks. 455 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.
457 457
458 🚧 Subcommands may also have an empty name either by calling `add_subcommand` with an empty string for the name or with no arguments. 458 🚧 Subcommands may also have an empty name either by calling `add_subcommand` with an empty string for the name or with no arguments.
459 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. 459 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
@@ -1045,7 +1045,7 @@ class App { @@ -1045,7 +1045,7 @@ class App {
1045 1045
1046 /// Check to see if a subcommand is part of this command (text version) 1046 /// Check to see if a subcommand is part of this command (text version)
1047 App *get_subcommand(std::string subcom) const { 1047 App *get_subcommand(std::string subcom) const {
1048 - auto subc = _find_subcommand(subcom, false); 1048 + auto subc = _find_subcommand(subcom, false, false);
1049 if(subc == nullptr) 1049 if(subc == nullptr)
1050 throw OptionNotFound(subcom); 1050 throw OptionNotFound(subcom);
1051 return subc; 1051 return subc;
@@ -1572,7 +1572,7 @@ class App { @@ -1572,7 +1572,7 @@ class App {
1572 /// Get the status of required 1572 /// Get the status of required
1573 bool get_required() const { return required_; } 1573 bool get_required() const { return required_; }
1574 1574
1575 - /// Get the status of required 1575 + /// Get the status of disabled
1576 bool get_disabled() const { return disabled_; } 1576 bool get_disabled() const { return disabled_; }
1577 1577
1578 /// Get the status of allow extras 1578 /// Get the status of allow extras
@@ -1736,8 +1736,8 @@ class App { @@ -1736,8 +1736,8 @@ class App {
1736 if(require_subcommand_max_ != 0 && parsed_subcommands_.size() >= require_subcommand_max_) { 1736 if(require_subcommand_max_ != 0 && parsed_subcommands_.size() >= require_subcommand_max_) {
1737 return parent_ != nullptr && parent_->_valid_subcommand(current); 1737 return parent_ != nullptr && parent_->_valid_subcommand(current);
1738 } 1738 }
1739 - auto com = _find_subcommand(current, true);  
1740 - if((com != nullptr) && !*com) { 1739 + auto com = _find_subcommand(current, true, true);
  1740 + if(com != nullptr) {
1741 return true; 1741 return true;
1742 } 1742 }
1743 // Check parent if exists, else return false 1743 // Check parent if exists, else return false
@@ -2135,33 +2135,49 @@ class App { @@ -2135,33 +2135,49 @@ class App {
2135 if(parent_ != nullptr && fallthrough_) 2135 if(parent_ != nullptr && fallthrough_)
2136 return parent_->_parse_positional(args); 2136 return parent_->_parse_positional(args);
2137 else { 2137 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_;
2140 } 2143 }
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);
2143 2156
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 + }
2148 } 2162 }
2149 } 2163 }
2150 } 2164 }
2151 } 2165 }
2152 2166
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 {
2155 for(const App_p &com : subcommands_) { 2170 for(const App_p &com : subcommands_) {
2156 if((com->disabled_) && (ignore_disabled)) 2171 if((com->disabled_) && (ignore_disabled))
2157 continue; 2172 continue;
2158 if(com->get_name().empty()) { 2173 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);
2160 if(subc != nullptr) { 2175 if(subc != nullptr) {
2161 return subc; 2176 return subc;
2162 } 2177 }
2163 } else if(com->check_name(subc_name)) { 2178 } else if(com->check_name(subc_name)) {
2164 - return com.get(); 2179 + if((!*com) || (!ignore_used))
  2180 + return com.get();
2165 } 2181 }
2166 } 2182 }
2167 return nullptr; 2183 return nullptr;
@@ -2173,7 +2189,7 @@ class App { @@ -2173,7 +2189,7 @@ class App {
2173 void _parse_subcommand(std::vector<std::string> &args) { 2189 void _parse_subcommand(std::vector<std::string> &args) {
2174 if(_count_remaining_positionals(/* required */ true) > 0) 2190 if(_count_remaining_positionals(/* required */ true) > 0)
2175 return _parse_positional(args); 2191 return _parse_positional(args);
2176 - auto com = _find_subcommand(args.back(), true); 2192 + auto com = _find_subcommand(args.back(), true, true);
2177 if(com != nullptr) { 2193 if(com != nullptr) {
2178 args.pop_back(); 2194 args.pop_back();
2179 if(std::find(std::begin(parsed_subcommands_), std::end(parsed_subcommands_), com) == 2195 if(std::find(std::begin(parsed_subcommands_), std::end(parsed_subcommands_), com) ==
tests/OptionGroupTest.cpp
@@ -501,3 +501,39 @@ TEST_F(ManyGroups, DisableFirst) { @@ -501,3 +501,39 @@ TEST_F(ManyGroups, DisableFirst) {
501 args = {"--name1", "test", "--name2", "test3", "--name3=test3"}; 501 args = {"--name1", "test", "--name2", "test3", "--name3=test3"};
502 EXPECT_NO_THROW(run()); 502 EXPECT_NO_THROW(run());
503 } 503 }
  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
@@ -163,6 +163,21 @@ TEST_F(TApp, FooFooProblem) { @@ -163,6 +163,21 @@ TEST_F(TApp, FooFooProblem) {
163 EXPECT_EQ(other_str, ""); 163 EXPECT_EQ(other_str, "");
164 } 164 }
165 165
  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 +
166 TEST_F(TApp, Callbacks) { 181 TEST_F(TApp, Callbacks) {
167 auto sub1 = app.add_subcommand("sub1"); 182 auto sub1 = app.add_subcommand("sub1");
168 sub1->callback([]() { throw CLI::Success(); }); 183 sub1->callback([]() { throw CLI::Success(); });