Commit c3d8d4a2d0253d12ab27dcf0419184a85c3c7185

Authored by Henry Schreiner
Committed by GitHub
1 parent 2ba85eb7

Adding new parse layout (#178)

* Adding new parse layout

* Dropping shortcurcuit from help, since it has special override

* Refactor help call

* Dropping shortcurcuit since it is not needed now that help has custom behavoir

* Dropping MaxSubcommand error (cannot occur)
CHANGELOG.md
  1 +## Version 1.7.0: Parse breakup
  2 +
  3 +The parsing procedure now maps much more sensibly to complex, nested subcommand structures. Each phase of the parsing happens on all subcommands before moving on with the next phase of the parse. This allows several features, like required environment variables, to work properly even through subcommand boundaries.
  4 +Passing the same subcommand multiple times is better supported.
  5 +
  6 +* Subcommands now track how many times they were parsed in a parsing process. `count()` with no arguments will return the number of times a subcommand was encountered.
  7 +* Parsing is now done in phases: `shortcurcuits`, `ini`, `env`, `callbacks`, and `requirements`; all subcommands complete a phase before moving on.
  8 +* Calling parse multiple times is now officially supported without `clear` (automatic).
  9 +* Dropped the mostly undocumented `short_curcuit` property, as help flag parsing is a bit more complex, and the default callback behavior of options now works properly.
  10 +
  11 +
1 ## Version 1.6.2: Help-all 12 ## Version 1.6.2: Help-all
2 13
3 This version fixes some formatting bugs with help-all. It also adds fixes for several warnings, including an experimental optional error on Clang 7. Several smaller fixes. 14 This version fixes some formatting bugs with help-all. It also adds fixes for several warnings, including an experimental optional error on Clang 7. Several smaller fixes.
include/CLI/App.hpp
@@ -145,8 +145,8 @@ class App { @@ -145,8 +145,8 @@ class App {
145 /// A pointer to the parent if this is a subcommand 145 /// A pointer to the parent if this is a subcommand
146 App *parent_{nullptr}; 146 App *parent_{nullptr};
147 147
148 - /// True if this command/subcommand was parsed  
149 - bool parsed_{false}; 148 + /// Counts the number of times this command/subcommand was parsed
  149 + size_t parsed_ = 0;
150 150
151 /// Minimum required subcommands (not inheritable!) 151 /// Minimum required subcommands (not inheritable!)
152 size_t require_subcommand_min_ = 0; 152 size_t require_subcommand_min_ = 0;
@@ -284,7 +284,7 @@ class App { @@ -284,7 +284,7 @@ class App {
284 } 284 }
285 285
286 /// Check to see if this subcommand was parsed, true only if received on command line. 286 /// Check to see if this subcommand was parsed, true only if received on command line.
287 - bool parsed() const { return parsed_; } 287 + bool parsed() const { return parsed_ > 0; }
288 288
289 /// Get the OptionDefault object, to set option defaults 289 /// Get the OptionDefault object, to set option defaults
290 OptionDefaults *option_defaults() { return &option_defaults_; } 290 OptionDefaults *option_defaults() { return &option_defaults_; }
@@ -408,8 +408,7 @@ class App { @@ -408,8 +408,7 @@ class App {
408 408
409 // Empty name will simply remove the help flag 409 // Empty name will simply remove the help flag
410 if(!name.empty()) { 410 if(!name.empty()) {
411 - help_ptr_ = add_flag_function(name, [](size_t) -> void { throw CallForHelp(); }, description);  
412 - help_ptr_->short_circuit(true); 411 + help_ptr_ = add_flag(name, description);
413 help_ptr_->configurable(false); 412 help_ptr_->configurable(false);
414 } 413 }
415 414
@@ -425,8 +424,7 @@ class App { @@ -425,8 +424,7 @@ class App {
425 424
426 // Empty name will simply remove the help all flag 425 // Empty name will simply remove the help all flag
427 if(!name.empty()) { 426 if(!name.empty()) {
428 - help_all_ptr_ = add_flag_function(name, [](size_t) -> void { throw CallForAllHelp(); }, description);  
429 - help_all_ptr_->short_circuit(true); 427 + help_all_ptr_ = add_flag(name, description);
430 help_all_ptr_->configurable(false); 428 help_all_ptr_->configurable(false);
431 } 429 }
432 430
@@ -826,6 +824,10 @@ class App { @@ -826,6 +824,10 @@ class App {
826 throw OptionNotFound(subcom); 824 throw OptionNotFound(subcom);
827 } 825 }
828 826
  827 + /// No argument version of count counts the number of times this subcommand was
  828 + /// passed in. The main app will return 1.
  829 + size_t count() const { return parsed_; }
  830 +
829 /// Changes the group membership 831 /// Changes the group membership
830 App *group(std::string name) { 832 App *group(std::string name) {
831 group_ = name; 833 group_ = name;
@@ -870,7 +872,7 @@ class App { @@ -870,7 +872,7 @@ class App {
870 872
871 /// Check to see if this subcommand was parsed, true only if received on command line. 873 /// Check to see if this subcommand was parsed, true only if received on command line.
872 /// This allows the subcommand to be directly checked. 874 /// This allows the subcommand to be directly checked.
873 - operator bool() const { return parsed_; } 875 + operator bool() const { return parsed_ > 0; }
874 876
875 ///@} 877 ///@}
876 /// @name Extras for subclassing 878 /// @name Extras for subclassing
@@ -917,15 +919,16 @@ class App { @@ -917,15 +919,16 @@ class App {
917 /// Changes the vector to the remaining options. 919 /// Changes the vector to the remaining options.
918 void parse(std::vector<std::string> &args) { 920 void parse(std::vector<std::string> &args) {
919 // Clear if parsed 921 // Clear if parsed
920 - if(parsed_) 922 + if(parsed_ > 0)
921 clear(); 923 clear();
922 924
923 - // Redundant (set by _parse on commands/subcommands) 925 + // _parse is incremented in commands/subcommands,
924 // but placed here to make sure this is cleared when 926 // but placed here to make sure this is cleared when
925 // running parse after an error is thrown, even by _validate. 927 // running parse after an error is thrown, even by _validate.
926 - parsed_ = true;  
927 - 928 + parsed_ = 1;
928 _validate(); 929 _validate();
  930 + parsed_ = 0;
  931 +
929 _parse(args); 932 _parse(args);
930 run_callback(); 933 run_callback();
931 } 934 }
@@ -1016,11 +1019,11 @@ class App { @@ -1016,11 +1019,11 @@ class App {
1016 /// Check to see if given subcommand was selected 1019 /// Check to see if given subcommand was selected
1017 bool got_subcommand(App *subcom) const { 1020 bool got_subcommand(App *subcom) const {
1018 // get subcom needed to verify that this was a real subcommand 1021 // get subcom needed to verify that this was a real subcommand
1019 - return get_subcommand(subcom)->parsed_; 1022 + return get_subcommand(subcom)->parsed_ > 0;
1020 } 1023 }
1021 1024
1022 /// Check with name instead of pointer to see if subcommand was selected 1025 /// Check with name instead of pointer to see if subcommand was selected
1023 - bool got_subcommand(std::string name) const { return get_subcommand(name)->parsed_; } 1026 + bool got_subcommand(std::string name) const { return get_subcommand(name)->parsed_ > 0; }
1024 1027
1025 ///@} 1028 ///@}
1026 /// @name Help 1029 /// @name Help
@@ -1282,19 +1285,10 @@ class App { @@ -1282,19 +1285,10 @@ class App {
1282 return detail::Classifer::NONE; 1285 return detail::Classifer::NONE;
1283 } 1286 }
1284 1287
1285 - /// Internal parse function  
1286 - void _parse(std::vector<std::string> &args) {  
1287 - parsed_ = true;  
1288 - bool positional_only = false;  
1289 -  
1290 - while(!args.empty()) {  
1291 - _parse_single(args, positional_only);  
1292 - }  
1293 -  
1294 - for(const Option_p &opt : options_)  
1295 - if(opt->get_short_circuit() && opt->count() > 0)  
1296 - opt->run_callback(); 1288 + // The parse function is now broken into several parts, and part of process
1297 1289
  1290 + /// Read and process an ini file (main app only)
  1291 + void _process_ini() {
1298 // Process an INI file 1292 // Process an INI file
1299 if(config_ptr_ != nullptr) { 1293 if(config_ptr_ != nullptr) {
1300 if(*config_ptr_) { 1294 if(*config_ptr_) {
@@ -1311,8 +1305,10 @@ class App { @@ -1311,8 +1305,10 @@ class App {
1311 } 1305 }
1312 } 1306 }
1313 } 1307 }
  1308 + }
1314 1309
1315 - // Get envname options if not yet passed 1310 + /// Get envname options if not yet passed. Runs on *all* subcommands.
  1311 + void _process_env() {
1316 for(const Option_p &opt : options_) { 1312 for(const Option_p &opt : options_) {
1317 if(opt->count() == 0 && !opt->envname_.empty()) { 1313 if(opt->count() == 0 && !opt->envname_.empty()) {
1318 char *buffer = nullptr; 1314 char *buffer = nullptr;
@@ -1338,18 +1334,52 @@ class App { @@ -1338,18 +1334,52 @@ class App {
1338 } 1334 }
1339 } 1335 }
1340 1336
1341 - // Process callbacks 1337 + for(App_p &sub : subcommands_) {
  1338 + sub->_process_env();
  1339 + }
  1340 + }
  1341 +
  1342 + /// Process callbacks. Runs on *all* subcommands.
  1343 + void _process_callbacks() {
1342 for(const Option_p &opt : options_) { 1344 for(const Option_p &opt : options_) {
1343 if(opt->count() > 0 && !opt->get_callback_run()) { 1345 if(opt->count() > 0 && !opt->get_callback_run()) {
1344 opt->run_callback(); 1346 opt->run_callback();
1345 } 1347 }
1346 } 1348 }
1347 1349
1348 - // Verify required options 1350 + for(App_p &sub : subcommands_) {
  1351 + sub->_process_callbacks();
  1352 + }
  1353 + }
  1354 +
  1355 + /// Run help flag processing if any are found.
  1356 + ///
  1357 + /// The flags allow recursive calls to remember if there was a help flag on a parent.
  1358 + void _process_help_flags(bool trigger_help = false, bool trigger_all_help = false) const {
  1359 + const Option *help_ptr = get_help_ptr();
  1360 + const Option *help_all_ptr = get_help_all_ptr();
  1361 +
  1362 + if(help_ptr != nullptr && help_ptr->count() > 0)
  1363 + trigger_help = true;
  1364 + if(help_all_ptr != nullptr && help_all_ptr->count() > 0)
  1365 + trigger_all_help = true;
  1366 +
  1367 + // If there were parsed subcommands, call those. First subcommand wins if there are multiple ones.
  1368 + if(!parsed_subcommands_.empty()) {
  1369 + for(const App *sub : parsed_subcommands_)
  1370 + sub->_process_help_flags(trigger_help, trigger_all_help);
  1371 +
  1372 + // Only the final subcommand should call for help. All help wins over help.
  1373 + } else if(trigger_all_help) {
  1374 + throw CallForAllHelp();
  1375 + } else if(trigger_help) {
  1376 + throw CallForHelp();
  1377 + }
  1378 + }
  1379 +
  1380 + /// Verify required options and cross requirements. Subcommands too (only if selected).
  1381 + void _process_requirements() {
1349 for(const Option_p &opt : options_) { 1382 for(const Option_p &opt : options_) {
1350 - // Exit if a help flag was passed (requirements not required in that case)  
1351 - if(_any_help_flag())  
1352 - break;  
1353 1383
1354 // Required or partially filled 1384 // Required or partially filled
1355 if(opt->get_required() || opt->count() != 0) { 1385 if(opt->get_required() || opt->count() != 0) {
@@ -1375,7 +1405,26 @@ class App { @@ -1375,7 +1405,26 @@ class App {
1375 if(require_subcommand_min_ > selected_subcommands.size()) 1405 if(require_subcommand_min_ > selected_subcommands.size())
1376 throw RequiredError::Subcommand(require_subcommand_min_); 1406 throw RequiredError::Subcommand(require_subcommand_min_);
1377 1407
1378 - // Convert missing (pairs) to extras (string only) 1408 + // Max error cannot occur, the extra subcommand will parse as an ExtrasError or a remaining item.
  1409 +
  1410 + for(App_p &sub : subcommands_) {
  1411 + if(sub->count() > 0)
  1412 + sub->_process_requirements();
  1413 + }
  1414 + }
  1415 +
  1416 + /// Process callbacks and such.
  1417 + void _process() {
  1418 + _process_ini();
  1419 + _process_env();
  1420 + _process_callbacks();
  1421 + _process_help_flags();
  1422 + _process_requirements();
  1423 + }
  1424 +
  1425 + /// Throw an error if anything is left over and should not be.
  1426 + /// Modifies the args to fill in the missing items before throwing.
  1427 + void _process_extras(std::vector<std::string> &args) {
1379 if(!(allow_extras_ || prefix_command_)) { 1428 if(!(allow_extras_ || prefix_command_)) {
1380 size_t num_left_over = remaining_size(); 1429 size_t num_left_over = remaining_size();
1381 if(num_left_over > 0) { 1430 if(num_left_over > 0) {
@@ -1384,24 +1433,30 @@ class App { @@ -1384,24 +1433,30 @@ class App {
1384 } 1433 }
1385 } 1434 }
1386 1435
1387 - if(parent_ == nullptr) {  
1388 - args = remaining(false); 1436 + for(App_p &sub : subcommands_) {
  1437 + if(sub->count() > 0)
  1438 + sub->_process_extras(args);
1389 } 1439 }
1390 } 1440 }
1391 1441
1392 - /// Return True if a help flag detected (checks all parents) (only run if help called before subcommand)  
1393 - bool _any_help_flag() const {  
1394 - bool result = false;  
1395 - const Option *help_ptr = get_help_ptr();  
1396 - const Option *help_all_ptr = get_help_all_ptr();  
1397 - if(help_ptr != nullptr && help_ptr->count() > 0)  
1398 - result = true;  
1399 - if(help_all_ptr != nullptr && help_all_ptr->count() > 0)  
1400 - result = true;  
1401 - if(parent_ != nullptr)  
1402 - return result || parent_->_any_help_flag();  
1403 - else  
1404 - return result; 1442 + /// Internal parse function
  1443 + void _parse(std::vector<std::string> &args) {
  1444 + parsed_++;
  1445 + bool positional_only = false;
  1446 +
  1447 + while(!args.empty()) {
  1448 + _parse_single(args, positional_only);
  1449 + }
  1450 +
  1451 + if(parent_ == nullptr) {
  1452 + _process();
  1453 +
  1454 + // Throw error if any items are left over (depending on settings)
  1455 + _process_extras(args);
  1456 +
  1457 + // Convert missing (pairs) to extras (string only)
  1458 + args = remaining(false);
  1459 + }
1405 } 1460 }
1406 1461
1407 /// Parse one config param, return false if not found in any subcommand, remove if it is 1462 /// Parse one config param, return false if not found in any subcommand, remove if it is
include/CLI/Option.hpp
@@ -215,9 +215,6 @@ class Option : public OptionBase&lt;Option&gt; { @@ -215,9 +215,6 @@ class Option : public OptionBase&lt;Option&gt; {
215 /// Options store a callback to do all the work 215 /// Options store a callback to do all the work
216 callback_t callback_; 216 callback_t callback_;
217 217
218 - /// Options can short-circuit for help options or similar (called before parsing is validated)  
219 - bool short_circuit_{false};  
220 -  
221 ///@} 218 ///@}
222 /// @name Parsing results 219 /// @name Parsing results
223 ///@{ 220 ///@{
@@ -423,14 +420,6 @@ class Option : public OptionBase&lt;Option&gt; { @@ -423,14 +420,6 @@ class Option : public OptionBase&lt;Option&gt; {
423 return this; 420 return this;
424 } 421 }
425 422
426 - /// Options with a short circuit set will run this function before parsing is finished.  
427 - ///  
428 - /// This is set on help functions, for example, to escape the normal validation.  
429 - Option *short_circuit(bool value = true) {  
430 - short_circuit_ = value;  
431 - return this;  
432 - }  
433 -  
434 ///@} 423 ///@}
435 /// @name Accessors 424 /// @name Accessors
436 ///@{ 425 ///@{
@@ -450,9 +439,6 @@ class Option : public OptionBase&lt;Option&gt; { @@ -450,9 +439,6 @@ class Option : public OptionBase&lt;Option&gt; {
450 /// The default value (for help printing) 439 /// The default value (for help printing)
451 std::string get_defaultval() const { return defaultval_; } 440 std::string get_defaultval() const { return defaultval_; }
452 441
453 - /// See if this is supposed to short circuit (skip validation, INI, etc) (Used for help flags)  
454 - bool get_short_circuit() const { return short_circuit_; }  
455 -  
456 /// Get the callback function 442 /// Get the callback function
457 callback_t get_callback() const { return callback_; } 443 callback_t get_callback() const { return callback_; }
458 444
@@ -556,6 +542,8 @@ class Option : public OptionBase&lt;Option&gt; { @@ -556,6 +542,8 @@ class Option : public OptionBase&lt;Option&gt; {
556 /// Process the callback 542 /// Process the callback
557 void run_callback() { 543 void run_callback() {
558 544
  545 + callback_run_ = true;
  546 +
559 // Run the validators (can change the string) 547 // Run the validators (can change the string)
560 if(!validators_.empty()) { 548 if(!validators_.empty()) {
561 for(std::string &result : results_) 549 for(std::string &result : results_)
tests/AppTest.cpp
@@ -1520,3 +1520,25 @@ TEST_F(TApp, EmptyOptionFail) { @@ -1520,3 +1520,25 @@ TEST_F(TApp, EmptyOptionFail) {
1520 args = {"--each", "that"}; 1520 args = {"--each", "that"};
1521 run(); 1521 run();
1522 } 1522 }
  1523 +
  1524 +TEST_F(TApp, BeforeRequirements) {
  1525 + app.add_flag_function("-a", [](size_t) { throw CLI::Success(); });
  1526 + app.add_flag_function("-b", [](size_t) { throw CLI::CallForHelp(); });
  1527 +
  1528 + args = {"extra"};
  1529 + EXPECT_THROW(run(), CLI::ExtrasError);
  1530 +
  1531 + args = {"-a", "extra"};
  1532 + EXPECT_THROW(run(), CLI::Success);
  1533 +
  1534 + args = {"-b", "extra"};
  1535 + EXPECT_THROW(run(), CLI::CallForHelp);
  1536 +
  1537 + // These run in definition order.
  1538 + args = {"-a", "-b", "extra"};
  1539 + EXPECT_THROW(run(), CLI::Success);
  1540 +
  1541 + // Currently, the original order is not preserved when calling callbacks
  1542 + // args = {"-b", "-a", "extra"};
  1543 + // EXPECT_THROW(run(), CLI::CallForHelp);
  1544 +}
tests/SubcommandTest.cpp
@@ -64,21 +64,19 @@ TEST_F(TApp, MultiSubFallthrough) { @@ -64,21 +64,19 @@ TEST_F(TApp, MultiSubFallthrough) {
64 EXPECT_TRUE(app.got_subcommand(sub1)); 64 EXPECT_TRUE(app.got_subcommand(sub1));
65 EXPECT_TRUE(*sub1); 65 EXPECT_TRUE(*sub1);
66 EXPECT_TRUE(sub1->parsed()); 66 EXPECT_TRUE(sub1->parsed());
  67 + EXPECT_EQ(sub1->count(), (size_t)1);
67 68
68 EXPECT_TRUE(app.got_subcommand("sub2")); 69 EXPECT_TRUE(app.got_subcommand("sub2"));
69 EXPECT_TRUE(app.got_subcommand(sub2)); 70 EXPECT_TRUE(app.got_subcommand(sub2));
70 EXPECT_TRUE(*sub2); 71 EXPECT_TRUE(*sub2);
71 72
72 app.require_subcommand(); 73 app.require_subcommand();
73 -  
74 run(); 74 run();
75 75
76 app.require_subcommand(2); 76 app.require_subcommand(2);
77 -  
78 run(); 77 run();
79 78
80 app.require_subcommand(1); 79 app.require_subcommand(1);
81 -  
82 EXPECT_THROW(run(), CLI::ExtrasError); 80 EXPECT_THROW(run(), CLI::ExtrasError);
83 81
84 args = {"sub1"}; 82 args = {"sub1"};
@@ -90,6 +88,7 @@ TEST_F(TApp, MultiSubFallthrough) { @@ -90,6 +88,7 @@ TEST_F(TApp, MultiSubFallthrough) {
90 EXPECT_TRUE(*sub1); 88 EXPECT_TRUE(*sub1);
91 EXPECT_FALSE(*sub2); 89 EXPECT_FALSE(*sub2);
92 EXPECT_FALSE(sub2->parsed()); 90 EXPECT_FALSE(sub2->parsed());
  91 + EXPECT_EQ(sub2->count(), (size_t)0);
93 92
94 EXPECT_THROW(app.got_subcommand("sub3"), CLI::OptionNotFound); 93 EXPECT_THROW(app.got_subcommand("sub3"), CLI::OptionNotFound);
95 } 94 }
@@ -736,3 +735,45 @@ TEST_F(ManySubcommands, Unlimited) { @@ -736,3 +735,45 @@ TEST_F(ManySubcommands, Unlimited) {
736 run(); 735 run();
737 EXPECT_EQ(app.remaining(true), vs_t()); 736 EXPECT_EQ(app.remaining(true), vs_t());
738 } 737 }
  738 +
  739 +TEST_F(ManySubcommands, HelpFlags) {
  740 +
  741 + args = {"-h"};
  742 +
  743 + EXPECT_THROW(run(), CLI::CallForHelp);
  744 +
  745 + args = {"sub2", "-h"};
  746 +
  747 + EXPECT_THROW(run(), CLI::CallForHelp);
  748 +
  749 + args = {"-h", "sub2"};
  750 +
  751 + EXPECT_THROW(run(), CLI::CallForHelp);
  752 +}
  753 +
  754 +TEST_F(ManySubcommands, MaxCommands) {
  755 +
  756 + app.require_subcommand(2);
  757 +
  758 + args = {"sub1", "sub2"};
  759 + EXPECT_NO_THROW(run());
  760 +
  761 + // The extra subcommand counts as an extra
  762 + args = {"sub1", "sub2", "sub3"};
  763 + EXPECT_NO_THROW(run());
  764 + EXPECT_EQ(sub2->remaining().size(), 1);
  765 +
  766 + // Currently, setting sub2 to throw causes an extras error
  767 + // In the future, would passing on up to app's extras be better?
  768 +
  769 + app.allow_extras(false);
  770 + sub1->allow_extras(false);
  771 + sub2->allow_extras(false);
  772 +
  773 + args = {"sub1", "sub2"};
  774 +
  775 + EXPECT_NO_THROW(run());
  776 +
  777 + args = {"sub1", "sub2", "sub3"};
  778 + EXPECT_THROW(run(), CLI::ExtrasError);
  779 +}
tests/app_helper.hpp
@@ -16,6 +16,7 @@ struct TApp : public ::testing::Test { @@ -16,6 +16,7 @@ struct TApp : public ::testing::Test {
16 input_t args; 16 input_t args;
17 17
18 void run() { 18 void run() {
  19 + // It is okay to re-parse - clear is called automatically before a parse.
19 input_t newargs = args; 20 input_t newargs = args;
20 std::reverse(std::begin(newargs), std::end(newargs)); 21 std::reverse(std::begin(newargs), std::end(newargs));
21 app.parse(newargs); 22 app.parse(newargs);