Commit 49e93cac3c04a0a0d46fe34dc9acc1bae1b091c2

Authored by Philip Top
Committed by Henry Schreiner
1 parent 059f6ef2

add docs for remove_subcommand and add_subcommand in option_group

add some test of the remove_excludes functions

add test for Issue #256

add remove_subcommand fail test

add remove_subcommand function and add_subcommand to option_group and some tests associated with them.
README.md
... ... @@ -479,6 +479,7 @@ There are several options that are supported on the main app and subcommands and
479 479 - `.require_subcommand(min, max)`: Explicitly set min and max allowed subcommands. Setting `max` to 0 is unlimited.
480 480 - `.add_subcommand(name="", description="")`: Add a subcommand, returns a pointer to the internally stored subcommand.
481 481 - `.add_subcommand(shared_ptr<App>)`: ๐Ÿšง Add a subcommand by shared_ptr, returns a pointer to the internally stored subcommand.
  482 +- `.remove_subcommand(App)`:๐Ÿšง Remove a subcommand from the app or subcommand.
482 483 - `.got_subcommand(App_or_name)`: Check to see if a subcommand was received on the command line.
483 484 - `.get_subcommands(filter)`: The list of subcommands that match a particular filter function.
484 485 - `.add_option_group(name="", description="")`: ๐Ÿšง Add an [option group](#option-groups) to an App, an option group is specialized subcommand intended for containing groups of options or other groups for controlling how options interact.
... ... @@ -562,18 +563,23 @@ The subcommand method
562 563 Will create an option Group, and return a pointer to it. An option group allows creation of a collection of options, similar to the groups function on options, but with additional controls and requirements. They allow specific sets of options to be composed and controlled as a collective. For an example see [range test](./tests/ranges.cpp). Option groups are a specialization of an App so all [functions](#subcommand-options) that work with an App also work on option groups. Options can be created as part of an option group using the add functions just like a subcommand, or previously created options can be added through
563 564  
564 565 ```cpp
565   -ogroup->add_option(option_pointer)
  566 +ogroup->add_option(option_pointer);
566 567 ```
567 568  
568 569 ```cpp
569   -ogroup->add_options(option_pointer)
  570 +ogroup->add_options(option_pointer);
570 571 ```
571 572  
572 573 ```cpp
573   -ogroup->add_options(option1,option2,option3,...)
  574 +ogroup->add_options(option1,option2,option3,...);
574 575 ```
575 576  
576   -The option pointers used in this function must be options defined in the parent application of the option group otherwise an error will be generated.
  577 +The option pointers used in this function must be options defined in the parent application of the option group otherwise an error will be generated. Subcommands can also be added via
  578 +```cpp
  579 +ogroup->add_subcommand(subcom_pointer);
  580 +```
  581 +This results in the subcommand being moved from its parent into the option group.
  582 +
577 583 Options in an option group are searched for a command line match after any options in the main app, so any positionals in the main app would be matched first. So care must be taken to make sure of the order when using positional arguments and option groups.
578 584 Option groups work well with `excludes` and `require_options` methods, as an Application will treat an option group as a single option for the purpose of counting and requirements. Option groups allow specifying requirements such as requiring 1 of 3 options in one group and 1 of 3 options in a different group. Option groups can contain other groups as well. Disabling an option group will turn off all options within the group.
579 585  
... ...
cmake/AddGoogletest.cmake
... ... @@ -6,7 +6,10 @@
6 6 #
7 7 set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
8 8 set(BUILD_SHARED_LIBS OFF)
9   -
  9 +# older version of google tests doesn't support MSYS so needs this flag to compile
  10 +if (MSYS)
  11 + set(gtest_disable_pthreads ON CACHE BOOL "" FORCE)
  12 +endif()
10 13 set(CMAKE_SUPPRESS_DEVELOPER_WARNINGS 1 CACHE BOOL "")
11 14 add_subdirectory("${CLI11_SOURCE_DIR}/extern/googletest" "${CLI11_BINARY_DIR}/extern/googletest" EXCLUDE_FROM_ALL)
12 15  
... ...
include/CLI/App.hpp
... ... @@ -1074,6 +1074,21 @@ class App {
1074 1074 return subcommands_.back().get();
1075 1075 }
1076 1076  
  1077 + /// Removes a subcommand from the App. Takes a subcommand pointer. Returns true if found and removed.
  1078 + bool remove_subcommand(App *subcom) {
  1079 + // Make sure no links exist
  1080 + for(App_p &sub : subcommands_) {
  1081 + sub->remove_excludes(subcom);
  1082 + }
  1083 +
  1084 + auto iterator = std::find_if(
  1085 + std::begin(subcommands_), std::end(subcommands_), [subcom](const App_p &v) { return v.get() == subcom; });
  1086 + if(iterator != std::end(subcommands_)) {
  1087 + subcommands_.erase(iterator);
  1088 + return true;
  1089 + }
  1090 + return false;
  1091 + }
1077 1092 /// Check to see if a subcommand is part of this command (doesn't have to be in command line)
1078 1093 /// returns the first subcommand if passed a nullptr
1079 1094 App *get_subcommand(App *subcom) const {
... ... @@ -1124,6 +1139,16 @@ class App {
1124 1139 throw OptionNotFound(std::to_string(index));
1125 1140 }
1126 1141  
  1142 + /// Check to see if an option group is part of this App
  1143 + App *get_option_group(std::string group_name) const {
  1144 + for(const App_p &app : subcommands_) {
  1145 + if(app->name_.empty() && app->group_ == group_name) {
  1146 + return app.get();
  1147 + }
  1148 + }
  1149 + throw OptionNotFound(group_name);
  1150 + }
  1151 +
1127 1152 /// No argument version of count counts the number of times this subcommand was
1128 1153 /// passed in. The main app will return 1. Unnamed subcommands will also return 1 unless
1129 1154 /// otherwise modified in a callback
... ... @@ -1437,7 +1462,9 @@ class App {
1437 1462 bool remove_excludes(App *app) {
1438 1463 auto iterator = std::find(std::begin(exclude_subcommands_), std::end(exclude_subcommands_), app);
1439 1464 if(iterator != std::end(exclude_subcommands_)) {
  1465 + auto other_app = *iterator;
1440 1466 exclude_subcommands_.erase(iterator);
  1467 + other_app->remove_excludes(this);
1441 1468 return true;
1442 1469 } else {
1443 1470 return false;
... ... @@ -2584,7 +2611,7 @@ class Option_group : public App {
2584 2611 // option groups should have automatic fallthrough
2585 2612 }
2586 2613 using App::add_option;
2587   - /// add an existing option to the Option_group
  2614 + /// Add an existing option to the Option_group
2588 2615 Option *add_option(Option *opt) {
2589 2616 if(get_parent() == nullptr) {
2590 2617 throw OptionNotFound("Unable to locate the specified option");
... ... @@ -2592,13 +2619,21 @@ class Option_group : public App {
2592 2619 get_parent()->_move_option(opt, this);
2593 2620 return opt;
2594 2621 }
2595   - /// add an existing option to the Option_group
  2622 + /// Add an existing option to the Option_group
2596 2623 void add_options(Option *opt) { add_option(opt); }
2597   - /// add a bunch of options to the group
  2624 + /// Add a bunch of options to the group
2598 2625 template <typename... Args> void add_options(Option *opt, Args... args) {
2599 2626 add_option(opt);
2600 2627 add_options(args...);
2601 2628 }
  2629 + using App::add_subcommand;
  2630 + /// Add an existing subcommand to be a member of an option_group
  2631 + App *add_subcommand(App *subcom) {
  2632 + App_p subc = subcom->get_parent()->get_subcommand_ptr(subcom);
  2633 + subc->get_parent()->remove_subcommand(subcom);
  2634 + add_subcommand(std::move(subc));
  2635 + return subcom;
  2636 + }
2602 2637 };
2603 2638 /// Helper function to enable one option group/subcommand when another is used
2604 2639 inline void TriggerOn(App *trigger_app, App *app_to_enable) {
... ...
tests/OptionGroupTest.cpp
... ... @@ -431,6 +431,11 @@ TEST_F(ManyGroups, ExcludesGroup) {
431 431 args = {"--name1", "test", "--name2", "test2"};
432 432  
433 433 EXPECT_THROW(run(), CLI::ExcludesError);
  434 +
  435 + EXPECT_TRUE(g1->remove_excludes(g2));
  436 + EXPECT_NO_THROW(run());
  437 + EXPECT_FALSE(g1->remove_excludes(g1));
  438 + EXPECT_FALSE(g1->remove_excludes(g2));
434 439 }
435 440  
436 441 TEST_F(ManyGroups, SingleGroupError) {
... ... @@ -605,6 +610,17 @@ TEST_F(ManyGroups, Inheritance) {
605 610 EXPECT_EQ(t2->count(), 2u);
606 611 }
607 612  
  613 +TEST_F(ManyGroups, Moving) {
  614 + remove_required();
  615 + auto mg = app.add_option_group("maing");
  616 + mg->add_subcommand(g1);
  617 + mg->add_subcommand(g2);
  618 +
  619 + EXPECT_EQ(g1->get_parent(), mg);
  620 + EXPECT_EQ(g2->get_parent(), mg);
  621 + EXPECT_EQ(g3->get_parent(), main);
  622 +}
  623 +
608 624 struct ManyGroupsPreTrigger : public ManyGroups {
609 625 size_t triggerMain, trigger1{87u}, trigger2{34u}, trigger3{27u};
610 626 ManyGroupsPreTrigger() {
... ...
tests/SubcommandTest.cpp
... ... @@ -641,6 +641,27 @@ TEST_F(TApp, InheritHelpAllFlag) {
641 641 EXPECT_EQ(help_opt_list.size(), 1u);
642 642 }
643 643  
  644 +TEST_F(TApp, RequiredPosInSubcommand) {
  645 + app.require_subcommand();
  646 + std::string bar;
  647 +
  648 + CLI::App *fooApp = app.add_subcommand("foo", "Foo a bar");
  649 + fooApp->add_option("bar", bar, "A bar to foo")->required();
  650 +
  651 + CLI::App *bazApp = app.add_subcommand("baz", "Baz a bar");
  652 + bazApp->add_option("bar", bar, "A bar a baz")->required();
  653 +
  654 + args = {"foo", "abc"};
  655 + run();
  656 + EXPECT_EQ(bar, "abc");
  657 + args = {"baz", "cba"};
  658 + run();
  659 + EXPECT_EQ(bar, "cba");
  660 +
  661 + args = {};
  662 + EXPECT_THROW(run(), CLI::RequiredError);
  663 +}
  664 +
644 665 struct SubcommandProgram : public TApp {
645 666  
646 667 CLI::App *start;
... ... @@ -962,6 +983,22 @@ TEST_F(ManySubcommands, Required4Failure) {
962 983 EXPECT_THROW(run(), CLI::RequiredError);
963 984 }
964 985  
  986 +TEST_F(ManySubcommands, RemoveSub) {
  987 + run();
  988 + EXPECT_EQ(app.remaining_size(true), 0u);
  989 + app.remove_subcommand(sub1);
  990 + app.allow_extras();
  991 + run();
  992 + EXPECT_EQ(app.remaining_size(true), 1u);
  993 +}
  994 +
  995 +TEST_F(ManySubcommands, RemoveSubFail) {
  996 + auto sub_sub = sub1->add_subcommand("subsub");
  997 + EXPECT_FALSE(app.remove_subcommand(sub_sub));
  998 + EXPECT_TRUE(sub1->remove_subcommand(sub_sub));
  999 + EXPECT_FALSE(app.remove_subcommand(nullptr));
  1000 +}
  1001 +
965 1002 TEST_F(ManySubcommands, manyIndexQuery) {
966 1003 auto s1 = app.get_subcommand(0);
967 1004 auto s2 = app.get_subcommand(1);
... ... @@ -1100,13 +1137,15 @@ TEST_F(ManySubcommands, SubcommandOptionExclusion) {
1100 1137 args = {"sub3", "sub4", "--exclude"};
1101 1138 EXPECT_NO_THROW(run());
1102 1139  
1103   - // the option comes later so doesn't exclude
1104 1140 args = {"sub1", "sub3", "--exclude"};
1105 1141 EXPECT_THROW(run(), CLI::ExcludesError);
  1142 + EXPECT_TRUE(sub1->remove_excludes(excluder_flag));
  1143 + EXPECT_NO_THROW(run());
  1144 + EXPECT_FALSE(sub1->remove_excludes(excluder_flag));
1106 1145  
1107 1146 args = {"--exclude", "sub2", "sub4"};
1108 1147 EXPECT_THROW(run(), CLI::ExcludesError);
1109   -
  1148 + EXPECT_EQ(sub1->excludes(excluder_flag), sub1);
1110 1149 args = {"sub1", "--exclude", "sub2", "sub4"};
1111 1150 try {
1112 1151 run();
... ...