Commit a5498bed17a20771d326b60c8533295a7695be38

Authored by Philip Top
Committed by GitHub
1 parent ba5ca8c4

feat: add an option to validate optional arguments like in a vector. (#668)

* add an option to validate optional arguments like in a vector.  This can resolve some issues with separating positionals from vector arguments

* style: pre-commit.ci fixes

* add some updates to the book

* style: pre-commit.ci fixes

* fix some precommit issues

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
README.md
@@ -591,6 +591,7 @@ There are several options that are supported on the main app and subcommands and @@ -591,6 +591,7 @@ There are several options that are supported on the main app and subcommands and
591 * `.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. 591 * `.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.
592 * `.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 592 * `.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
593 * `.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. 593 * `.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.
  594 +* `.validate_optional_arguments()`:🚧 Specify that optional arguments should pass validation before being assigned to an option. 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 subcommand or extra arguments.
594 * `.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. 595 * `.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.
595 * `.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. 596 * `.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.
596 * `.require_option()`: Require 1 or more options or option groups be used. 597 * `.require_option()`: Require 1 or more options or option groups be used.
book/chapters/internals.md
@@ -20,7 +20,7 @@ Obviously, you can&#39;t access `T` after the `add_` method is over, so it stores th @@ -20,7 +20,7 @@ Obviously, you can&#39;t access `T` after the `add_` method is over, so it stores th
20 20
21 Parsing follows the following procedure: 21 Parsing follows the following procedure:
22 22
23 -1. `_validate`: Make sure the defined options are self consistent. 23 +1. `_validate`: Make sure the defined options and subcommands are self consistent.
24 2. `_parse`: Main parsing routine. See below. 24 2. `_parse`: Main parsing routine. See below.
25 3. `_run_callback`: Run an App callback if present. 25 3. `_run_callback`: Run an App callback if present.
26 26
book/chapters/options.md
@@ -155,7 +155,7 @@ When you call `add_option`, you get a pointer to the added option. You can use t @@ -155,7 +155,7 @@ When you call `add_option`, you get a pointer to the added option. You can use t
155 | `->always_capture_default()` | Always run `capture_default_str()` when creating new options. Only useful on an App's `option_defaults`. | 155 | `->always_capture_default()` | Always run `capture_default_str()` when creating new options. Only useful on an App's `option_defaults`. |
156 | `->run_callback_for_default()` | Force the option callback to be executed or the variable set when the `default_val` is used. | 156 | `->run_callback_for_default()` | Force the option callback to be executed or the variable set when the `default_val` is used. |
157 | `->force_callback()` | Force the option callback to be executed regardless of whether the option was used or not. Will use the default_str if available, if no default is given the callback will be executed with an empty string as an argument, which will translate to a default initialized value, which can be compiler dependent | 157 | `->force_callback()` | Force the option callback to be executed regardless of whether the option was used or not. Will use the default_str if available, if no default is given the callback will be executed with an empty string as an argument, which will translate to a default initialized value, which can be compiler dependent |
158 -|`->trigger_on_parse()` | Have the option callback be triggered when the value is parsed vs. at the end of all parsing, the option callback can potentially be executed multiple times. Generally only useful if you have a user defined callback or validation check. Or potentially if a vector input is given multiple times as it will clear the results when a repeat option is given via command line. It will trigger the callbacks once per option call on the command line| 158 +| `->trigger_on_parse()` | Have the option callback be triggered when the value is parsed vs. at the end of all parsing, the option callback can potentially be executed multiple times. Generally only useful if you have a user defined callback or validation check. Or potentially if a vector input is given multiple times as it will clear the results when a repeat option is given via command line. It will trigger the callbacks once per option call on the command line|
159 | `->option_text(string)` | Sets the text between the option name and description. | 159 | `->option_text(string)` | Sets the text between the option name and description. |
160 160
161 The `->check(...)` and `->transform(...)` modifiers can also take a callback function of the form `bool function(std::string)` that runs on every value that the option receives, and returns a value that tells CLI11 whether the check passed or failed. 161 The `->check(...)` and `->transform(...)` modifiers can also take a callback function of the form `bool function(std::string)` that runs on every value that the option receives, and returns a value that tells CLI11 whether the check passed or failed.
@@ -286,6 +286,10 @@ There are some additional options that can be specified to modify an option for @@ -286,6 +286,10 @@ There are some additional options that can be specified to modify an option for
286 286
287 * `->run_callback_for_default()` will specify that the callback should be executed when a default_val is set. This is set automatically when appropriate though it can be turned on or off and any user specified callback for an option will be executed when the default value for an option is set. 287 * `->run_callback_for_default()` will specify that the callback should be executed when a default_val is set. This is set automatically when appropriate though it can be turned on or off and any user specified callback for an option will be executed when the default value for an option is set.
288 288
  289 +* `->force_callback()` will for the callback/value assignment to run at the conclusion of parsing regardless of whether the option was supplied or not. This can be used to force the default or execute some code.
  290 +
  291 +* `->trigger_on_parse()` will trigger the callback or value assignment each time the argument is passed. The value is reset if the option is supplied multiple times.
  292 +
289 ## Unusual circumstances 293 ## Unusual circumstances
290 294
291 There are a few cases where some things break down in the type system managing options and definitions. Using the `add_option` method defines a lambda function to extract a default value if required. In most cases this is either straightforward or a failure is detected automatically and handled. But in a few cases a streaming template is available that several layers down may not actually be defined. This results in CLI11 not being able to detect this circumstance automatically and will result in compile error. One specific known case is `boost::optional` if the boost optional_io header is included. This header defines a template for all boost optional values even if they do not actually have a streaming operator. For example `boost::optional<std::vector>` does not have a streaming operator but one is detected since it is part of a template. For these cases a secondary method `app->add_option_no_stream(...)` is provided that bypasses this operation completely and should compile in these cases. 295 There are a few cases where some things break down in the type system managing options and definitions. Using the `add_option` method defines a lambda function to extract a default value if required. In most cases this is either straightforward or a failure is detected automatically and handled. But in a few cases a streaming template is available that several layers down may not actually be defined. This results in CLI11 not being able to detect this circumstance automatically and will result in compile error. One specific known case is `boost::optional` if the boost optional_io header is included. This header defines a template for all boost optional values even if they do not actually have a streaming operator. For example `boost::optional<std::vector>` does not have a streaming operator but one is detected since it is part of a template. For these cases a secondary method `app->add_option_no_stream(...)` is provided that bypasses this operation completely and should compile in these cases.
book/chapters/subcommands.md
@@ -85,6 +85,8 @@ The following values are inherited when you add a new subcommand. This happens a @@ -85,6 +85,8 @@ The following values are inherited when you add a new subcommand. This happens a
85 * Fallthrough 85 * Fallthrough
86 * Group name 86 * Group name
87 * Max required subcommands 87 * Max required subcommands
  88 +* validate positional arguments
  89 +* validate optional arguments
88 90
89 ## Special modes 91 ## Special modes
90 92
@@ -126,3 +128,21 @@ This would allow calling help such as: @@ -126,3 +128,21 @@ This would allow calling help such as:
126 ./app help 128 ./app help
127 ./app help sub1 129 ./app help sub1
128 ``` 130 ```
  131 +
  132 +### Positional Validation
  133 +
  134 +Some arguments supplied on the command line may be legitamately applied to more than 1 positional argument. In this context enabling `positional_validation` on the application or subcommand will check any validators before applying the command line argument to the positional option. It is not an error to fail validation in this context, positional arguments not matching any validators will go into the `extra_args` field which may generate an error depending on settings.
  135 +
  136 +### Optional Argument Validation
  137 +
  138 +Similar to positional validation, there are occasional contexts in which case it might be ambiguous whether an argument should be applied to an option or a positional option.
  139 +
  140 +```c++
  141 + std::vector<std::string> vec;
  142 + std::vector<int> ivec;
  143 + app.add_option("pos", vec);
  144 + app.add_option("--args", ivec)->check(CLI::Number);
  145 + app.validate_optional_arguments();
  146 +```
  147 +
  148 +In this case a sequence of integers is expected for the argument and remaining strings go to the positional string vector. Without the `validate_optional_arguments()` active it would be impossible get any later arguments into the positional if the `--args` option is used. The validator in this context is used to make sure the optional arguments match with what the argument is expecting and if not the `-args` option is closed, and remaining arguments fall into the positional.
include/CLI/App.hpp
@@ -222,6 +222,9 @@ class App { @@ -222,6 +222,9 @@ class App {
222 /// If set to true positional options are validated before assigning INHERITABLE 222 /// If set to true positional options are validated before assigning INHERITABLE
223 bool validate_positionals_{false}; 223 bool validate_positionals_{false};
224 224
  225 + /// If set to true optional vector arguments are validated before assigning INHERITABLE
  226 + bool validate_optional_arguments_{false};
  227 +
225 /// indicator that the subcommand is silent and won't show up in subcommands list 228 /// indicator that the subcommand is silent and won't show up in subcommands list
226 /// This is potentially useful as a modifier subcommand 229 /// This is potentially useful as a modifier subcommand
227 bool silent_{false}; 230 bool silent_{false};
@@ -286,6 +289,7 @@ class App { @@ -286,6 +289,7 @@ class App {
286 ignore_underscore_ = parent_->ignore_underscore_; 289 ignore_underscore_ = parent_->ignore_underscore_;
287 fallthrough_ = parent_->fallthrough_; 290 fallthrough_ = parent_->fallthrough_;
288 validate_positionals_ = parent_->validate_positionals_; 291 validate_positionals_ = parent_->validate_positionals_;
  292 + validate_optional_arguments_ = parent_->validate_optional_arguments_;
289 configurable_ = parent_->configurable_; 293 configurable_ = parent_->configurable_;
290 allow_windows_style_options_ = parent_->allow_windows_style_options_; 294 allow_windows_style_options_ = parent_->allow_windows_style_options_;
291 group_ = parent_->group_; 295 group_ = parent_->group_;
@@ -450,6 +454,12 @@ class App { @@ -450,6 +454,12 @@ class App {
450 return this; 454 return this;
451 } 455 }
452 456
  457 + /// Set the subcommand to validate optional vector arguments before assigning
  458 + App *validate_optional_arguments(bool validate = true) {
  459 + validate_optional_arguments_ = validate;
  460 + return this;
  461 + }
  462 +
453 /// ignore extras in config files 463 /// ignore extras in config files
454 App *allow_config_extras(bool allow = true) { 464 App *allow_config_extras(bool allow = true) {
455 if(allow) { 465 if(allow) {
@@ -1715,6 +1725,8 @@ class App { @@ -1715,6 +1725,8 @@ class App {
1715 bool get_enabled_by_default() const { return (default_startup == startup_mode::enabled); } 1725 bool get_enabled_by_default() const { return (default_startup == startup_mode::enabled); }
1716 /// Get the status of validating positionals 1726 /// Get the status of validating positionals
1717 bool get_validate_positionals() const { return validate_positionals_; } 1727 bool get_validate_positionals() const { return validate_positionals_; }
  1728 + /// Get the status of validating optional vector arguments
  1729 + bool get_validate_optional_arguments() const { return validate_optional_arguments_; }
1718 1730
1719 /// Get the status of allow extras 1731 /// Get the status of allow extras
1720 config_extras_mode get_allow_config_extras() const { return allow_config_extras_; } 1732 config_extras_mode get_allow_config_extras() const { return allow_config_extras_; }
@@ -2801,6 +2813,7 @@ class App { @@ -2801,6 +2813,7 @@ class App {
2801 throw ArgumentMismatch::TypedAtLeast(op->get_name(), min_num, op->get_type_name()); 2813 throw ArgumentMismatch::TypedAtLeast(op->get_name(), min_num, op->get_type_name());
2802 } 2814 }
2803 2815
  2816 + // now check for optional arguments
2804 if(max_num > collected || op->get_allow_extra_args()) { // we allow optional arguments 2817 if(max_num > collected || op->get_allow_extra_args()) { // we allow optional arguments
2805 auto remreqpos = _count_remaining_positionals(true); 2818 auto remreqpos = _count_remaining_positionals(true);
2806 // we have met the minimum now optionally check up to the maximum 2819 // we have met the minimum now optionally check up to the maximum
@@ -2810,7 +2823,13 @@ class App { @@ -2810,7 +2823,13 @@ class App {
2810 if(remreqpos >= args.size()) { 2823 if(remreqpos >= args.size()) {
2811 break; 2824 break;
2812 } 2825 }
2813 - 2826 + if(validate_optional_arguments_) {
  2827 + std::string optarg = args.back();
  2828 + optarg = op->_validate(optarg, 0);
  2829 + if(!optarg.empty()) {
  2830 + break;
  2831 + }
  2832 + }
2814 op->add_result(args.back(), result_count); 2833 op->add_result(args.back(), result_count);
2815 parse_order_.push_back(op.get()); 2834 parse_order_.push_back(op.get());
2816 args.pop_back(); 2835 args.pop_back();
tests/AppTest.cpp
@@ -1476,6 +1476,34 @@ TEST_CASE_METHOD(TApp, &quot;BigPositional&quot;, &quot;[app]&quot;) { @@ -1476,6 +1476,34 @@ TEST_CASE_METHOD(TApp, &quot;BigPositional&quot;, &quot;[app]&quot;) {
1476 CHECK(vec == args); 1476 CHECK(vec == args);
1477 } 1477 }
1478 1478
  1479 +TEST_CASE_METHOD(TApp, "VectorArgAndPositional", "[app]") {
  1480 + std::vector<std::string> vec;
  1481 + std::vector<int> ivec;
  1482 + app.add_option("pos", vec);
  1483 + app.add_option("--args", ivec)->check(CLI::Number);
  1484 + app.validate_optional_arguments();
  1485 + args = {"one"};
  1486 +
  1487 + run();
  1488 + CHECK(vec == args);
  1489 +
  1490 + args = {"--args", "1", "2"};
  1491 +
  1492 + run();
  1493 + CHECK(ivec.size() == 2);
  1494 + vec.clear();
  1495 + ivec.clear();
  1496 +
  1497 + args = {"--args", "1", "2", "one", "two"};
  1498 + run();
  1499 +
  1500 + CHECK(vec.size() == 2);
  1501 + CHECK(ivec.size() == 2);
  1502 +
  1503 + app.validate_optional_arguments(false);
  1504 + CHECK_THROWS(run());
  1505 +}
  1506 +
1479 TEST_CASE_METHOD(TApp, "Reset", "[app]") { 1507 TEST_CASE_METHOD(TApp, "Reset", "[app]") {
1480 1508
1481 app.add_flag("--simple"); 1509 app.add_flag("--simple");