From f4bf6d7226ba72a9eb35c0541b608f9c47fb1f01 Mon Sep 17 00:00:00 2001 From: Henry Fredrick Schreiner Date: Sun, 12 Feb 2017 14:04:36 -0500 Subject: [PATCH] Adding requires, excludes, and getenv (untested) --- README.md | 5 ++++- include/CLI/App.hpp | 22 ++++++++++++++++++++ include/CLI/Error.hpp | 10 +++++++++ include/CLI/Option.hpp | 46 ++++++++++++++++++++++++++++++++++++++++++ tests/CLITest.cpp | 2 +- 5 files changed, 83 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b9d09e39..c7260750 100644 --- a/README.md +++ b/README.md @@ -103,8 +103,11 @@ Adding a configuration option is special. If it is present, it will be read alon The add commands return a pointer to an internally stored `Option`. If you set the final argument to true, the default value is captured and printed on the command line with the help flag. This option can be used direcly to check for the count (`->count()`) after parsing to avoid a string based lookup. Before parsing, you can set the following options: -* `->required()`: The program will quit if this option is not present +* `->required()`: The program will quit if this option is not present. This is `manditory` in Plumbum, but required options seems to be a more standard term. * `->expected(N)`: Take `N` values instead of as many as possible, only for vector args +* `->requires(opt)`: This option requires another option to also be present, opt is an `Option` pointer +* `->excludes(opt)`: This option cannot be given with `opt` present, opt is an `Option` pointer +* `->envname(name)`: Gets the value from the environment if present and not passed on the command line * `->group(name)`: The help group to put the option in. No effect for positional options. Defaults to `"Options"`. * `->check(CLI::ExistingFile)`: Requires that the file exists if given * `->check(CLI::ExistingDirectory)`: Requires that the directory exists diff --git a/include/CLI/App.hpp b/include/CLI/App.hpp index 6678eaa0..bc4c0f37 100644 --- a/include/CLI/App.hpp +++ b/include/CLI/App.hpp @@ -55,6 +55,7 @@ protected: std::string ini_file; bool ini_required {false}; Option* ini_setting {nullptr}; + public: @@ -379,12 +380,23 @@ public: opt->add_result(0, positionals.front()); positionals.pop_front(); } + + if (first_parse && opt->count() == 0 && opt->_envname != "") { + // Will not interact very well with ini files + char *ename = std::getenv(opt->_envname.c_str()); + if(ename != nullptr) { + opt->get_new(); + opt->add_result(0, std::string(ename)); + } + } + if (opt->count() > 0) { if(!opt->run_callback()) throw ConversionError(opt->get_name() + "=" + detail::join(opt->flatten_results())); } } + // Process an INI file if (first_parse && ini_setting != nullptr && ini_file != "") { try { std::vector values = detail::parse_ini(ini_file); @@ -398,9 +410,19 @@ public: } } + // Verify required options for(const Option_p& opt : options) { + // Required if (opt->get_required() && opt->count() < opt->get_expected()) throw RequiredError(opt->get_name()); + // Requires + for (const Option* opt_req : opt->_requires) + if (opt_req->count() == 0) + throw RequiresError(opt->get_name(), opt_req->get_name()); + // Excludes + for (const Option* opt_ex : opt->_excludes) + if (opt_ex->count() != 0) + throw ExcludesError(opt->get_name(), opt_ex->get_name()); } if(positionals.size()>0) diff --git a/include/CLI/Error.hpp b/include/CLI/Error.hpp index eedee7d0..230f823f 100644 --- a/include/CLI/Error.hpp +++ b/include/CLI/Error.hpp @@ -75,6 +75,16 @@ struct RequiredError : public ParseError { RequiredError(std::string name) : ParseError("RequiredError", name, 5) {} }; +/// Thrown when a requires option is missing +struct RequiresError : public ParseError { + RequiresError(std::string name, std::string subname) : ParseError("RequiresError", name + " requires " + subname, 13) {} +}; + +/// Thrown when a exludes option is present +struct ExcludesError : public ParseError { + ExcludesError(std::string name, std::string subname) : ParseError("ExcludesError", name + " excludes " + subname, 14) {} +}; + /// Thrown when too many positionals are found struct PositionalError : public ParseError { PositionalError(std::string name) : ParseError("PositionalError", name, 6) {} diff --git a/include/CLI/Option.hpp b/include/CLI/Option.hpp index b5c94b98..25ee6129 100644 --- a/include/CLI/Option.hpp +++ b/include/CLI/Option.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include "CLI/Error.hpp" #include "CLI/StringTools.hpp" @@ -42,6 +43,10 @@ protected: bool allow_vector {false}; std::vector> _validators; + std::set _requires; + std::set _excludes; + std::string _envname; + // Results results_t results; @@ -69,6 +74,7 @@ public: return this; } + /// True if this is a required option bool get_required() const { return _required; } @@ -115,11 +121,13 @@ public: return this; } + /// Changes the group membership Option* group(std::string name) { _group = name; return this; } + /// Get the group of this option const std::string& get_group() const { return _group; } @@ -129,6 +137,42 @@ public: return description; } + /// Sets required options + Option* requires(Option* opt) { + auto tup = _requires.insert(opt); + if(!tup.second) + throw OptionAlreadyAdded(get_name() + " requires " + opt->get_name()); + return this; + } + + /// Any number supported + template + Option* requires(Option* opt, Option* opt1, ARG... args) { + requires(opt); + return requires(opt1, args...); + } + + /// Sets excluded options + Option* excludes(Option* opt) { + auto tup = _excludes.insert(opt); + if(!tup.second) + throw OptionAlreadyAdded(get_name() + " excludes " + opt->get_name()); + return this; + } + + /// Any number supported + template + Option* excludes(Option* opt, Option* opt1, ARG... args) { + excludes(opt); + return excludes(opt1, args...); + } + + /// Sets environment variable to read if no option given + Option* envname(std::string name) { + _envname = name; + return this; + } + /// The name and any extras needed for positionals std::string help_positional() const { std::string out = pname; @@ -144,6 +188,8 @@ public: std::string get_pname() const { return pname; } + + /// Process the callback bool run_callback() const { if(_validators.size()>0) { diff --git a/tests/CLITest.cpp b/tests/CLITest.cpp index a84588d4..bbf970be 100644 --- a/tests/CLITest.cpp +++ b/tests/CLITest.cpp @@ -537,4 +537,4 @@ TEST_F(SubcommandProgram, SpareSub) { } -// TODO: Check help output and formatting +// TODO: add tests for requires, excludes, envname