From d46c2c5727c645b132a1dd109481abbe54280509 Mon Sep 17 00:00:00 2001 From: Henry Fredrick Schreiner Date: Tue, 8 May 2018 10:32:04 +0200 Subject: [PATCH] Adding Config class --- CHANGELOG.md | 23 ++++- README.md | 4 +- examples/formatter.cpp | 2 +- include/CLI/App.hpp | 172 +++++++++++++---------------------- include/CLI/CLI.hpp | 4 +- include/CLI/Config.hpp | 68 ++++++++++++++ include/CLI/ConfigFwd.hpp | 156 +++++++++++++++++++++++++++++++ include/CLI/Error.hpp | 14 +-- include/CLI/FormatterFwd.hpp | 3 +- include/CLI/Ini.hpp | 115 ----------------------- include/CLI/Option.hpp | 1 + tests/FormatterTest.cpp | 2 +- tests/HelpTest.cpp | 2 +- tests/IniTest.cpp | 76 ++++++++-------- 14 files changed, 365 insertions(+), 277 deletions(-) create mode 100644 include/CLI/Config.hpp create mode 100644 include/CLI/ConfigFwd.hpp delete mode 100644 include/CLI/Ini.hpp diff --git a/CHANGELOG.md b/CHANGELOG.md index f70072685..0ceb394a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,11 @@ ## Version 1.6: Formatting help -Added a new formatting system [#109]. You can now set the formatter on Apps. This has also simplified the internals of Apps and Options a bit by separating all formatting code. +Added a new formatting system [#109]. You can now set the formatter on Apps. This has also simplified the internals of Apps and Options a bit by separating most formatting code. * Added `CLI::Formatter` and `formatter` slot for apps, inherited. +* `FormatterBase` is the minimum required +* `FormatterLambda` provides for the easy addition of an arbitrary function * Added `help_all` support (not added by default) -* Added filter argument to `get_subcommands`, `get_options`; use empty filter `{}` to avoid filtering -* Added `get_groups()` to get groups -* Added getters for the missing parts of options (help no longer uses any private parts) Changes to the help system (most normal users will not notice this): @@ -17,6 +16,17 @@ Changes to the help system (most normal users will not notice this): * Protected function `_has_help_positional` removed * `format_help` can now be chained + +New for Config file reading and writing [#121]: + +* Overridable, bidirectional Config +* ConfigINI provided and used by default +* Renamed ini to config in many places +* Has `config_formatter()` and `get_config_formatter()` +* Dropped prefix argument from `config_to_str` +* Added `ConfigItem` + + Validators are now much more powerful [#118], all built in validators upgraded to the new form: * A subclass of `CLI::Validator` is now also accepted. @@ -26,6 +36,9 @@ Validators are now much more powerful [#118], all built in validators upgraded t Other changes: +* Added filter argument to `get_subcommands`, `get_options`; use empty filter `{}` to avoid filtering +* Added `get_groups()` to get groups +* Added getters for the missing parts of options (help no longer uses any private parts) * Better support for manual options with `get_option`, `set_results`, and `empty` [#119] * `lname` and `sname` have getters, added `const get_parent` [#120] * Using `add_set` will now capture L-values for sets, allowing further modification [#113] @@ -35,6 +48,7 @@ Other changes: * Removed `requires` in favor of `needs` (deprecated in last version) [#112] * Better CMake policy handling [#110] * Includes are properly sorted [#120] +* Help flags now use new `short_circuit` property to simplify parsing [#121] [#109]: https://github.com/CLIUtils/CLI11/pull/109 [#110]: https://github.com/CLIUtils/CLI11/pull/110 @@ -45,6 +59,7 @@ Other changes: [#118]: https://github.com/CLIUtils/CLI11/pull/118 [#119]: https://github.com/CLIUtils/CLI11/pull/119 [#120]: https://github.com/CLIUtils/CLI11/pull/120 +[#121]: https://github.com/CLIUtils/CLI11/pull/121 ### Version 1.5.3: Compiler compatibility This version fixes older AppleClang compilers by removing the optimization for casting. The minimum version of Boost Optional supported has been clarified to be 1.58. CUDA 7.0 NVCC is now supported. diff --git a/README.md b/README.md index e397bb8e3..315ecc8d4 100644 --- a/README.md +++ b/README.md @@ -192,7 +192,7 @@ The add commands return a pointer to an internally stored `Option`. If you set t * `->check(CLI::NonexistentPath)`: Requires that the path does not exist. * `->check(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. * `->transform(std::string(std::string))`: Converts the input string into the output string, in-place in the parsed options. -* `->configurable(false)`: Disable this option from being in an ini configuration file. +* `->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). @@ -272,7 +272,7 @@ app.set_config(option_name="", required=false) ``` -If this is called with no arguments, it will remove the configuration file option (like `set_help_flag`). Setting a configuration option is special. If it is present, it will be read along with the normal command line arguments. The file will be read if it exists, and does not throw an error unless `required` is `true`. Configuration files are in `ini` format. An example of a file: +If this is called with no arguments, it will remove the configuration file option (like `set_help_flag`). Setting a configuration option is special. If it is present, it will be read along with the normal command line arguments. The file will be read if it exists, and does not throw an error unless `required` is `true`. Configuration files are in `ini` format by default (other formats can be added by an adept user). An example of a file: ```ini ; Commments are supported, using a ; diff --git a/examples/formatter.cpp b/examples/formatter.cpp index 4fa5fe1c1..4d67ec612 100644 --- a/examples/formatter.cpp +++ b/examples/formatter.cpp @@ -2,7 +2,7 @@ class MyFormatter : public CLI::Formatter { public: - using Formatter::Formatter; + MyFormatter() : Formatter() {} std::string make_option_opts(const CLI::Option *) const override { return " OPTION"; } }; diff --git a/include/CLI/App.hpp b/include/CLI/App.hpp index 993f27819..c26e23965 100644 --- a/include/CLI/App.hpp +++ b/include/CLI/App.hpp @@ -17,9 +17,9 @@ #include // CLI Library includes +#include "CLI/ConfigFwd.hpp" #include "CLI/Error.hpp" #include "CLI/FormatterFwd.hpp" -#include "CLI/Ini.hpp" #include "CLI/Macros.hpp" #include "CLI/Option.hpp" #include "CLI/Split.hpp" @@ -75,7 +75,7 @@ class App { bool allow_extras_{false}; /// If true, allow extra arguments in the ini file (ie, don't throw an error). INHERITABLE - bool allow_ini_extras_{false}; + bool allow_config_extras_{false}; /// If true, return immediately on an unrecognised option (implies allow_extras) INHERITABLE bool prefix_command_{false}; @@ -170,6 +170,9 @@ class App { /// Pointer to the config option Option *config_ptr_{nullptr}; + /// This is the formatter for help printing. Default provided. INHERITABLE (same pointer) + std::shared_ptr config_formatter_{new ConfigINI()}; + ///@} /// Special private constructor for subcommand @@ -189,13 +192,14 @@ class App { // INHERITABLE failure_message_ = parent_->failure_message_; allow_extras_ = parent_->allow_extras_; - allow_ini_extras_ = parent_->allow_ini_extras_; + allow_config_extras_ = parent_->allow_config_extras_; prefix_command_ = parent_->prefix_command_; ignore_case_ = parent_->ignore_case_; fallthrough_ = parent_->fallthrough_; group_ = parent_->group_; footer_ = parent_->footer_; formatter_ = parent_->formatter_; + config_formatter_ = parent_->config_formatter_; require_subcommand_max_ = parent_->require_subcommand_max_; } } @@ -237,9 +241,9 @@ class App { /// Remove the error when extras are left over on the command line. /// Will also call App::allow_extras(). - App *allow_ini_extras(bool allow = true) { + App *allow_config_extras(bool allow = true) { allow_extras(allow); - allow_ini_extras_ = allow; + allow_config_extras_ = allow; return this; } @@ -268,11 +272,17 @@ class App { } /// Set the help formatter - App *formatter(std::function fmt) { + App *formatter_fn(std::function fmt) { formatter_ = std::make_shared(fmt); return this; } + /// Set the config formatter + App *config_formatter(std::shared_ptr fmt) { + config_formatter_ = fmt; + return this; + } + /// Check to see if this subcommand was parsed, true only if received on command line. bool parsed() const { return parsed_; } @@ -1015,53 +1025,8 @@ class App { /// Produce a string that could be read in as a config of the current values of the App. Set default_also to include /// default arguments. Prefix will add a string to the beginning of each option. - std::string - config_to_str(bool default_also = false, std::string prefix = "", bool write_description = false) const { - std::stringstream out; - for(const Option_p &opt : options_) { - - // Only process option with a long-name and configurable - if(!opt->get_lnames().empty() && opt->get_configurable()) { - std::string name = prefix + opt->get_lnames()[0]; - std::string value; - - // Non-flags - if(opt->get_type_size() != 0) { - - // If the option was found on command line - if(opt->count() > 0) - value = detail::inijoin(opt->results()); - - // If the option has a default and is requested by optional argument - else if(default_also && !opt->get_defaultval().empty()) - value = opt->get_defaultval(); - // Flag, one passed - } else if(opt->count() == 1) { - value = "true"; - - // Flag, multiple passed - } else if(opt->count() > 1) { - value = std::to_string(opt->count()); - - // Flag, not present - } else if(opt->count() == 0 && default_also) { - value = "false"; - } - - if(!value.empty()) { - if(write_description && opt->has_description()) { - if(static_cast(out.tellp()) != 0) { - out << std::endl; - } - out << "; " << detail::fix_newlines("; ", opt->get_description()) << std::endl; - } - out << name << "=" << value << std::endl; - } - } - } - for(const App_p &subcom : subcommands_) - out << subcom->config_to_str(default_also, prefix + subcom->name_ + "."); - return out.str(); + std::string config_to_str(bool default_also = false, bool write_description = false) const { + return config_formatter_->to_config(this, default_also, write_description, ""); } /// Makes a help message, using the currently configured formatter @@ -1087,6 +1052,9 @@ class App { /// Access the formatter std::shared_ptr get_formatter() const { return formatter_; } + /// Access the config formatter + std::shared_ptr get_config_formatter() const { return config_formatter_; } + /// Get the app or subcommand description std::string get_description() const { return description_; } @@ -1117,6 +1085,16 @@ class App { throw OptionNotFound(name); } + /// Get an option by name (non-const version) + Option *get_option(std::string name) { + for(Option_p &opt : options_) { + if(opt->check_name(name)) { + return opt.get(); + } + } + throw OptionNotFound(name); + } + /// Check the status of ignore_case bool get_ignore_case() const { return ignore_case_; } @@ -1142,7 +1120,7 @@ class App { bool get_allow_extras() const { return allow_extras_; } /// Get the status of allow extras - bool get_allow_ini_extras() const { return allow_ini_extras_; } + bool get_allow_config_extras() const { return allow_config_extras_; } /// Get a pointer to the help flag. Option *get_help_ptr() { return help_ptr_; } @@ -1156,15 +1134,15 @@ class App { /// Get a pointer to the config option. Option *get_config_ptr() { return config_ptr_; } + /// Get a pointer to the config option. (const) + const Option *get_config_ptr() const { return config_ptr_; } + /// Get the parent of this subcommand (or nullptr if master app) App *get_parent() { return parent_; } /// Get the parent of this subcommand (or nullptr if master app) (const version) const App *get_parent() const { return parent_; } - /// Get a pointer to the config option. (const) - const Option *get_config_ptr() const { return config_ptr_; } - /// Get the name of the current app std::string get_name() const { return name_; } @@ -1304,12 +1282,8 @@ class App { } if(!config_name_.empty()) { try { - std::vector values = detail::parse_ini(config_name_); - while(!values.empty()) { - if(!_parse_ini(values)) { - throw INIError::Extras(values.back().fullname); - } - } + std::vector values = config_formatter_->from_file(config_name_); + _parse_config(values); } catch(const FileError &) { if(config_required_) throw; @@ -1387,70 +1361,54 @@ class App { } } - /// Parse one ini param, return false if not found in any subcommand, remove if it is + /// Parse one config param, return false if not found in any subcommand, remove if it is /// /// If this has more than one dot.separated.name, go into the subcommand matching it /// Returns true if it managed to find the option, if false you'll need to remove the arg manually. - bool _parse_ini(std::vector &args) { - detail::ini_ret_t ¤t = args.back(); - std::string parent = current.parent(); // respects current.level - std::string name = current.name(); - - // If a parent is listed, go to a subcommand - if(!parent.empty()) { - current.level++; - for(const App_p &com : subcommands_) - if(com->check_name(parent)) - return com->_parse_ini(args); - return false; + void _parse_config(std::vector &args) { + for(ConfigItem item : args) { + if(!_parse_single_config(item) && !allow_config_extras_) + throw ConfigError::Extras(item.fullname()); } + } - auto op_ptr = std::find_if( - std::begin(options_), std::end(options_), [name](const Option_p &v) { return v->check_lname(name); }); + /// Fill in a single config option + bool _parse_single_config(const ConfigItem &item, size_t level = 0) { + if(level < item.parents.size()) { + App *subcom; + try { + std::cout << item.parents.at(level) << std::endl; + subcom = get_subcommand(item.parents.at(level)); + } catch(const OptionNotFound &) { + return false; + } + return subcom->_parse_single_config(item, level + 1); + } - if(op_ptr == std::end(options_)) { - if(allow_ini_extras_) { + Option *op; + try { + op = get_option("--" + item.name); + } catch(const OptionNotFound &) { + // If the option was not present + if(get_allow_config_extras()) // Should we worry about classifying the extras properly? - missing_.emplace_back(detail::Classifer::NONE, current.fullname); - args.pop_back(); - return true; - } + missing_.emplace_back(detail::Classifer::NONE, item.fullname()); return false; } - // Let's not go crazy with pointer syntax - Option_p &op = *op_ptr; - if(!op->get_configurable()) - throw INIError::NotConfigurable(current.fullname); + throw ConfigError::NotConfigurable(item.fullname()); if(op->empty()) { // Flag parsing if(op->get_type_size() == 0) { - if(current.inputs.size() == 1) { - std::string val = current.inputs.at(0); - val = detail::to_lower(val); - if(val == "true" || val == "on" || val == "yes") - op->set_results({""}); - else if(val == "false" || val == "off" || val == "no") - ; - else - try { - size_t ui = std::stoul(val); - for(size_t i = 0; i < ui; i++) - op->add_result(""); - } catch(const std::invalid_argument &) { - throw ConversionError::TrueFalse(current.fullname); - } - } else - throw ConversionError::TooManyInputsFlag(current.fullname); + op->set_results(config_formatter_->to_flag(item)); } else { - op->set_results(current.inputs); + op->set_results(item.inputs); op->run_callback(); } } - args.pop_back(); return true; } diff --git a/include/CLI/CLI.hpp b/include/CLI/CLI.hpp index a06d30253..2feb19247 100644 --- a/include/CLI/CLI.hpp +++ b/include/CLI/CLI.hpp @@ -20,7 +20,7 @@ #include "CLI/Split.hpp" -#include "CLI/Ini.hpp" +#include "CLI/ConfigFwd.hpp" #include "CLI/Validators.hpp" @@ -30,4 +30,6 @@ #include "CLI/App.hpp" +#include "CLI/Config.hpp" + #include "CLI/Formatter.hpp" diff --git a/include/CLI/Config.hpp b/include/CLI/Config.hpp new file mode 100644 index 000000000..33f861973 --- /dev/null +++ b/include/CLI/Config.hpp @@ -0,0 +1,68 @@ +#pragma once + +// Distributed under the 3-Clause BSD License. See accompanying +// file LICENSE or https://github.com/CLIUtils/CLI11 for details. + +#include +#include +#include +#include + +#include "CLI/App.hpp" +#include "CLI/ConfigFwd.hpp" +#include "CLI/StringTools.hpp" + +namespace CLI { + +inline std::string +ConfigINI::to_config(const App *app, bool default_also, bool write_description, std::string prefix) const { + std::stringstream out; + for(const Option *opt : app->get_options({})) { + + // Only process option with a long-name and configurable + if(!opt->get_lnames().empty() && opt->get_configurable()) { + std::string name = prefix + opt->get_lnames()[0]; + std::string value; + + // Non-flags + if(opt->get_type_size() != 0) { + + // If the option was found on command line + if(opt->count() > 0) + value = detail::inijoin(opt->results()); + + // If the option has a default and is requested by optional argument + else if(default_also && !opt->get_defaultval().empty()) + value = opt->get_defaultval(); + // Flag, one passed + } else if(opt->count() == 1) { + value = "true"; + + // Flag, multiple passed + } else if(opt->count() > 1) { + value = std::to_string(opt->count()); + + // Flag, not present + } else if(opt->count() == 0 && default_also) { + value = "false"; + } + + if(!value.empty()) { + if(write_description && opt->has_description()) { + if(static_cast(out.tellp()) != 0) { + out << std::endl; + } + out << "; " << detail::fix_newlines("; ", opt->get_description()) << std::endl; + } + out << name << "=" << value << std::endl; + } + } + } + + for(const App *subcom : app->get_subcommands({})) + out << to_config(subcom, default_also, write_description, prefix + subcom->get_name() + "."); + + return out.str(); +} + +} // namespace CLI diff --git a/include/CLI/ConfigFwd.hpp b/include/CLI/ConfigFwd.hpp new file mode 100644 index 000000000..41114a67c --- /dev/null +++ b/include/CLI/ConfigFwd.hpp @@ -0,0 +1,156 @@ +#pragma once + +// Distributed under the 3-Clause BSD License. See accompanying +// file LICENSE or https://github.com/CLIUtils/CLI11 for details. + +#include +#include +#include +#include + +#include "CLI/StringTools.hpp" + +namespace CLI { + +class App; + +namespace detail { + +inline std::string inijoin(std::vector args) { + std::ostringstream s; + size_t start = 0; + for(const auto &arg : args) { + if(start++ > 0) + s << " "; + + auto it = std::find_if(arg.begin(), arg.end(), [](char ch) { return std::isspace(ch, std::locale()); }); + if(it == arg.end()) + s << arg; + else if(arg.find(R"(")") == std::string::npos) + s << R"(")" << arg << R"(")"; + else + s << R"(')" << arg << R"(')"; + } + + return s.str(); +} + +} // namespace detail + +struct ConfigItem { + /// This is the list of parents + std::vector parents; + + /// This is the name + std::string name; + + /// Listing of inputs + std::vector inputs; + + /// The list of parents and name joined by "." + std::string fullname() const { + std::vector tmp = parents; + tmp.emplace_back(name); + return detail::join(tmp, "."); + } +}; + +/// This class provides a converter for configuration files. +class Config { + protected: + std::vector items; + + public: + /// Convert an app into a configuration + virtual std::string to_config(const App *, bool, bool, std::string) const = 0; + + /// Convert a configuration into an app + virtual std::vector from_config(std::istream &) const = 0; + + /// Convert a flag to a bool + virtual std::vector to_flag(const ConfigItem &) const = 0; + + /// Parse a config file, throw an error (ParseError:ConfigParseError or FileError) on failure + std::vector from_file(const std::string &name) { + std::ifstream input{name}; + if(!input.good()) + throw FileError::Missing(name); + + return from_config(input); + } +}; + +/// This converter works with INI files +class ConfigINI : public Config { + public: + std::string to_config(const App *, bool default_also, bool write_description, std::string prefix) const override; + + std::vector to_flag(const ConfigItem &item) const override { + if(item.inputs.size() == 1) { + std::string val = item.inputs.at(0); + val = detail::to_lower(val); + + if(val == "true" || val == "on" || val == "yes") { + return std::vector(1); + } else if(val == "false" || val == "off" || val == "no") { + return std::vector(); + } else { + try { + size_t ui = std::stoul(val); + return std::vector(ui); + } catch(const std::invalid_argument &) { + throw ConversionError::TrueFalse(item.fullname()); + } + } + } else { + throw ConversionError::TooManyInputsFlag(item.fullname()); + } + } + + std::vector from_config(std::istream &input) const override { + std::string line; + std::string section = "default"; + + std::vector output; + + while(getline(input, line)) { + std::vector items; + + detail::trim(line); + size_t len = line.length(); + if(len > 1 && line[0] == '[' && line[len - 1] == ']') { + section = line.substr(1, len - 2); + } else if(len > 0 && line[0] != ';') { + output.emplace_back(); + ConfigItem &out = output.back(); + + // Find = in string, split and recombine + auto pos = line.find('='); + if(pos != std::string::npos) { + out.name = detail::trim_copy(line.substr(0, pos)); + std::string item = detail::trim_copy(line.substr(pos + 1)); + items = detail::split_up(item); + } else { + out.name = detail::trim_copy(line); + items = {"ON"}; + } + + if(detail::to_lower(section) != "default") { + out.parents = {section}; + } + + if(out.name.find('.') != std::string::npos) { + std::vector plist = detail::split(out.name, '.'); + out.name = plist.back(); + plist.pop_back(); + out.parents.insert(out.parents.end(), plist.begin(), plist.end()); + } + + out.inputs.insert(std::end(out.inputs), std::begin(items), std::end(items)); + } + } + return output; + } +}; + +} // namespace CLI diff --git a/include/CLI/Error.hpp b/include/CLI/Error.hpp index d64fbe7f1..2a658d2df 100644 --- a/include/CLI/Error.hpp +++ b/include/CLI/Error.hpp @@ -42,7 +42,7 @@ enum class ExitCodes { RequiresError, ExcludesError, ExtrasError, - INIError, + ConfigError, InvalidError, HorribleError, OptionNotFound, @@ -257,12 +257,12 @@ class ExtrasError : public ParseError { }; /// Thrown when extra values are found in an INI file -class INIError : public ParseError { - CLI11_ERROR_DEF(ParseError, INIError) - CLI11_ERROR_SIMPLE(INIError) - static INIError Extras(std::string item) { return INIError("INI was not able to parse " + item); } - static INIError NotConfigurable(std::string item) { - return INIError(item + ": This option is not allowed in a configuration file"); +class ConfigError : public ParseError { + CLI11_ERROR_DEF(ParseError, ConfigError) + CLI11_ERROR_SIMPLE(ConfigError) + static ConfigError Extras(std::string item) { return ConfigError("INI was not able to parse " + item); } + static ConfigError NotConfigurable(std::string item) { + return ConfigError(item + ": This option is not allowed in a configuration file"); } }; diff --git a/include/CLI/FormatterFwd.hpp b/include/CLI/FormatterFwd.hpp index c2a279127..5e87dec4a 100644 --- a/include/CLI/FormatterFwd.hpp +++ b/include/CLI/FormatterFwd.hpp @@ -5,6 +5,7 @@ #include #include +#include #include "CLI/StringTools.hpp" @@ -88,7 +89,7 @@ class FormatterLambda final : public FormatterBase { funct_t lambda_; public: - explicit FormatterLambda(funct_t funct) : lambda_(funct) {} + explicit FormatterLambda(funct_t funct) : lambda_(std::move(funct)) {} /// This will simply call the lambda function std::string make_help(const App *app, std::string name, AppFormatMode mode) const override { diff --git a/include/CLI/Ini.hpp b/include/CLI/Ini.hpp deleted file mode 100644 index f9999fff3..000000000 --- a/include/CLI/Ini.hpp +++ /dev/null @@ -1,115 +0,0 @@ -#pragma once - -// Distributed under the 3-Clause BSD License. See accompanying -// file LICENSE or https://github.com/CLIUtils/CLI11 for details. - -#include -#include -#include -#include - -#include "CLI/StringTools.hpp" - -namespace CLI { -namespace detail { - -inline std::string inijoin(std::vector args) { - std::ostringstream s; - size_t start = 0; - for(const auto &arg : args) { - if(start++ > 0) - s << " "; - - auto it = std::find_if(arg.begin(), arg.end(), [](char ch) { return std::isspace(ch, std::locale()); }); - if(it == arg.end()) - s << arg; - else if(arg.find(R"(")") == std::string::npos) - s << R"(")" << arg << R"(")"; - else - s << R"(')" << arg << R"(')"; - } - - return s.str(); -} - -struct ini_ret_t { - /// This is the full name with dots - std::string fullname; - - /// Listing of inputs - std::vector inputs; - - /// Current parent level - size_t level = 0; - - /// Return parent or empty string, based on level - /// - /// Level 0, a.b.c would return a - /// Level 1, a.b.c could return b - std::string parent() const { - std::vector plist = detail::split(fullname, '.'); - if(plist.size() > (level + 1)) - return plist[level]; - else - return ""; - } - - /// Return name - std::string name() const { - std::vector plist = detail::split(fullname, '.'); - return plist.at(plist.size() - 1); - } -}; - -/// Internal parsing function -inline std::vector parse_ini(std::istream &input) { - std::string name, line; - std::string section = "default"; - - std::vector output; - - while(getline(input, line)) { - std::vector items; - - detail::trim(line); - size_t len = line.length(); - if(len > 1 && line[0] == '[' && line[len - 1] == ']') { - section = line.substr(1, len - 2); - } else if(len > 0 && line[0] != ';') { - output.emplace_back(); - ini_ret_t &out = output.back(); - - // Find = in string, split and recombine - auto pos = line.find("="); - if(pos != std::string::npos) { - name = detail::trim_copy(line.substr(0, pos)); - std::string item = detail::trim_copy(line.substr(pos + 1)); - items = detail::split_up(item); - } else { - name = detail::trim_copy(line); - items = {"ON"}; - } - - if(detail::to_lower(section) == "default") - out.fullname = name; - else - out.fullname = section + "." + name; - - out.inputs.insert(std::end(out.inputs), std::begin(items), std::end(items)); - } - } - return output; -} - -/// Parse an INI file, throw an error (ParseError:INIParseError or FileError) on failure -inline std::vector parse_ini(const std::string &name) { - - std::ifstream input{name}; - if(!input.good()) - throw FileError::Missing(name); - - return parse_ini(input); -} - -} // namespace detail -} // namespace CLI diff --git a/include/CLI/Option.hpp b/include/CLI/Option.hpp index 86047c75f..93991ba82 100644 --- a/include/CLI/Option.hpp +++ b/include/CLI/Option.hpp @@ -16,6 +16,7 @@ #include "CLI/Macros.hpp" #include "CLI/Split.hpp" #include "CLI/StringTools.hpp" +#include "CLI/Validators.hpp" namespace CLI { diff --git a/tests/FormatterTest.cpp b/tests/FormatterTest.cpp index dafbaff59..74af18b9e 100644 --- a/tests/FormatterTest.cpp +++ b/tests/FormatterTest.cpp @@ -33,7 +33,7 @@ TEST(Formatter, Nothing) { TEST(Formatter, NothingLambda) { CLI::App app{"My prog"}; - app.formatter( + app.formatter_fn( [](const CLI::App *, std::string, CLI::AppFormatMode) { return std::string("This is really simple"); }); std::string help = app.help(); diff --git a/tests/HelpTest.cpp b/tests/HelpTest.cpp index 8e496e082..17bee24c7 100644 --- a/tests/HelpTest.cpp +++ b/tests/HelpTest.cpp @@ -519,7 +519,7 @@ TEST_F(CapturedHelp, CallForAllHelpOutput) { " --three \n"); } TEST_F(CapturedHelp, NewFormattedHelp) { - app.formatter([](const CLI::App *, std::string, CLI::AppFormatMode) { return "New Help"; }); + app.formatter_fn([](const CLI::App *, std::string, CLI::AppFormatMode) { return "New Help"; }); EXPECT_EQ(run(CLI::CallForHelp()), 0); EXPECT_EQ(out.str(), "New Help"); EXPECT_EQ(err.str(), ""); diff --git a/tests/IniTest.cpp b/tests/IniTest.cpp index 918b80e21..92c4e35e5 100644 --- a/tests/IniTest.cpp +++ b/tests/IniTest.cpp @@ -15,13 +15,13 @@ TEST(StringBased, First) { ofile.seekg(0, std::ios::beg); - std::vector output = CLI::detail::parse_ini(ofile); + std::vector output = CLI::ConfigINI().from_config(ofile); EXPECT_EQ((size_t)2, output.size()); - EXPECT_EQ("one", output.at(0).name()); + EXPECT_EQ("one", output.at(0).name); EXPECT_EQ((size_t)1, output.at(0).inputs.size()); EXPECT_EQ("three", output.at(0).inputs.at(0)); - EXPECT_EQ("two", output.at(1).name()); + EXPECT_EQ("two", output.at(1).name); EXPECT_EQ((size_t)1, output.at(1).inputs.size()); EXPECT_EQ("four", output.at(1).inputs.at(0)); } @@ -36,13 +36,13 @@ TEST(StringBased, FirstWithComments) { ofile.seekg(0, std::ios::beg); - auto output = CLI::detail::parse_ini(ofile); + std::vector output = CLI::ConfigINI().from_config(ofile); EXPECT_EQ((size_t)2, output.size()); - EXPECT_EQ("one", output.at(0).name()); + EXPECT_EQ("one", output.at(0).name); EXPECT_EQ((size_t)1, output.at(0).inputs.size()); EXPECT_EQ("three", output.at(0).inputs.at(0)); - EXPECT_EQ("two", output.at(1).name()); + EXPECT_EQ("two", output.at(1).name); EXPECT_EQ((size_t)1, output.at(1).inputs.size()); EXPECT_EQ("four", output.at(1).inputs.at(0)); } @@ -56,16 +56,16 @@ TEST(StringBased, Quotes) { ofile.seekg(0, std::ios::beg); - auto output = CLI::detail::parse_ini(ofile); + std::vector output = CLI::ConfigINI().from_config(ofile); EXPECT_EQ((size_t)3, output.size()); - EXPECT_EQ("one", output.at(0).name()); + EXPECT_EQ("one", output.at(0).name); EXPECT_EQ((size_t)1, output.at(0).inputs.size()); EXPECT_EQ("three", output.at(0).inputs.at(0)); - EXPECT_EQ("two", output.at(1).name()); + EXPECT_EQ("two", output.at(1).name); EXPECT_EQ((size_t)1, output.at(1).inputs.size()); EXPECT_EQ("four", output.at(1).inputs.at(0)); - EXPECT_EQ("five", output.at(2).name()); + EXPECT_EQ("five", output.at(2).name); EXPECT_EQ((size_t)1, output.at(2).inputs.size()); EXPECT_EQ("six and seven", output.at(2).inputs.at(0)); } @@ -79,16 +79,16 @@ TEST(StringBased, Vector) { ofile.seekg(0, std::ios::beg); - auto output = CLI::detail::parse_ini(ofile); + std::vector output = CLI::ConfigINI().from_config(ofile); EXPECT_EQ((size_t)3, output.size()); - EXPECT_EQ("one", output.at(0).name()); + EXPECT_EQ("one", output.at(0).name); EXPECT_EQ((size_t)1, output.at(0).inputs.size()); EXPECT_EQ("three", output.at(0).inputs.at(0)); - EXPECT_EQ("two", output.at(1).name()); + EXPECT_EQ("two", output.at(1).name); EXPECT_EQ((size_t)1, output.at(1).inputs.size()); EXPECT_EQ("four", output.at(1).inputs.at(0)); - EXPECT_EQ("five", output.at(2).name()); + EXPECT_EQ("five", output.at(2).name); EXPECT_EQ((size_t)3, output.at(2).inputs.size()); EXPECT_EQ("six", output.at(2).inputs.at(0)); EXPECT_EQ("and", output.at(2).inputs.at(1)); @@ -103,13 +103,13 @@ TEST(StringBased, Spaces) { ofile.seekg(0, std::ios::beg); - auto output = CLI::detail::parse_ini(ofile); + std::vector output = CLI::ConfigINI().from_config(ofile); EXPECT_EQ((size_t)2, output.size()); - EXPECT_EQ("one", output.at(0).name()); + EXPECT_EQ("one", output.at(0).name); EXPECT_EQ((size_t)1, output.at(0).inputs.size()); EXPECT_EQ("three", output.at(0).inputs.at(0)); - EXPECT_EQ("two", output.at(1).name()); + EXPECT_EQ("two", output.at(1).name); EXPECT_EQ((size_t)1, output.at(1).inputs.size()); EXPECT_EQ("four", output.at(1).inputs.at(0)); } @@ -123,16 +123,17 @@ TEST(StringBased, Sections) { ofile.seekg(0, std::ios::beg); - auto output = CLI::detail::parse_ini(ofile); + std::vector output = CLI::ConfigINI().from_config(ofile); EXPECT_EQ((size_t)2, output.size()); - EXPECT_EQ("one", output.at(0).name()); + EXPECT_EQ("one", output.at(0).name); EXPECT_EQ((size_t)1, output.at(0).inputs.size()); EXPECT_EQ("three", output.at(0).inputs.at(0)); - EXPECT_EQ("two", output.at(1).name()); - EXPECT_EQ("second", output.at(1).parent()); + EXPECT_EQ("two", output.at(1).name); + EXPECT_EQ("second", output.at(1).parents.at(0)); EXPECT_EQ((size_t)1, output.at(1).inputs.size()); EXPECT_EQ("four", output.at(1).inputs.at(0)); + EXPECT_EQ("second.two", output.at(1).fullname()); } TEST(StringBased, SpacesSections) { @@ -146,14 +147,15 @@ TEST(StringBased, SpacesSections) { ofile.seekg(0, std::ios::beg); - auto output = CLI::detail::parse_ini(ofile); + std::vector output = CLI::ConfigINI().from_config(ofile); EXPECT_EQ((size_t)2, output.size()); - EXPECT_EQ("one", output.at(0).name()); + EXPECT_EQ("one", output.at(0).name); EXPECT_EQ((size_t)1, output.at(0).inputs.size()); EXPECT_EQ("three", output.at(0).inputs.at(0)); - EXPECT_EQ("two", output.at(1).name()); - EXPECT_EQ("second", output.at(1).parent()); + EXPECT_EQ("two", output.at(1).name); + EXPECT_EQ((size_t)1, output.at(1).parents.size()); + EXPECT_EQ("second", output.at(1).parents.at(0)); EXPECT_EQ((size_t)1, output.at(1).inputs.size()); EXPECT_EQ("four", output.at(1).inputs.at(0)); } @@ -199,7 +201,7 @@ TEST_F(TApp, IniSuccessOnUnknownOption) { TempFile tmpini{"TestIniTmp.ini"}; app.set_config("--config", tmpini); - app.allow_ini_extras(true); + app.allow_config_extras(true); { std::ofstream out{tmpini}; @@ -209,7 +211,7 @@ TEST_F(TApp, IniSuccessOnUnknownOption) { int two = 0; app.add_option("--two", two); - EXPECT_NO_THROW(run()); + run(); EXPECT_EQ(99, two); } @@ -217,7 +219,7 @@ TEST_F(TApp, IniGetRemainingOption) { TempFile tmpini{"TestIniTmp.ini"}; app.set_config("--config", tmpini); - app.allow_ini_extras(true); + app.allow_config_extras(true); std::string ExtraOption = "three"; std::string ExtraOptionValue = "3"; @@ -238,7 +240,7 @@ TEST_F(TApp, IniGetNoRemaining) { TempFile tmpini{"TestIniTmp.ini"}; app.set_config("--config", tmpini); - app.allow_ini_extras(true); + app.allow_config_extras(true); { std::ofstream out{tmpini}; @@ -413,7 +415,7 @@ TEST_F(TApp, IniLayered) { auto subsubcom = subcom->add_subcommand("subsubcom"); subsubcom->add_option("--val", three); - ASSERT_NO_THROW(run()); + run(); EXPECT_EQ(1, one); EXPECT_EQ(2, two); @@ -432,7 +434,7 @@ TEST_F(TApp, IniFailure) { out << "val=1" << std::endl; } - EXPECT_THROW(run(), CLI::INIError); + EXPECT_THROW(run(), CLI::ConfigError); } TEST_F(TApp, IniConfigurable) { @@ -467,7 +469,7 @@ TEST_F(TApp, IniNotConfigurable) { out << "val=1" << std::endl; } - EXPECT_THROW(run(), CLI::INIError); + EXPECT_THROW(run(), CLI::ConfigError); } TEST_F(TApp, IniSubFailure) { @@ -483,7 +485,7 @@ TEST_F(TApp, IniSubFailure) { out << "val=1" << std::endl; } - EXPECT_THROW(run(), CLI::INIError); + EXPECT_THROW(run(), CLI::ConfigError); } TEST_F(TApp, IniNoSubFailure) { @@ -498,7 +500,7 @@ TEST_F(TApp, IniNoSubFailure) { out << "val=1" << std::endl; } - EXPECT_THROW(run(), CLI::INIError); + EXPECT_THROW(run(), CLI::ConfigError); } TEST_F(TApp, IniFlagConvertFailure) { @@ -638,7 +640,7 @@ TEST_F(TApp, IniOutputShortSingleDescription) { run(); - std::string str = app.config_to_str(true, "", true); + std::string str = app.config_to_str(true, true); EXPECT_THAT(str, HasSubstr("; " + description + "\n" + flag + "=false\n")); } @@ -652,7 +654,7 @@ TEST_F(TApp, IniOutputShortDoubleDescription) { run(); - std::string str = app.config_to_str(true, "", true); + std::string str = app.config_to_str(true, true); EXPECT_EQ(str, "; " + description1 + "\n" + flag1 + "=false\n\n; " + description2 + "\n" + flag2 + "=false\n"); } @@ -663,7 +665,7 @@ TEST_F(TApp, IniOutputMultiLineDescription) { run(); - std::string str = app.config_to_str(true, "", true); + std::string str = app.config_to_str(true, true); EXPECT_THAT(str, HasSubstr("; Some short description.\n")); EXPECT_THAT(str, HasSubstr("; That has lines.\n")); EXPECT_THAT(str, HasSubstr(flag + "=false\n"));