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 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 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 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 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 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 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 20  
21 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 24 2. `_parse`: Main parsing routine. See below.
25 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 155 | `->always_capture_default()` | Always run `capture_default_str()` when creating new options. Only useful on an App's `option_defaults`. |
156 156 | `->run_callback_for_default()` | Force the option callback to be executed or the variable set when the `default_val` is used. |
157 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 159 | `->option_text(string)` | Sets the text between the option name and description. |
160 160  
161 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 286  
287 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 293 ## Unusual circumstances
290 294  
291 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 85 * Fallthrough
86 86 * Group name
87 87 * Max required subcommands
  88 +* validate positional arguments
  89 +* validate optional arguments
88 90  
89 91 ## Special modes
90 92  
... ... @@ -126,3 +128,21 @@ This would allow calling help such as:
126 128 ./app help
127 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 222 /// If set to true positional options are validated before assigning INHERITABLE
223 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 228 /// indicator that the subcommand is silent and won't show up in subcommands list
226 229 /// This is potentially useful as a modifier subcommand
227 230 bool silent_{false};
... ... @@ -286,6 +289,7 @@ class App {
286 289 ignore_underscore_ = parent_->ignore_underscore_;
287 290 fallthrough_ = parent_->fallthrough_;
288 291 validate_positionals_ = parent_->validate_positionals_;
  292 + validate_optional_arguments_ = parent_->validate_optional_arguments_;
289 293 configurable_ = parent_->configurable_;
290 294 allow_windows_style_options_ = parent_->allow_windows_style_options_;
291 295 group_ = parent_->group_;
... ... @@ -450,6 +454,12 @@ class App {
450 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 463 /// ignore extras in config files
454 464 App *allow_config_extras(bool allow = true) {
455 465 if(allow) {
... ... @@ -1715,6 +1725,8 @@ class App {
1715 1725 bool get_enabled_by_default() const { return (default_startup == startup_mode::enabled); }
1716 1726 /// Get the status of validating positionals
1717 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 1731 /// Get the status of allow extras
1720 1732 config_extras_mode get_allow_config_extras() const { return allow_config_extras_; }
... ... @@ -2801,6 +2813,7 @@ class App {
2801 2813 throw ArgumentMismatch::TypedAtLeast(op->get_name(), min_num, op->get_type_name());
2802 2814 }
2803 2815  
  2816 + // now check for optional arguments
2804 2817 if(max_num > collected || op->get_allow_extra_args()) { // we allow optional arguments
2805 2818 auto remreqpos = _count_remaining_positionals(true);
2806 2819 // we have met the minimum now optionally check up to the maximum
... ... @@ -2810,7 +2823,13 @@ class App {
2810 2823 if(remreqpos >= args.size()) {
2811 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 2833 op->add_result(args.back(), result_count);
2815 2834 parse_order_.push_back(op.get());
2816 2835 args.pop_back();
... ...
tests/AppTest.cpp
... ... @@ -1476,6 +1476,34 @@ TEST_CASE_METHOD(TApp, &quot;BigPositional&quot;, &quot;[app]&quot;) {
1476 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 1507 TEST_CASE_METHOD(TApp, "Reset", "[app]") {
1480 1508  
1481 1509 app.add_flag("--simple");
... ...