diff --git a/book/chapters/config.md b/book/chapters/config.md index 54f866139..8a726102e 100644 --- a/book/chapters/config.md +++ b/book/chapters/config.md @@ -222,6 +222,43 @@ characters. Characters not in this form will be translated as given. If argument values with unprintable characters are used to generate a config file this binary form will be used in the output string. +### vector of vector inputs + +It is possible to specify vector of vector inputs in config file. This can be +done in a couple different ways + +```toml +# Examples of vector of vector inputs in config + +# this example is how config_to_str writes it out +vector1 = [1,2,3,"",4,5,6] + +# alternative with vector separator sequence +vector2 = [1,2,3,"%%",4,5,6] + +# multiline format +vector3 = [1,2,3] +vector3 = [4,5,6] + +``` + +The `%%` is ignored in multiline format if the inject_separator modifier on the +option is set to false, thus for vector 3 if the option is storing to a single +vector all the elements will be in that vector. + +For config file multiple sequential duplicate variable names are treated as if +they are a vector input, with possible separator insertion in the case of +multiple input vectors. + +The config parser has a modifier + +```C++ + app.get_config_formatter_base()->allowDuplicateFields(); +``` + +This modification will insert the separator between each line even if not +sequential. This allows an input option to be configured with multiple lines. + ## Multiple configuration files If it is desired that multiple configuration be allowed. Use diff --git a/include/CLI/ConfigFwd.hpp b/include/CLI/ConfigFwd.hpp index 3db801027..3f8ffdb7f 100644 --- a/include/CLI/ConfigFwd.hpp +++ b/include/CLI/ConfigFwd.hpp @@ -34,12 +34,14 @@ struct ConfigItem { std::string name{}; /// Listing of inputs std::vector inputs{}; - + /// @brief indicator if a multiline vector separator was inserted + bool multiline{false}; /// The list of parents and name joined by "." CLI11_NODISCARD std::string fullname() const { std::vector tmp = parents; tmp.emplace_back(name); return detail::join(tmp, "."); + (void)multiline; // suppression for cppcheck false positive } }; @@ -102,6 +104,8 @@ class ConfigBase : public Config { char parentSeparatorChar{'.'}; /// Specify the configuration index to use for arrayed sections int16_t configIndex{-1}; + /// specify the config reader should collapse repeated field names to a single vector + bool allowMultipleDuplicateFields{false}; /// Specify the configuration section that should be used std::string configSection{}; @@ -166,6 +170,11 @@ class ConfigBase : public Config { configIndex = sectionIndex; return this; } + /// specify that multiple duplicate arguments should be merged even if not sequential + ConfigBase *allowDuplicateFields(bool value = true) { + allowMultipleDuplicateFields = value; + return this; + } }; /// the default Config is the TOML file format diff --git a/include/CLI/impl/App_inl.hpp b/include/CLI/impl/App_inl.hpp index b6c54701f..12b095a2c 100644 --- a/include/CLI/impl/App_inl.hpp +++ b/include/CLI/impl/App_inl.hpp @@ -1529,9 +1529,17 @@ CLI11_INLINE bool App::_parse_single_config(const ConfigItem &item, std::size_t } throw ConfigError::NotConfigurable(item.fullname()); } - if(op->empty()) { - + std::vector buffer; // a buffer to use for copying an modifying inputs in a few cases + bool useBuffer{false}; + if(item.multiline) { + if(!op->get_inject_separator()) { + buffer = item.inputs; + buffer.erase(std::remove(buffer.begin(), buffer.end(), "%%"), buffer.end()); + useBuffer = true; + } + } + const std::vector &inputs = (useBuffer) ? buffer : item.inputs; if(op->get_expected_min() == 0) { if(item.inputs.size() <= 1) { // Flag parsing @@ -1555,10 +1563,10 @@ CLI11_INLINE bool App::_parse_single_config(const ConfigItem &item, std::size_t op->add_result(res); return true; } - if(static_cast(item.inputs.size()) > op->get_items_expected_max() && + if(static_cast(inputs.size()) > op->get_items_expected_max() && op->get_multi_option_policy() != MultiOptionPolicy::TakeAll) { if(op->get_items_expected_max() > 1) { - throw ArgumentMismatch::AtMost(item.fullname(), op->get_items_expected_max(), item.inputs.size()); + throw ArgumentMismatch::AtMost(item.fullname(), op->get_items_expected_max(), inputs.size()); } if(!op->get_disable_flag_override()) { @@ -1566,7 +1574,7 @@ CLI11_INLINE bool App::_parse_single_config(const ConfigItem &item, std::size_t } // if the disable flag override is set then we must have the flag values match a known flag value // this is true regardless of the output value, so an array input is possible and must be accounted for - for(const auto &res : item.inputs) { + for(const auto &res : inputs) { bool valid_value{false}; if(op->default_flag_values_.empty()) { if(res == "true" || res == "false" || res == "1" || res == "0") { @@ -1590,7 +1598,7 @@ CLI11_INLINE bool App::_parse_single_config(const ConfigItem &item, std::size_t return true; } } - op->add_result(item.inputs); + op->add_result(inputs); op->run_callback(); } diff --git a/include/CLI/impl/Config_inl.hpp b/include/CLI/impl/Config_inl.hpp index 255d97bee..848f0b266 100644 --- a/include/CLI/impl/Config_inl.hpp +++ b/include/CLI/impl/Config_inl.hpp @@ -201,6 +201,27 @@ CLI11_INLINE bool hasMLString(std::string const &fullString, char check) { auto it = fullString.rbegin(); return (*it == check) && (*(it + 1) == check) && (*(it + 2) == check); } + +/// @brief find a matching configItem in a list +inline auto find_matching_config(std::vector &items, + const std::vector &parents, + const std::string &name, + bool fullSearch) -> decltype(items.begin()) { + if(items.empty()) { + return items.end(); + } + auto search = items.end() - 1; + do { + if(search->parents == parents && search->name == name) { + return search; + } + if(search == items.begin()) { + break; + } + --search; + } while(fullSearch); + return items.end(); +} } // namespace detail inline std::vector ConfigBase::from_config(std::istream &input) const { @@ -426,8 +447,17 @@ inline std::vector ConfigBase::from_config(std::istream &input) cons parents.erase(parents.begin()); inSection = true; } - if(!output.empty() && name == output.back().name && parents == output.back().parents) { - output.back().inputs.insert(output.back().inputs.end(), items_buffer.begin(), items_buffer.end()); + auto match = detail::find_matching_config(output, parents, name, allowMultipleDuplicateFields); + if(match != output.end()) { + if((match->inputs.size() > 1 && items_buffer.size() > 1) || allowMultipleDuplicateFields) { + // insert a separator if one is not already present + if(!(match->inputs.back().empty() || items_buffer.front().empty() || match->inputs.back() == "%%" || + items_buffer.front() == "%%")) { + match->inputs.emplace_back("%%"); + match->multiline = true; + } + } + match->inputs.insert(match->inputs.end(), items_buffer.begin(), items_buffer.end()); } else { output.emplace_back(); output.back().parents = std::move(parents); diff --git a/tests/ConfigFileTest.cpp b/tests/ConfigFileTest.cpp index 001473cef..00e8fa0f7 100644 --- a/tests/ConfigFileTest.cpp +++ b/tests/ConfigFileTest.cpp @@ -1555,6 +1555,104 @@ TEST_CASE_METHOD(TApp, "TOMLVectordirect", "[config]") { CHECK(three == std::vector({1, 2, 3})); } +TEST_CASE_METHOD(TApp, "TOMLVectorVector", "[config]") { + + TempFile tmpini{"TestIniTmp.ini"}; + + app.set_config("--config", tmpini); + + app.config_formatter(std::make_shared()); + + { + std::ofstream out{tmpini}; + out << "#this is a comment line\n"; + out << "[default]\n"; + out << "two=1,2,3\n"; + out << "two= 4, 5, 6\n"; + out << "three=1,2,3\n"; + out << "three= 4, 5, 6\n"; + out << "four=1,2\n"; + out << "four= 3,4\n"; + out << "four=5,6\n"; + out << "four= 7,8\n"; + } + + std::vector> two; + std::vector three, four; + app.add_option("--two", two)->delimiter(','); + app.add_option("--three", three)->delimiter(','); + app.add_option("--four", four)->delimiter(','); + + run(); + + auto str = app.config_to_str(); + CHECK(two == std::vector>({{1, 2, 3}, {4, 5, 6}})); + CHECK(three == std::vector({1, 2, 3, 4, 5, 6})); + CHECK(four == std::vector({1, 2, 3, 4, 5, 6, 7, 8})); +} + +TEST_CASE_METHOD(TApp, "TOMLVectorVectorSeparated", "[config]") { + + TempFile tmpini{"TestIniTmp.ini"}; + + app.set_config("--config", tmpini); + + app.config_formatter(std::make_shared()); + app.get_config_formatter_base()->allowDuplicateFields(); + { + std::ofstream out{tmpini}; + out << "#this is a comment line\n"; + out << "[default]\n"; + out << "two=1,2,3\n"; + out << "three=1,2,3\n"; + out << "three= 4, 5, 6\n"; + out << "two= 4, 5, 6\n"; + } + + std::vector> two; + std::vector three; + app.add_option("--two", two)->delimiter(','); + app.add_option("--three", three)->delimiter(','); + + run(); + + auto str = app.config_to_str(); + CHECK(two == std::vector>({{1, 2, 3}, {4, 5, 6}})); + CHECK(three == std::vector({1, 2, 3, 4, 5, 6})); +} + +TEST_CASE_METHOD(TApp, "TOMLVectorVectorSeparatedSingleElement", "[config]") { + + TempFile tmpini{"TestIniTmp.ini"}; + + app.set_config("--config", tmpini); + + app.config_formatter(std::make_shared()); + app.get_config_formatter_base()->allowDuplicateFields(); + { + std::ofstream out{tmpini}; + out << "#this is a comment line\n"; + out << "[default]\n"; + out << "two=1\n"; + out << "three=1\n"; + out << "three= 4\n"; + out << "three= 5\n"; + out << "two= 2\n"; + out << "two=3\n"; + } + + std::vector> two; + std::vector three; + app.add_option("--two", two)->delimiter(','); + app.add_option("--three", three)->delimiter(','); + + run(); + + auto str = app.config_to_str(); + CHECK(two == std::vector>({{1}, {2}, {3}})); + CHECK(three == std::vector({1, 4, 5})); +} + TEST_CASE_METHOD(TApp, "TOMLStringVector", "[config]") { TempFile tmptoml{"TestTomlTmp.toml"};