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 12 ## Version 1.6.2: Help-all
2 13  
3 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 145 /// A pointer to the parent if this is a subcommand
146 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 151 /// Minimum required subcommands (not inheritable!)
152 152 size_t require_subcommand_min_ = 0;
... ... @@ -284,7 +284,7 @@ class App {
284 284 }
285 285  
286 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 289 /// Get the OptionDefault object, to set option defaults
290 290 OptionDefaults *option_defaults() { return &option_defaults_; }
... ... @@ -408,8 +408,7 @@ class App {
408 408  
409 409 // Empty name will simply remove the help flag
410 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 412 help_ptr_->configurable(false);
414 413 }
415 414  
... ... @@ -425,8 +424,7 @@ class App {
425 424  
426 425 // Empty name will simply remove the help all flag
427 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 428 help_all_ptr_->configurable(false);
431 429 }
432 430  
... ... @@ -826,6 +824,10 @@ class App {
826 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 831 /// Changes the group membership
830 832 App *group(std::string name) {
831 833 group_ = name;
... ... @@ -870,7 +872,7 @@ class App {
870 872  
871 873 /// Check to see if this subcommand was parsed, true only if received on command line.
872 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 878 /// @name Extras for subclassing
... ... @@ -917,15 +919,16 @@ class App {
917 919 /// Changes the vector to the remaining options.
918 920 void parse(std::vector<std::string> &args) {
919 921 // Clear if parsed
920   - if(parsed_)
  922 + if(parsed_ > 0)
921 923 clear();
922 924  
923   - // Redundant (set by _parse on commands/subcommands)
  925 + // _parse is incremented in commands/subcommands,
924 926 // but placed here to make sure this is cleared when
925 927 // running parse after an error is thrown, even by _validate.
926   - parsed_ = true;
927   -
  928 + parsed_ = 1;
928 929 _validate();
  930 + parsed_ = 0;
  931 +
929 932 _parse(args);
930 933 run_callback();
931 934 }
... ... @@ -1016,11 +1019,11 @@ class App {
1016 1019 /// Check to see if given subcommand was selected
1017 1020 bool got_subcommand(App *subcom) const {
1018 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 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 1029 /// @name Help
... ... @@ -1282,19 +1285,10 @@ class App {
1282 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 1292 // Process an INI file
1299 1293 if(config_ptr_ != nullptr) {
1300 1294 if(*config_ptr_) {
... ... @@ -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 1312 for(const Option_p &opt : options_) {
1317 1313 if(opt->count() == 0 && !opt->envname_.empty()) {
1318 1314 char *buffer = nullptr;
... ... @@ -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 1344 for(const Option_p &opt : options_) {
1343 1345 if(opt->count() > 0 && !opt->get_callback_run()) {
1344 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 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 1384 // Required or partially filled
1355 1385 if(opt->get_required() || opt->count() != 0) {
... ... @@ -1375,7 +1405,26 @@ class App {
1375 1405 if(require_subcommand_min_ > selected_subcommands.size())
1376 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 1428 if(!(allow_extras_ || prefix_command_)) {
1380 1429 size_t num_left_over = remaining_size();
1381 1430 if(num_left_over > 0) {
... ... @@ -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 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 215 /// Options store a callback to do all the work
216 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 219 /// @name Parsing results
223 220 ///@{
... ... @@ -423,14 +420,6 @@ class Option : public OptionBase&lt;Option&gt; {
423 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 424 /// @name Accessors
436 425 ///@{
... ... @@ -450,9 +439,6 @@ class Option : public OptionBase&lt;Option&gt; {
450 439 /// The default value (for help printing)
451 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 442 /// Get the callback function
457 443 callback_t get_callback() const { return callback_; }
458 444  
... ... @@ -556,6 +542,8 @@ class Option : public OptionBase&lt;Option&gt; {
556 542 /// Process the callback
557 543 void run_callback() {
558 544  
  545 + callback_run_ = true;
  546 +
559 547 // Run the validators (can change the string)
560 548 if(!validators_.empty()) {
561 549 for(std::string &result : results_)
... ...
tests/AppTest.cpp
... ... @@ -1520,3 +1520,25 @@ TEST_F(TApp, EmptyOptionFail) {
1520 1520 args = {"--each", "that"};
1521 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 64 EXPECT_TRUE(app.got_subcommand(sub1));
65 65 EXPECT_TRUE(*sub1);
66 66 EXPECT_TRUE(sub1->parsed());
  67 + EXPECT_EQ(sub1->count(), (size_t)1);
67 68  
68 69 EXPECT_TRUE(app.got_subcommand("sub2"));
69 70 EXPECT_TRUE(app.got_subcommand(sub2));
70 71 EXPECT_TRUE(*sub2);
71 72  
72 73 app.require_subcommand();
73   -
74 74 run();
75 75  
76 76 app.require_subcommand(2);
77   -
78 77 run();
79 78  
80 79 app.require_subcommand(1);
81   -
82 80 EXPECT_THROW(run(), CLI::ExtrasError);
83 81  
84 82 args = {"sub1"};
... ... @@ -90,6 +88,7 @@ TEST_F(TApp, MultiSubFallthrough) {
90 88 EXPECT_TRUE(*sub1);
91 89 EXPECT_FALSE(*sub2);
92 90 EXPECT_FALSE(sub2->parsed());
  91 + EXPECT_EQ(sub2->count(), (size_t)0);
93 92  
94 93 EXPECT_THROW(app.got_subcommand("sub3"), CLI::OptionNotFound);
95 94 }
... ... @@ -736,3 +735,45 @@ TEST_F(ManySubcommands, Unlimited) {
736 735 run();
737 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 16 input_t args;
17 17  
18 18 void run() {
  19 + // It is okay to re-parse - clear is called automatically before a parse.
19 20 input_t newargs = args;
20 21 std::reverse(std::begin(newargs), std::end(newargs));
21 22 app.parse(newargs);
... ...