mirror of
https://github.com/CLIUtils/CLI11.git
synced 2025-04-29 12:13:52 +00:00
Adding smart validators
This commit is contained in:
parent
3917b1ab59
commit
2b6b62c52c
@ -272,6 +272,14 @@ class Option : public OptionBase<Option> {
|
||||
return this;
|
||||
}
|
||||
|
||||
/// Adds a validator with a built in type name
|
||||
Option *check(const Validator &validator) {
|
||||
validators_.emplace_back(validator.func);
|
||||
if(!validator.tname.empty())
|
||||
set_type_name(validator.tname);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// Adds a validator
|
||||
Option *check(std::function<std::string(const std::string &)> validator) {
|
||||
validators_.emplace_back(validator);
|
||||
|
@ -18,74 +18,171 @@
|
||||
namespace CLI {
|
||||
|
||||
/// @defgroup validator_group Validators
|
||||
|
||||
/// @brief Some validators that are provided
|
||||
///
|
||||
/// These are simple `void(std::string&)` validators that are useful. They throw
|
||||
/// a ValidationError if they fail (or the normally expected error if the cast fails)
|
||||
/// These are simple `std::string(const std::string&)` validators that are useful. They return
|
||||
/// a string if the validation fails. A custom struct is provided, as well, with the same user
|
||||
/// semantics, but with the ability to provide a new type name.
|
||||
/// @{
|
||||
|
||||
/// Check for an existing file
|
||||
inline std::string ExistingFile(const std::string &filename) {
|
||||
struct stat buffer;
|
||||
bool exist = stat(filename.c_str(), &buffer) == 0;
|
||||
bool is_dir = (buffer.st_mode & S_IFDIR) != 0;
|
||||
if(!exist) {
|
||||
return "File does not exist: " + filename;
|
||||
} else if(is_dir) {
|
||||
return "File is actually a directory: " + filename;
|
||||
}
|
||||
return std::string();
|
||||
}
|
||||
///
|
||||
struct Validator {
|
||||
/// This is the type name, if emtpy the type name will not be changed
|
||||
std::string tname;
|
||||
std::function<std::string(const std::string &filename)> func;
|
||||
|
||||
/// Check for an existing directory
|
||||
inline std::string ExistingDirectory(const std::string &filename) {
|
||||
struct stat buffer;
|
||||
bool exist = stat(filename.c_str(), &buffer) == 0;
|
||||
bool is_dir = (buffer.st_mode & S_IFDIR) != 0;
|
||||
if(!exist) {
|
||||
return "Directory does not exist: " + filename;
|
||||
} else if(!is_dir) {
|
||||
return "Directory is actually a file: " + filename;
|
||||
/// This is the required operator for a validator - provided to help
|
||||
/// users (CLI11 uses the func directly)
|
||||
std::string operator()(const std::string &filename) const { return func(filename); };
|
||||
|
||||
/// Combining validators is a new validator
|
||||
Validator operator&(const Validator &other) const {
|
||||
Validator newval;
|
||||
newval.tname = (tname == other.tname ? tname : "");
|
||||
|
||||
// Give references (will make a copy in lambda function)
|
||||
const std::function<std::string(const std::string &filename)> &f1 = func;
|
||||
const std::function<std::string(const std::string &filename)> &f2 = other.func;
|
||||
|
||||
newval.func = [f1, f2](const std::string &filename) {
|
||||
std::string s1 = f1(filename);
|
||||
std::string s2 = f2(filename);
|
||||
if(!s1.empty() && !s2.empty())
|
||||
return s1 + " & " + s2;
|
||||
else
|
||||
return s1 + s2;
|
||||
};
|
||||
return newval;
|
||||
}
|
||||
return std::string();
|
||||
}
|
||||
|
||||
/// Combining validators is a new validator
|
||||
Validator operator|(const Validator &other) const {
|
||||
Validator newval;
|
||||
newval.tname = (tname == other.tname ? tname : "");
|
||||
|
||||
// Give references (will make a copy in lambda function)
|
||||
const std::function<std::string(const std::string &filename)> &f1 = func;
|
||||
const std::function<std::string(const std::string &filename)> &f2 = other.func;
|
||||
|
||||
newval.func = [f1, f2](const std::string &filename) {
|
||||
std::string s1 = f1(filename);
|
||||
std::string s2 = f2(filename);
|
||||
if(s1.empty() || s2.empty())
|
||||
return std::string();
|
||||
else
|
||||
return s1 + " & " + s2;
|
||||
};
|
||||
return newval;
|
||||
}
|
||||
};
|
||||
|
||||
// The implemntation of the built in validators is using the Validator class;
|
||||
// the user is only expected to use the const static versions (since there's no setup).
|
||||
// Therefore, this is in detail.
|
||||
namespace detail {
|
||||
|
||||
/// Check for an existing file (returns error message if check fails)
|
||||
struct ExistingFileValidator : public Validator {
|
||||
ExistingFileValidator() {
|
||||
tname = "FILE";
|
||||
func = [](const std::string &filename) {
|
||||
struct stat buffer;
|
||||
bool exist = stat(filename.c_str(), &buffer) == 0;
|
||||
bool is_dir = (buffer.st_mode & S_IFDIR) != 0;
|
||||
if(!exist) {
|
||||
return "File does not exist: " + filename;
|
||||
} else if(is_dir) {
|
||||
return "File is actually a directory: " + filename;
|
||||
}
|
||||
return std::string();
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// Check for an existing directory (returns error message if check fails)
|
||||
struct ExistingDirectoryValidator : public Validator {
|
||||
ExistingDirectoryValidator() {
|
||||
tname = "DIR";
|
||||
func = [](const std::string &filename) {
|
||||
struct stat buffer;
|
||||
bool exist = stat(filename.c_str(), &buffer) == 0;
|
||||
bool is_dir = (buffer.st_mode & S_IFDIR) != 0;
|
||||
if(!exist) {
|
||||
return "Directory does not exist: " + filename;
|
||||
} else if(!is_dir) {
|
||||
return "Directory is actually a file: " + filename;
|
||||
}
|
||||
return std::string();
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// Check for an existing path
|
||||
inline std::string ExistingPath(const std::string &filename) {
|
||||
struct stat buffer;
|
||||
bool const exist = stat(filename.c_str(), &buffer) == 0;
|
||||
if(!exist) {
|
||||
return "Path does not exist: " + filename;
|
||||
struct ExistingPathValidator : public Validator {
|
||||
ExistingPathValidator() {
|
||||
tname = "PATH";
|
||||
func = [](const std::string &filename) {
|
||||
struct stat buffer;
|
||||
bool const exist = stat(filename.c_str(), &buffer) == 0;
|
||||
if(!exist) {
|
||||
return "Path does not exist: " + filename;
|
||||
}
|
||||
return std::string();
|
||||
};
|
||||
}
|
||||
return std::string();
|
||||
}
|
||||
};
|
||||
|
||||
/// Check for a non-existing path
|
||||
inline std::string NonexistentPath(const std::string &filename) {
|
||||
struct stat buffer;
|
||||
bool exist = stat(filename.c_str(), &buffer) == 0;
|
||||
if(exist) {
|
||||
return "Path already exists: " + filename;
|
||||
/// Check for an non-existing path
|
||||
struct NonexistentPathValidator : public Validator {
|
||||
NonexistentPathValidator() {
|
||||
tname = "PATH";
|
||||
func = [](const std::string &filename) {
|
||||
struct stat buffer;
|
||||
bool exist = stat(filename.c_str(), &buffer) == 0;
|
||||
if(exist) {
|
||||
return "Path already exists: " + filename;
|
||||
}
|
||||
return std::string();
|
||||
};
|
||||
}
|
||||
return std::string();
|
||||
}
|
||||
};
|
||||
} // namespace detail
|
||||
|
||||
/// Produce a range validator function
|
||||
template <typename T> std::function<std::string(const std::string &)> Range(T min, T max) {
|
||||
return [min, max](std::string input) {
|
||||
T val;
|
||||
detail::lexical_cast(input, val);
|
||||
if(val < min || val > max)
|
||||
return "Value " + input + " not in range " + std::to_string(min) + " to " + std::to_string(max);
|
||||
// Static is not needed here, because global const implies static.
|
||||
|
||||
return std::string();
|
||||
};
|
||||
}
|
||||
/// Check for existing file (returns error message if check fails)
|
||||
const static detail::ExistingFileValidator ExistingFile;
|
||||
|
||||
/// Range of one value is 0 to value
|
||||
template <typename T> std::function<std::string(const std::string &)> Range(T max) {
|
||||
return Range(static_cast<T>(0), max);
|
||||
}
|
||||
/// Check for an existing directory (returns error message if check fails)
|
||||
const static detail::ExistingDirectoryValidator ExistingDirectory;
|
||||
|
||||
/// Check for an existing path
|
||||
const static detail::ExistingPathValidator ExistingPath;
|
||||
|
||||
/// Check for an non-existing path
|
||||
const static detail::NonexistentPathValidator NonexistentPath;
|
||||
|
||||
/// Produce a range (factory). Min and max are inclusive.
|
||||
struct Range : public Validator {
|
||||
template <typename T> Range(T min, T max) {
|
||||
std::stringstream out;
|
||||
out << detail::type_name<T>() << " in [" << min << " - " << max << "]";
|
||||
|
||||
tname = out.str();
|
||||
func = [min, max](std::string input) {
|
||||
T val;
|
||||
detail::lexical_cast(input, val);
|
||||
if(val < min || val > max)
|
||||
return "Value " + input + " not in range " + std::to_string(min) + " to " + std::to_string(max);
|
||||
|
||||
return std::string();
|
||||
};
|
||||
}
|
||||
|
||||
/// Range of one value is 0 to value
|
||||
template <typename T> explicit Range(T max) : Range(static_cast<T>(0), max) {}
|
||||
};
|
||||
|
||||
/// @}
|
||||
|
||||
|
@ -588,3 +588,81 @@ TEST(THelp, GroupOrder) {
|
||||
EXPECT_NE(aee_loc, std::string::npos);
|
||||
EXPECT_LT(zee_loc, aee_loc);
|
||||
}
|
||||
|
||||
TEST(THelp, ValidatorsText) {
|
||||
CLI::App app;
|
||||
|
||||
std::string filename;
|
||||
int x;
|
||||
unsigned int y;
|
||||
app.add_option("--f1", filename)->check(CLI::ExistingFile);
|
||||
app.add_option("--f3", x)->check(CLI::Range(1, 4));
|
||||
app.add_option("--f4", y)->check(CLI::Range(12));
|
||||
|
||||
std::string help = app.help();
|
||||
EXPECT_THAT(help, HasSubstr("FILE"));
|
||||
EXPECT_THAT(help, HasSubstr("INT in [1 - 4]"));
|
||||
EXPECT_THAT(help, HasSubstr("INT in [0 - 12]")); // Loses UINT
|
||||
EXPECT_THAT(help, Not(HasSubstr("TEXT")));
|
||||
}
|
||||
|
||||
TEST(THelp, ValidatorsNonPathText) {
|
||||
CLI::App app;
|
||||
|
||||
std::string filename;
|
||||
app.add_option("--f2", filename)->check(CLI::NonexistentPath);
|
||||
|
||||
std::string help = app.help();
|
||||
EXPECT_THAT(help, HasSubstr("PATH"));
|
||||
EXPECT_THAT(help, Not(HasSubstr("TEXT")));
|
||||
}
|
||||
|
||||
TEST(THelp, ValidatorsDirText) {
|
||||
CLI::App app;
|
||||
|
||||
std::string filename;
|
||||
app.add_option("--f2", filename)->check(CLI::ExistingDirectory);
|
||||
|
||||
std::string help = app.help();
|
||||
EXPECT_THAT(help, HasSubstr("DIR"));
|
||||
EXPECT_THAT(help, Not(HasSubstr("TEXT")));
|
||||
}
|
||||
|
||||
TEST(THelp, ValidatorsPathText) {
|
||||
CLI::App app;
|
||||
|
||||
std::string filename;
|
||||
app.add_option("--f2", filename)->check(CLI::ExistingPath);
|
||||
|
||||
std::string help = app.help();
|
||||
EXPECT_THAT(help, HasSubstr("PATH"));
|
||||
EXPECT_THAT(help, Not(HasSubstr("TEXT")));
|
||||
}
|
||||
|
||||
TEST(THelp, CombinedValidatorsText) {
|
||||
CLI::App app;
|
||||
|
||||
std::string filename;
|
||||
app.add_option("--f1", filename)->check(CLI::ExistingFile | CLI::ExistingDirectory);
|
||||
|
||||
// This would be nice if it put something other than string, but would it be path or file?
|
||||
// Can't programatically tell!
|
||||
// (Users can use ExistingPath, by the way)
|
||||
std::string help = app.help();
|
||||
EXPECT_THAT(help, HasSubstr("TEXT"));
|
||||
EXPECT_THAT(help, Not(HasSubstr("PATH")));
|
||||
EXPECT_THAT(help, Not(HasSubstr("FILE")));
|
||||
}
|
||||
|
||||
// Don't do this in real life, please
|
||||
TEST(THelp, CombinedValidatorsPathyText) {
|
||||
CLI::App app;
|
||||
|
||||
std::string filename;
|
||||
app.add_option("--f1", filename)->check(CLI::ExistingPath | CLI::NonexistentPath);
|
||||
|
||||
// Combining validators with the same type string is OK
|
||||
std::string help = app.help();
|
||||
EXPECT_THAT(help, Not(HasSubstr("TEXT")));
|
||||
EXPECT_THAT(help, HasSubstr("PATH"));
|
||||
}
|
||||
|
@ -155,6 +155,63 @@ TEST(Validators, PathNotExistsDir) {
|
||||
EXPECT_NE(CLI::ExistingPath(mydir), "");
|
||||
}
|
||||
|
||||
TEST(Validators, CombinedAndRange) {
|
||||
auto crange = CLI::Range(0, 12) & CLI::Range(4, 16);
|
||||
EXPECT_TRUE(crange("4").empty());
|
||||
EXPECT_TRUE(crange("12").empty());
|
||||
EXPECT_TRUE(crange("7").empty());
|
||||
|
||||
EXPECT_FALSE(crange("-2").empty());
|
||||
EXPECT_FALSE(crange("2").empty());
|
||||
EXPECT_FALSE(crange("15").empty());
|
||||
EXPECT_FALSE(crange("16").empty());
|
||||
EXPECT_FALSE(crange("18").empty());
|
||||
}
|
||||
|
||||
TEST(Validators, CombinedOrRange) {
|
||||
auto crange = CLI::Range(0, 4) | CLI::Range(8, 12);
|
||||
|
||||
EXPECT_FALSE(crange("-2").empty());
|
||||
EXPECT_TRUE(crange("2").empty());
|
||||
EXPECT_FALSE(crange("5").empty());
|
||||
EXPECT_TRUE(crange("8").empty());
|
||||
EXPECT_TRUE(crange("12").empty());
|
||||
EXPECT_FALSE(crange("16").empty());
|
||||
}
|
||||
|
||||
TEST(Validators, CombinedPaths) {
|
||||
std::string myfile{"TestFileNotUsed.txt"};
|
||||
EXPECT_FALSE(CLI::ExistingFile(myfile).empty());
|
||||
bool ok = static_cast<bool>(std::ofstream(myfile.c_str()).put('a')); // create file
|
||||
EXPECT_TRUE(ok);
|
||||
|
||||
std::string dir{"../tests"};
|
||||
std::string notpath{"nondirectory"};
|
||||
|
||||
auto path_or_dir = CLI::ExistingPath | CLI::ExistingDirectory;
|
||||
EXPECT_TRUE(path_or_dir(dir).empty());
|
||||
EXPECT_TRUE(path_or_dir(myfile).empty());
|
||||
EXPECT_FALSE(path_or_dir(notpath).empty());
|
||||
|
||||
auto file_or_dir = CLI::ExistingFile | CLI::ExistingDirectory;
|
||||
EXPECT_TRUE(file_or_dir(dir).empty());
|
||||
EXPECT_TRUE(file_or_dir(myfile).empty());
|
||||
EXPECT_FALSE(file_or_dir(notpath).empty());
|
||||
|
||||
auto path_and_dir = CLI::ExistingPath & CLI::ExistingDirectory;
|
||||
EXPECT_TRUE(path_and_dir(dir).empty());
|
||||
EXPECT_FALSE(path_and_dir(myfile).empty());
|
||||
EXPECT_FALSE(path_and_dir(notpath).empty());
|
||||
|
||||
auto path_and_file = CLI::ExistingFile & CLI::ExistingDirectory;
|
||||
EXPECT_FALSE(path_and_file(dir).empty());
|
||||
EXPECT_FALSE(path_and_file(myfile).empty());
|
||||
EXPECT_FALSE(path_and_file(notpath).empty());
|
||||
|
||||
std::remove(myfile.c_str());
|
||||
EXPECT_FALSE(CLI::ExistingFile(myfile).empty());
|
||||
}
|
||||
|
||||
// Yes, this is testing an app_helper :)
|
||||
TEST(AppHelper, TempfileCreated) {
|
||||
std::string name = "TestFileNotUsed.txt";
|
||||
|
Loading…
x
Reference in New Issue
Block a user