Commit 76d2cde6568c9c8870b728aa9bc64b70b29127fd

Authored by Philip Top
Committed by Henry Schreiner
1 parent a1c18e05

Positional argument checks (#262)

* some tweaks with optional

* remove set_results function that was bypassing some of the result processing in some cases of config files.

* add positional Validator example and tests add CLI::Number validator.

* add positional Validator example and tests add CLI::Number validator.

* do some reformatting for style checks and remove auto in test lambda.
README.md
... ... @@ -342,8 +342,9 @@ CLI11 has several Validators built-in that perform some common checks
342 342 - `CLI::ExistingPath`: Requires that the path (file or directory) exists.
343 343 - `CLI::NonexistentPath`: Requires that the path does not exist.
344 344 - `CLI::Range(min,max)`: Requires that the option be between min and max (make sure to use floating point if needed). Min defaults to 0.
345   -- `CLI::Bounded(min,max)`: ๐Ÿšง Modify the input such that it is always between min and max (make sure to use floating point if needed). Min defaults to 0. Will produce an Error if conversion is not possible.
346   -- `CLI::PositiveNumber`: ๐Ÿšง Requires the number be greater or equal to 0.
  345 +- `CLI::Bounded(min,max)`: ๐Ÿšง Modify the input such that it is always between min and max (make sure to use floating point if needed). Min defaults to 0. Will produce an error if conversion is not possible.
  346 +- `CLI::PositiveNumber`: ๐Ÿšง Requires the number be greater or equal to 0
  347 +- `CLI::Number`: ๐Ÿšง Requires the input be a number.
347 348 - `CLI::ValidIPV4`: ๐Ÿšง Requires that the option be a valid IPv4 string e.g. `'255.255.255.255'`, `'10.1.1.7'`.
348 349  
349 350 These Validators can be used by simply passing the name into the `check` or `transform` methods on an option
... ... @@ -467,6 +468,7 @@ There are several options that are supported on the main app and subcommands and
467 468 - `.disable()`: ๐Ÿšง Specify that the subcommand is disabled, if given with a bool value it will enable or disable the subcommand or option group.
468 469 - `.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.
469 470 - `.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.
  471 +- `.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.
470 472 - `.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.
471 473 - `.require_option()`: ๐Ÿšง Require 1 or more options or option groups be used.
472 474 - `.require_option(N)`: ๐Ÿšง Require `N` options or option groups, if `N>0`, or up to `N` if `N<0`. `N=0` resets to the default to 0 or more.
... ...
examples/CMakeLists.txt
... ... @@ -109,6 +109,22 @@ add_test(NAME positional_arity_fail COMMAND positional_arity 1 one two)
109 109 set_property(TEST positional_arity_fail PROPERTY PASS_REGULAR_EXPRESSION
110 110 "Could not convert")
111 111  
  112 + add_cli_exe(positional_validation positional_validation.cpp)
  113 +add_test(NAME positional_validation1 COMMAND positional_validation one )
  114 +set_property(TEST positional_validation1 PROPERTY PASS_REGULAR_EXPRESSION
  115 + "File 1 = one")
  116 +add_test(NAME positional_validation2 COMMAND positional_validation one 1 2 two )
  117 +set_property(TEST positional_validation2 PROPERTY PASS_REGULAR_EXPRESSION
  118 + "File 1 = one"
  119 + "File 2 = two")
  120 +add_test(NAME positional_validation3 COMMAND positional_validation 1 2 one)
  121 +set_property(TEST positional_validation3 PROPERTY PASS_REGULAR_EXPRESSION
  122 + "File 1 = one")
  123 +add_test(NAME positional_validation4 COMMAND positional_validation 1 one two 2)
  124 +set_property(TEST positional_validation4 PROPERTY PASS_REGULAR_EXPRESSION
  125 + "File 1 = one"
  126 + "File 2 = two")
  127 +
112 128 add_cli_exe(shapes shapes.cpp)
113 129 add_test(NAME shapes_all COMMAND shapes circle 4.4 circle 10.7 rectangle 4 4 circle 2.3 triangle 4.5 ++ rectangle 2.1 ++ circle 234.675)
114 130 set_property(TEST shapes_all PROPERTY PASS_REGULAR_EXPRESSION
... ...
examples/positional_validation.cpp 0 โ†’ 100644
  1 +#include "CLI/CLI.hpp"
  2 +
  3 +int main(int argc, char **argv) {
  4 +
  5 + CLI::App app("test for positional validation");
  6 +
  7 + int num1 = -1, num2 = -1;
  8 + app.add_option("num1", num1, "first number")->check(CLI::Number);
  9 + app.add_option("num2", num2, "second number")->check(CLI::Number);
  10 + std::string file1, file2;
  11 + app.add_option("file1", file1, "first file")->required();
  12 + app.add_option("file2", file2, "second file");
  13 + app.validate_positionals();
  14 +
  15 + CLI11_PARSE(app, argc, argv);
  16 +
  17 + if(num1 != -1)
  18 + std::cout << "Num1 = " << num1 << '\n';
  19 +
  20 + if(num2 != -1)
  21 + std::cout << "Num2 = " << num2 << '\n';
  22 +
  23 + std::cout << "File 1 = " << file1 << '\n';
  24 + if(!file2.empty()) {
  25 + std::cout << "File 2 = " << file2 << '\n';
  26 + }
  27 +
  28 + return 0;
  29 +}
... ...
include/CLI/App.hpp
... ... @@ -187,7 +187,8 @@ class App {
187 187 bool disabled_by_default_{false};
188 188 /// If set to true the subcommand will be reenabled at the start of each parse
189 189 bool enabled_by_default_{false};
190   -
  190 + /// If set to true positional options are validated before assigning INHERITABLE
  191 + bool validate_positionals_{false};
191 192 /// A pointer to the parent if this is a subcommand
192 193 App *parent_{nullptr};
193 194  
... ... @@ -250,6 +251,7 @@ class App {
250 251 ignore_case_ = parent_->ignore_case_;
251 252 ignore_underscore_ = parent_->ignore_underscore_;
252 253 fallthrough_ = parent_->fallthrough_;
  254 + validate_positionals_ = parent_->validate_positionals_;
253 255 allow_windows_style_options_ = parent_->allow_windows_style_options_;
254 256 group_ = parent_->group_;
255 257 footer_ = parent_->footer_;
... ... @@ -334,6 +336,12 @@ class App {
334 336 return this;
335 337 }
336 338  
  339 + /// Set the subcommand to validate positional arguments before assigning
  340 + App *validate_positionals(bool validate = true) {
  341 + validate_positionals_ = validate;
  342 + return this;
  343 + }
  344 +
337 345 /// Remove the error when extras are left over on the command line.
338 346 /// Will also call App::allow_extras().
339 347 App *allow_config_extras(bool allow = true) {
... ... @@ -489,18 +497,19 @@ class App {
489 497 return add_option(option_name, CLI::callback_t(), option_description, false);
490 498 }
491 499  
492   - /// Add option for non-vectors with a default print
  500 + /// Add option for non-vectors with a default print, allow template to specify conversion type
493 501 template <typename T,
494   - enable_if_t<!is_vector<T>::value && !std::is_const<T>::value, detail::enabler> = detail::dummy>
  502 + typename XC = T,
  503 + enable_if_t<!is_vector<XC>::value && !std::is_const<XC>::value, detail::enabler> = detail::dummy>
495 504 Option *add_option(std::string option_name,
496 505 T &variable, ///< The variable to set
497 506 std::string option_description,
498 507 bool defaulted) {
499   -
500   - CLI::callback_t fun = [&variable](CLI::results_t res) { return detail::lexical_cast(res[0], variable); };
  508 + static_assert(std::is_constructible<T, XC>::value, "assign type must be assignable from conversion type");
  509 + CLI::callback_t fun = [&variable](CLI::results_t res) { return detail::lexical_cast<XC>(res[0], variable); };
501 510  
502 511 Option *opt = add_option(option_name, fun, option_description, defaulted);
503   - opt->type_name(detail::type_name<T>());
  512 + opt->type_name(detail::type_name<XC>());
504 513 if(defaulted) {
505 514 std::stringstream out;
506 515 out << variable;
... ... @@ -1654,6 +1663,8 @@ class App {
1654 1663  
1655 1664 /// Get the status of disabled by default
1656 1665 bool get_enabled_by_default() const { return enabled_by_default_; }
  1666 + /// Get the status of validating positionals
  1667 + bool get_validate_positionals() const { return validate_positionals_; }
1657 1668  
1658 1669 /// Get the status of allow extras
1659 1670 bool get_allow_config_extras() const { return allow_config_extras_; }
... ... @@ -2192,7 +2203,7 @@ class App {
2192 2203 op->add_result(res);
2193 2204  
2194 2205 } else {
2195   - op->set_results(item.inputs);
  2206 + op->add_result(item.inputs);
2196 2207 op->run_callback();
2197 2208 }
2198 2209 }
... ... @@ -2274,7 +2285,13 @@ class App {
2274 2285 // Eat options, one by one, until done
2275 2286 if(opt->get_positional() &&
2276 2287 (static_cast<int>(opt->count()) < opt->get_items_expected() || opt->get_items_expected() < 0)) {
2277   -
  2288 + if(validate_positionals_) {
  2289 + std::string pos = positional;
  2290 + pos = opt->_validate(pos);
  2291 + if(!pos.empty()) {
  2292 + continue;
  2293 + }
  2294 + }
2278 2295 opt->add_result(positional);
2279 2296 parse_order_.push_back(opt.get());
2280 2297 args.pop_back();
... ...
include/CLI/Option.hpp
... ... @@ -666,19 +666,11 @@ class Option : public OptionBase&lt;Option&gt; {
666 666  
667 667 // Run the validators (can change the string)
668 668 if(!validators_.empty()) {
669   - for(std::string &result : results_)
670   - for(const auto &vali : validators_) {
671   - std::string err_msg;
672   -
673   - try {
674   - err_msg = vali(result);
675   - } catch(const ValidationError &err) {
676   - throw ValidationError(get_name(), err.what());
677   - }
678   -
679   - if(!err_msg.empty())
680   - throw ValidationError(get_name(), err_msg);
681   - }
  669 + for(std::string &result : results_) {
  670 + auto err_msg = _validate(result);
  671 + if(!err_msg.empty())
  672 + throw ValidationError(get_name(), err_msg);
  673 + }
682 674 }
683 675 if(!(callback_)) {
684 676 return;
... ... @@ -842,13 +834,6 @@ class Option : public OptionBase&lt;Option&gt; {
842 834 return this;
843 835 }
844 836  
845   - /// Set the results vector all at once
846   - Option *set_results(std::vector<std::string> result_vector) {
847   - results_ = std::move(result_vector);
848   - callback_run_ = false;
849   - return this;
850   - }
851   -
852 837 /// Get a copy of the results
853 838 std::vector<std::string> results() const { return results_; }
854 839  
... ... @@ -963,6 +948,21 @@ class Option : public OptionBase&lt;Option&gt; {
963 948 }
964 949  
965 950 private:
  951 + // run through the validators
  952 + std::string _validate(std::string &result) {
  953 + std::string err_msg;
  954 + for(const auto &vali : validators_) {
  955 + try {
  956 + err_msg = vali(result);
  957 + } catch(const ValidationError &err) {
  958 + err_msg = err.what();
  959 + }
  960 + if(!err_msg.empty())
  961 + break;
  962 + }
  963 + return err_msg;
  964 + }
  965 +
966 966 int _add_result(std::string &&result) {
967 967 int result_count = 0;
968 968 if(delimiter_ == '\0') {
... ...
include/CLI/Validators.hpp
... ... @@ -321,6 +321,20 @@ class PositiveNumber : public Validator {
321 321 }
322 322 };
323 323  
  324 +/// Validate the argument is a number and greater than or equal to 0
  325 +class Number : public Validator {
  326 + public:
  327 + Number() : Validator("NUMBER") {
  328 + func_ = [](std::string &number_str) {
  329 + double number;
  330 + if(!detail::lexical_cast(number_str, number)) {
  331 + return "Failed parsing as a number " + number_str;
  332 + }
  333 + return std::string();
  334 + };
  335 + }
  336 +};
  337 +
324 338 } // namespace detail
325 339  
326 340 // Static is not needed here, because global const implies static.
... ... @@ -343,6 +357,9 @@ const detail::IPV4Validator ValidIPV4;
343 357 /// Check for a positive number
344 358 const detail::PositiveNumber PositiveNumber;
345 359  
  360 +/// Check for a number
  361 +const detail::Number Number;
  362 +
346 363 /// Produce a range (factory). Min and max are inclusive.
347 364 class Range : public Validator {
348 365 public:
... ...
tests/AppTest.cpp
... ... @@ -962,6 +962,27 @@ TEST_F(TApp, PositionalAtEnd) {
962 962 EXPECT_THROW(run(), CLI::ExtrasError);
963 963 }
964 964  
  965 +// Tests positionals at end
  966 +TEST_F(TApp, PositionalValidation) {
  967 + std::string options;
  968 + std::string foo;
  969 +
  970 + app.add_option("bar", options)->check(CLI::Number);
  971 + app.add_option("foo", foo);
  972 + app.validate_positionals();
  973 + args = {"1", "param1"};
  974 + run();
  975 +
  976 + EXPECT_EQ(options, "1");
  977 + EXPECT_EQ(foo, "param1");
  978 +
  979 + args = {"param1", "1"};
  980 + run();
  981 +
  982 + EXPECT_EQ(options, "1");
  983 + EXPECT_EQ(foo, "param1");
  984 +}
  985 +
965 986 TEST_F(TApp, PositionalNoSpaceLong) {
966 987 std::vector<std::string> options;
967 988 std::string foo, bar;
... ...
tests/CreationTest.cpp
... ... @@ -467,7 +467,7 @@ TEST_F(TApp, GetNameCheck) {
467 467 }
468 468  
469 469 TEST_F(TApp, SubcommandDefaults) {
470   - // allow_extras, prefix_command, ignore_case, fallthrough, group, min/max subcommand
  470 + // allow_extras, prefix_command, ignore_case, fallthrough, group, min/max subcommand, validate_positionals
471 471  
472 472 // Initial defaults
473 473 EXPECT_FALSE(app.get_allow_extras());
... ... @@ -481,6 +481,8 @@ TEST_F(TApp, SubcommandDefaults) {
481 481 EXPECT_FALSE(app.get_allow_windows_style_options());
482 482 #endif
483 483 EXPECT_FALSE(app.get_fallthrough());
  484 + EXPECT_FALSE(app.get_validate_positionals());
  485 +
484 486 EXPECT_EQ(app.get_footer(), "");
485 487 EXPECT_EQ(app.get_group(), "Subcommands");
486 488 EXPECT_EQ(app.get_require_subcommand_min(), 0u);
... ... @@ -498,6 +500,7 @@ TEST_F(TApp, SubcommandDefaults) {
498 500 #endif
499 501  
500 502 app.fallthrough();
  503 + app.validate_positionals();
501 504 app.footer("footy");
502 505 app.group("Stuff");
503 506 app.require_subcommand(2, 3);
... ... @@ -516,6 +519,7 @@ TEST_F(TApp, SubcommandDefaults) {
516 519 EXPECT_TRUE(app2->get_allow_windows_style_options());
517 520 #endif
518 521 EXPECT_TRUE(app2->get_fallthrough());
  522 + EXPECT_TRUE(app2->get_validate_positionals());
519 523 EXPECT_EQ(app2->get_footer(), "footy");
520 524 EXPECT_EQ(app2->get_group(), "Stuff");
521 525 EXPECT_EQ(app2->get_require_subcommand_min(), 0u);
... ...
tests/HelpersTest.cpp
... ... @@ -239,6 +239,21 @@ TEST(Validators, PositiveValidator) {
239 239 EXPECT_FALSE(CLI::PositiveNumber(num).empty());
240 240 }
241 241  
  242 +TEST(Validators, NumberValidator) {
  243 + std::string num = "1.1.1.1";
  244 + EXPECT_FALSE(CLI::Number(num).empty());
  245 + num = "1.7";
  246 + EXPECT_TRUE(CLI::Number(num).empty());
  247 + num = "10000";
  248 + EXPECT_TRUE(CLI::Number(num).empty());
  249 + num = "-0.000";
  250 + EXPECT_TRUE(CLI::Number(num).empty());
  251 + num = "+1.55";
  252 + EXPECT_TRUE(CLI::Number(num).empty());
  253 + num = "a";
  254 + EXPECT_FALSE(CLI::Number(num).empty());
  255 +}
  256 +
242 257 TEST(Validators, CombinedAndRange) {
243 258 auto crange = CLI::Range(0, 12) & CLI::Range(4, 16);
244 259 EXPECT_TRUE(crange("4").empty());
... ...
tests/OptionalTest.cpp
... ... @@ -62,6 +62,20 @@ TEST_F(TApp, BoostOptionalTest) {
62 62 EXPECT_EQ(*opt, 3);
63 63 }
64 64  
  65 +TEST_F(TApp, BoostOptionalVector) {
  66 + boost::optional<std::vector<int>> opt;
  67 + app.add_option_function<std::vector<int>>("-v,--vec", [&opt](const std::vector<int> &v) { opt = v; }, "some vector")
  68 + ->expected(3);
  69 + run();
  70 + EXPECT_FALSE(opt);
  71 +
  72 + args = {"-v", "1", "4", "5"};
  73 + run();
  74 + EXPECT_TRUE(opt);
  75 + std::vector<int> expV{1, 4, 5};
  76 + EXPECT_EQ(*opt, expV);
  77 +}
  78 +
65 79 #endif
66 80  
67 81 #if !CLI11_OPTIONAL
... ...