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 283 - `->envname(name)`: Gets the value from the environment if present and not passed on the command line.
284 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 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 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 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 289 - `->description(str)`: πŸ†• Set/change the description.
... ... @@ -379,7 +379,7 @@ of `IsMember`:
379 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 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 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 383 of `Transformer`:
384 384  
385 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 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 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 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 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 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 1045  
1046 1046 /// Check to see if a subcommand is part of this command (text version)
1047 1047 App *get_subcommand(std::string subcom) const {
1048   - auto subc = _find_subcommand(subcom, false);
  1048 + auto subc = _find_subcommand(subcom, false, false);
1049 1049 if(subc == nullptr)
1050 1050 throw OptionNotFound(subcom);
1051 1051 return subc;
... ... @@ -1572,7 +1572,7 @@ class App {
1572 1572 /// Get the status of required
1573 1573 bool get_required() const { return required_; }
1574 1574  
1575   - /// Get the status of required
  1575 + /// Get the status of disabled
1576 1576 bool get_disabled() const { return disabled_; }
1577 1577  
1578 1578 /// Get the status of allow extras
... ... @@ -1736,8 +1736,8 @@ class App {
1736 1736 if(require_subcommand_max_ != 0 && parsed_subcommands_.size() >= require_subcommand_max_) {
1737 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 1741 return true;
1742 1742 }
1743 1743 // Check parent if exists, else return false
... ... @@ -2135,33 +2135,49 @@ class App {
2135 2135 if(parent_ != nullptr && fallthrough_)
2136 2136 return parent_->_parse_positional(args);
2137 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 2170 for(const App_p &com : subcommands_) {
2156 2171 if((com->disabled_) && (ignore_disabled))
2157 2172 continue;
2158 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 2175 if(subc != nullptr) {
2161 2176 return subc;
2162 2177 }
2163 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 2183 return nullptr;
... ... @@ -2173,7 +2189,7 @@ class App {
2173 2189 void _parse_subcommand(std::vector<std::string> &args) {
2174 2190 if(_count_remaining_positionals(/* required */ true) > 0)
2175 2191 return _parse_positional(args);
2176   - auto com = _find_subcommand(args.back(), true);
  2192 + auto com = _find_subcommand(args.back(), true, true);
2177 2193 if(com != nullptr) {
2178 2194 args.pop_back();
2179 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 501 args = {"--name1", "test", "--name2", "test3", "--name3=test3"};
502 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 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 181 TEST_F(TApp, Callbacks) {
167 182 auto sub1 = app.add_subcommand("sub1");
168 183 sub1->callback([]() { throw CLI::Success(); });
... ...