Commit f0461525bb826071221a2292a3657382a7b77fa2

Authored by Philip Top
Committed by GitHub
1 parent 31be35b2

feat: add a silent option to subcommands (#529)

Co-authored-by: Henry Schreiner <HenrySchreinerIII@gmail.com>
README.md
@@ -560,6 +560,7 @@ There are several options that are supported on the main app and subcommands and @@ -560,6 +560,7 @@ There are several options that are supported on the main app and subcommands and
560 - `.disable()`: Specify that the subcommand is disabled, if given with a bool value it will enable or disable the subcommand or option group. 560 - `.disable()`: Specify that the subcommand is disabled, if given with a bool value it will enable or disable the subcommand or option group.
561 - `.disabled_by_default()`: Specify that at the start of parsing the subcommand/option_group should be disabled. This is useful for allowing some Subcommands to trigger others. 561 - `.disabled_by_default()`: Specify that at the start of parsing the subcommand/option_group should be disabled. This is useful for allowing some Subcommands to trigger others.
562 - `.enabled_by_default()`: Specify that at the start of each parse the subcommand/option_group should be enabled. This is useful for allowing some Subcommands to disable others. 562 - `.enabled_by_default()`: Specify that at the start of each parse the subcommand/option_group should be enabled. This is useful for allowing some Subcommands to disable others.
  563 +- `.silent()`: ๐Ÿšง Specify that the subcommand is silent meaning that if used it won't show up in the subcommand list. This allows the use of subcommands as modifiers
563 - `.validate_positionals()`: Specify that positionals should pass validation before matching. Validation is specified through `transform`, `check`, and `each` for an option. If an argument fails validation it is not an error and matching proceeds to the next available positional or extra arguments. 564 - `.validate_positionals()`: Specify that positionals should pass validation before matching. Validation is specified through `transform`, `check`, and `each` for an option. If an argument fails validation it is not an error and matching proceeds to the next available positional or extra arguments.
564 - `.excludes(option_or_subcommand)`: If given an option pointer or pointer to another subcommand, these subcommands cannot be given together. In the case of options, if the option is passed the subcommand cannot be used and will generate an error. 565 - `.excludes(option_or_subcommand)`: If given an option pointer or pointer to another subcommand, these subcommands cannot be given together. In the case of options, if the option is passed the subcommand cannot be used and will generate an error.
565 - `.needs(option_or_subcommand)`: ๐Ÿ†• If given an option pointer or pointer to another subcommand, the subcommands will require the given option to have been given before this subcommand is validated which occurs prior to execution of any callback or after parsing is completed. 566 - `.needs(option_or_subcommand)`: ๐Ÿ†• If given an option pointer or pointer to another subcommand, the subcommands will require the given option to have been given before this subcommand is validated which occurs prior to execution of any callback or after parsing is completed.
book/chapters/subcommands.md
@@ -112,3 +112,18 @@ Here, `--shared_flag` was set on the main app, and on the command line it &quot;falls @@ -112,3 +112,18 @@ Here, `--shared_flag` was set on the main app, and on the command line it &quot;falls
112 112
113 This is a special mode that allows "prefix" commands, where the parsing completely stops when it gets to an unknown option. Further unknown options are ignored, even if they could match. Git is the traditional example for prefix commands; if you run git with an unknown subcommand, like "`git thing`", it then calls another command called "`git-thing`" with the remaining options intact. 113 This is a special mode that allows "prefix" commands, where the parsing completely stops when it gets to an unknown option. Further unknown options are ignored, even if they could match. Git is the traditional example for prefix commands; if you run git with an unknown subcommand, like "`git thing`", it then calls another command called "`git-thing`" with the remaining options intact.
114 114
  115 +### Silent subcommands
  116 +
  117 +Subcommands can be modified by using the `silent` option. This will prevent the subcommand from showing up in the get_subcommands list. This can be used to make subcommands into modifiers. For example, a help subcommand might look like
  118 +
  119 +```c++
  120 + auto sub1 = app.add_subcommand("help")->silent();
  121 + sub1->parse_complete_callback([]() { throw CLI::CallForHelp(); });
  122 +```
  123 +
  124 +This would allow calling help such as:
  125 +
  126 +```bash
  127 +./app help
  128 +./app help sub1
  129 +```
include/CLI/App.hpp
@@ -218,11 +218,12 @@ class App { @@ -218,11 +218,12 @@ class App {
218 /// If set to true positional options are validated before assigning INHERITABLE 218 /// If set to true positional options are validated before assigning INHERITABLE
219 bool validate_positionals_{false}; 219 bool validate_positionals_{false};
220 220
221 - /// A pointer to the parent if this is a subcommand  
222 - App *parent_{nullptr}; 221 + /// indicator that the subcommand is silent and won't show up in subcommands list
  222 + /// This is potentially useful as a modifier subcommand
  223 + bool silent_{false};
223 224
224 /// Counts the number of times this command/subcommand was parsed 225 /// Counts the number of times this command/subcommand was parsed
225 - std::size_t parsed_{0}; 226 + std::uint32_t parsed_{0U};
226 227
227 /// Minimum required subcommands (not inheritable!) 228 /// Minimum required subcommands (not inheritable!)
228 std::size_t require_subcommand_min_{0}; 229 std::size_t require_subcommand_min_{0};
@@ -236,6 +237,9 @@ class App { @@ -236,6 +237,9 @@ class App {
236 /// Max number of options allowed. 0 is unlimited (not inheritable) 237 /// Max number of options allowed. 0 is unlimited (not inheritable)
237 std::size_t require_option_max_{0}; 238 std::size_t require_option_max_{0};
238 239
  240 + /// A pointer to the parent if this is a subcommand
  241 + App *parent_{nullptr};
  242 +
239 /// The group membership INHERITABLE 243 /// The group membership INHERITABLE
240 std::string group_{"Subcommands"}; 244 std::string group_{"Subcommands"};
241 245
@@ -396,6 +400,12 @@ class App { @@ -396,6 +400,12 @@ class App {
396 return this; 400 return this;
397 } 401 }
398 402
  403 + /// silence the subcommand from showing up in the processed list
  404 + App *silent(bool silence = true) {
  405 + silent_ = silence;
  406 + return this;
  407 + }
  408 +
399 /// Set the subcommand to be disabled by default, so on clear(), at the start of each parse it is disabled 409 /// Set the subcommand to be disabled by default, so on clear(), at the start of each parse it is disabled
400 App *disabled_by_default(bool disable = true) { 410 App *disabled_by_default(bool disable = true) {
401 if(disable) { 411 if(disable) {
@@ -1767,6 +1777,9 @@ class App { @@ -1767,6 +1777,9 @@ class App {
1767 /// Get the status of disabled 1777 /// Get the status of disabled
1768 bool get_disabled() const { return disabled_; } 1778 bool get_disabled() const { return disabled_; }
1769 1779
  1780 + /// Get the status of silence
  1781 + bool get_silent() const { return silent_; }
  1782 +
1770 /// Get the status of disabled 1783 /// Get the status of disabled
1771 bool get_immediate_callback() const { return immediate_callback_; } 1784 bool get_immediate_callback() const { return immediate_callback_; }
1772 1785
@@ -2673,12 +2686,16 @@ class App { @@ -2673,12 +2686,16 @@ class App {
2673 auto com = _find_subcommand(args.back(), true, true); 2686 auto com = _find_subcommand(args.back(), true, true);
2674 if(com != nullptr) { 2687 if(com != nullptr) {
2675 args.pop_back(); 2688 args.pop_back();
2676 - parsed_subcommands_.push_back(com); 2689 + if(!com->silent_) {
  2690 + parsed_subcommands_.push_back(com);
  2691 + }
2677 com->_parse(args); 2692 com->_parse(args);
2678 auto parent_app = com->parent_; 2693 auto parent_app = com->parent_;
2679 while(parent_app != this) { 2694 while(parent_app != this) {
2680 parent_app->_trigger_pre_parse(args.size()); 2695 parent_app->_trigger_pre_parse(args.size());
2681 - parent_app->parsed_subcommands_.push_back(com); 2696 + if(!com->silent_) {
  2697 + parent_app->parsed_subcommands_.push_back(com);
  2698 + }
2682 parent_app = parent_app->parent_; 2699 parent_app = parent_app->parent_;
2683 } 2700 }
2684 return true; 2701 return true;
tests/SubcommandTest.cpp
@@ -1466,6 +1466,21 @@ TEST_F(ManySubcommands, SubcommandTriggeredOn) { @@ -1466,6 +1466,21 @@ TEST_F(ManySubcommands, SubcommandTriggeredOn) {
1466 EXPECT_THROW(run(), CLI::ExtrasError); 1466 EXPECT_THROW(run(), CLI::ExtrasError);
1467 } 1467 }
1468 1468
  1469 +TEST_F(ManySubcommands, SubcommandSilence) {
  1470 +
  1471 + sub1->silent();
  1472 + args = {"sub1", "sub2"};
  1473 + EXPECT_NO_THROW(run());
  1474 +
  1475 + auto subs = app.get_subcommands();
  1476 + EXPECT_EQ(subs.size(), 1U);
  1477 + sub1->silent(false);
  1478 + EXPECT_FALSE(sub1->get_silent());
  1479 + run();
  1480 + subs = app.get_subcommands();
  1481 + EXPECT_EQ(subs.size(), 2U);
  1482 +}
  1483 +
1469 TEST_F(TApp, UnnamedSub) { 1484 TEST_F(TApp, UnnamedSub) {
1470 double val{0.0}; 1485 double val{0.0};
1471 auto sub = app.add_subcommand("", "empty name"); 1486 auto sub = app.add_subcommand("", "empty name");
@@ -1622,6 +1637,23 @@ TEST_F(TApp, OptionGroupAlias) { @@ -1622,6 +1637,23 @@ TEST_F(TApp, OptionGroupAlias) {
1622 EXPECT_EQ(val, -3); 1637 EXPECT_EQ(val, -3);
1623 } 1638 }
1624 1639
  1640 +TEST_F(TApp, subcommand_help) {
  1641 + auto sub1 = app.add_subcommand("help")->silent();
  1642 + bool flag{false};
  1643 + app.add_flag("--one", flag, "FLAGGER");
  1644 + sub1->parse_complete_callback([]() { throw CLI::CallForHelp(); });
  1645 + bool called{false};
  1646 + args = {"help"};
  1647 + try {
  1648 + run();
  1649 + } catch(const CLI::CallForHelp &) {
  1650 + called = true;
  1651 + }
  1652 + auto helpstr = app.help();
  1653 + EXPECT_THAT(helpstr, HasSubstr("FLAGGER"));
  1654 + EXPECT_TRUE(called);
  1655 +}
  1656 +
1625 TEST_F(TApp, AliasErrors) { 1657 TEST_F(TApp, AliasErrors) {
1626 auto sub1 = app.add_subcommand("sub1"); 1658 auto sub1 = app.add_subcommand("sub1");
1627 auto sub2 = app.add_subcommand("sub2"); 1659 auto sub2 = app.add_subcommand("sub2");