From 571fb07cfb866d7754e0154655d5bbb97b1fc8ec Mon Sep 17 00:00:00 2001 From: Philip Top Date: Wed, 20 Feb 2019 10:15:34 -0800 Subject: [PATCH] Add a ! operator to Validators for checking a false condition (#230) --- README.md | 2 +- include/CLI/App.hpp | 2 +- include/CLI/Validators.hpp | 23 +++++++++++++++++++++++ tests/AppTest.cpp | 18 ++++++++++++++++++ tests/SetTest.cpp | 14 ++++++++++++++ 5 files changed, 57 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 16e959d56..c4ac160c9 100644 --- a/README.md +++ b/README.md @@ -274,7 +274,7 @@ Before parsing, you can set the following options: - `->configurable(false)`: Disable this option from being in a configuration file. -These options return the `Option` pointer, so you can chain them together, and even skip storing the pointer entirely. Check takes any function that has the signature `void(const std::string&)`; it should throw a `ValidationError` when validation fails. The help message will have the name of the parent option prepended. Since `check` and `transform` use the same underlying mechanism, you can chain as many as you want, and they will be executed in order. If you just want to see the unconverted values, use `.results()` to get the `std::vector` of results. Validate can also be a subclass of `CLI::Validator`, in which case it can also set the type name and can be combined with `&` and `|` (all built-in validators are this sort). +These options return the `Option` pointer, so you can chain them together, and even skip storing the pointer entirely. Check takes any function that has the signature `void(const std::string&)`; it should throw a `ValidationError` when validation fails. The help message will have the name of the parent option prepended. Since `check` and `transform` use the same underlying mechanism, you can chain as many as you want, and they will be executed in order. If you just want to see the unconverted values, use `.results()` to get the `std::vector` of results. Validate can also be a subclass of `CLI::Validator`, in which case it can also set the type name and can be combined with `&` and `|` (all built-in validators are this sort). Validators can also be inverted with `!` such as `->check(!CLI::ExistingFile)` which would check that a file doesn't exist. The `IsMember` validator lets you specify a set of predefined options. You can pass any container or copyable pointer (including `std::shared_ptr`) to a container to this validator; the container just needs to be iterable and have a `::value_type`. The type should be convertible from a string. You can use an initializer list directly if you like. If you need to modify the set later, the pointer form lets you do that; the type message and check will correctly refer to the current version of the set. After specifying a set of options, you can also specify "filter" functions of the form `T(T)`, where `T` is the type of the values. The most common choices probably will be `CLI::ignore_case` an `CLI::ignore_underscore`. diff --git a/include/CLI/App.hpp b/include/CLI/App.hpp index 381246aa1..b30022f85 100644 --- a/include/CLI/App.hpp +++ b/include/CLI/App.hpp @@ -292,7 +292,7 @@ class App { return this; } - /// specify that the positional arguments are only at the end of the sequence + /// Specify that the positional arguments are only at the end of the sequence App *positionals_at_end(bool value = true) { positionals_at_end_ = value; return this; diff --git a/include/CLI/Validators.hpp b/include/CLI/Validators.hpp index e050669fe..4ffe09130 100644 --- a/include/CLI/Validators.hpp +++ b/include/CLI/Validators.hpp @@ -100,6 +100,29 @@ class Validator { }; return newval; } + + /// Create a validator that fails when a given validator succeeds + Validator operator!() const { + Validator newval; + std::string typestring = tname; + if(tname.empty()) { + typestring = tname_function(); + } + newval.tname = "NOT " + typestring; + + std::string failString = "check " + typestring + " succeeded improperly"; + // Give references (will make a copy in lambda function) + const std::function &f1 = func; + + newval.func = [f1, failString](std::string &test) -> std::string { + std::string s1 = f1(test); + if(s1.empty()) + return failString; + else + return std::string(); + }; + return newval; + } }; // The implementation of the built in validators is using the Validator class; diff --git a/tests/AppTest.cpp b/tests/AppTest.cpp index 93b63fc91..323a17ac1 100644 --- a/tests/AppTest.cpp +++ b/tests/AppTest.cpp @@ -1317,6 +1317,24 @@ TEST_F(TApp, FileExists) { EXPECT_FALSE(CLI::ExistingFile(myfile).empty()); } +TEST_F(TApp, NotFileExists) { + std::string myfile{"TestNonFileNotUsed.txt"}; + EXPECT_FALSE(CLI::ExistingFile(myfile).empty()); + + std::string filename = "Failed"; + app.add_option("--file", filename)->check(!CLI::ExistingFile); + args = {"--file", myfile}; + + EXPECT_NO_THROW(run()); + + bool ok = static_cast(std::ofstream(myfile.c_str()).put('a')); // create file + EXPECT_TRUE(ok); + EXPECT_THROW(run(), CLI::ValidationError); + + std::remove(myfile.c_str()); + EXPECT_FALSE(CLI::ExistingFile(myfile).empty()); +} + TEST_F(TApp, VectorFixedString) { std::vector strvec; std::vector answer{"mystring", "mystring2", "mystring3"}; diff --git a/tests/SetTest.cpp b/tests/SetTest.cpp index 079c821e0..6d3e9c286 100644 --- a/tests/SetTest.cpp +++ b/tests/SetTest.cpp @@ -414,6 +414,20 @@ TEST_F(TApp, InSetIgnoreCasePointer) { EXPECT_THROW(run(), CLI::ArgumentMismatch); } +TEST_F(TApp, NotInSetIgnoreCasePointer) { + + std::set *options = new std::set{"one", "Two", "THREE"}; + std::string choice; + app.add_option("-q,--quick", choice)->check(!CLI::IsMember(*options, CLI::ignore_case)); + + args = {"--quick", "One"}; + EXPECT_THROW(run(), CLI::ValidationError); + + args = {"--quick", "four"}; + run(); + EXPECT_EQ(choice, "four"); +} + TEST_F(TApp, InSetIgnoreUnderscore) { std::string choice;