diff --git a/README.md b/README.md index c8bf4f80..708c801e 100644 --- a/README.md +++ b/README.md @@ -748,6 +748,14 @@ Spaces before and after the name and argument are ignored. Multiple arguments ar To print a configuration file from the passed arguments, use `.config_to_str(default_also=false, write_description=false)`, where `default_also` will also show any defaulted arguments, and `write_description` will include the app and option descriptions. See [Config files](https://cliutils.github.io/CLI11/book/chapters/config.html) for some additional details. +If it is desired that multiple configuration be allowed. Use + +```cpp +app.set_config("--config")->expected(1, X); +``` + +Where X is some positive number and will allow up to `X` configuration files to be specified by separate `--config` arguments. + ### Inheriting defaults Many of the defaults for subcommands and even options are inherited from their creators. The inherited default values for subcommands are `allow_extras`, `prefix_command`, `ignore_case`, `ignore_underscore`, `fallthrough`, `group`, `footer`,`immediate_callback` and maximum number of required subcommands. The help flag existence, name, and description are inherited, as well. diff --git a/book/chapters/config.md b/book/chapters/config.md index fd2dbd7d..219c3d9d 100644 --- a/book/chapters/config.md +++ b/book/chapters/config.md @@ -78,6 +78,16 @@ sub.subcommand = true The main differences are in vector notation and comment character. Note: CLI11 is not a full TOML parser as it just reads values as strings. It is possible (but not recommended) to mix notation. +## Multiple configuration files + +If it is desired that multiple configuration be allowed. Use + +```cpp +app.set_config("--config")->expected(1, X); +``` + +Where X is some positive integer and will allow up to `X` configuration files to be specified by separate `--config` arguments. + ## Writing out a configure file To print a configuration file from the passed arguments, use `.config_to_str(default_also=false, write_description=false)`, where `default_also` will also show any defaulted arguments, and `write_description` will include option descriptions and the App description diff --git a/include/CLI/App.hpp b/include/CLI/App.hpp index 6534a2b2..94f05c85 100644 --- a/include/CLI/App.hpp +++ b/include/CLI/App.hpp @@ -2053,29 +2053,31 @@ class App { void _process_config_file() { if(config_ptr_ != nullptr) { bool config_required = config_ptr_->get_required(); - bool file_given = config_ptr_->count() > 0; - auto config_file = config_ptr_->as(); - if(config_file.empty()) { + auto file_given = config_ptr_->count() > 0; + auto config_files = config_ptr_->as>(); + if(config_files.empty() || config_files.front().empty()) { if(config_required) { throw FileError::Missing("no specified config file"); } return; } - - auto path_result = detail::check_path(config_file.c_str()); - if(path_result == detail::path_type::file) { - try { - std::vector values = config_formatter_->from_file(config_file); - _parse_config(values); - if(!file_given) { - config_ptr_->add_result(config_file); + for(auto rit = config_files.rbegin(); rit != config_files.rend(); ++rit) { + const auto &config_file = *rit; + auto path_result = detail::check_path(config_file.c_str()); + if(path_result == detail::path_type::file) { + try { + std::vector values = config_formatter_->from_file(config_file); + _parse_config(values); + if(!file_given) { + config_ptr_->add_result(config_file); + } + } catch(const FileError &) { + if(config_required || file_given) + throw; } - } catch(const FileError &) { - if(config_required || file_given) - throw; + } else if(config_required || file_given) { + throw FileError::Missing(config_file); } - } else if(config_required || file_given) { - throw FileError::Missing(config_file); } } } diff --git a/tests/ConfigFileTest.cpp b/tests/ConfigFileTest.cpp index dc8ee05e..590eb6b6 100644 --- a/tests/ConfigFileTest.cpp +++ b/tests/ConfigFileTest.cpp @@ -591,6 +591,89 @@ TEST_F(TApp, IniNotRequiredNotDefault) { EXPECT_EQ(app.get_config_ptr()->as(), tmpini2.c_str()); } +TEST_F(TApp, MultiConfig) { + + TempFile tmpini{"TestIniTmp.ini"}; + TempFile tmpini2{"TestIniTmp2.ini"}; + + app.set_config("--config")->expected(1, 3); + + { + std::ofstream out{tmpini}; + out << "[default]" << std::endl; + out << "two=99" << std::endl; + out << "three=3" << std::endl; + } + + { + std::ofstream out{tmpini2}; + out << "[default]" << std::endl; + out << "one=55" << std::endl; + out << "three=4" << std::endl; + } + + int one{0}, two{0}, three{0}; + app.add_option("--one", one); + app.add_option("--two", two); + app.add_option("--three", three); + + args = {"--config", tmpini2, "--config", tmpini}; + run(); + + EXPECT_EQ(99, two); + EXPECT_EQ(3, three); + EXPECT_EQ(55, one); + + args = {"--config", tmpini, "--config", tmpini2}; + run(); + + EXPECT_EQ(99, two); + EXPECT_EQ(4, three); + EXPECT_EQ(55, one); +} + +TEST_F(TApp, MultiConfig_single) { + + TempFile tmpini{"TestIniTmp.ini"}; + TempFile tmpini2{"TestIniTmp2.ini"}; + + app.set_config("--config")->multi_option_policy(CLI::MultiOptionPolicy::TakeLast); + + { + std::ofstream out{tmpini}; + out << "[default]" << std::endl; + out << "two=99" << std::endl; + out << "three=3" << std::endl; + } + + { + std::ofstream out{tmpini2}; + out << "[default]" << std::endl; + out << "one=55" << std::endl; + out << "three=4" << std::endl; + } + + int one{0}, two{0}, three{0}; + app.add_option("--one", one); + app.add_option("--two", two); + app.add_option("--three", three); + + args = {"--config", tmpini2, "--config", tmpini}; + run(); + + EXPECT_EQ(99, two); + EXPECT_EQ(3, three); + EXPECT_EQ(0, one); + + two = 0; + args = {"--config", tmpini, "--config", tmpini2}; + run(); + + EXPECT_EQ(0, two); + EXPECT_EQ(4, three); + EXPECT_EQ(55, one); +} + TEST_F(TApp, IniRequiredNotFound) { std::string noini = "TestIniNotExist.ini"; diff --git a/tests/TransformTest.cpp b/tests/TransformTest.cpp index ae802af0..523472f0 100644 --- a/tests/TransformTest.cpp +++ b/tests/TransformTest.cpp @@ -7,6 +7,7 @@ #include "app_helper.hpp" #include +#include #include #include @@ -850,6 +851,22 @@ TEST_F(TApp, AsSizeValue1000_1024) { EXPECT_EQ(value, ki_value); } +TEST_F(TApp, duration_test) { + std::chrono::seconds duration{1}; + + app.option_defaults()->ignore_case(); + app.add_option_function( + "--duration", + [&](size_t a_value) { duration = std::chrono::seconds{a_value}; }, + "valid units: sec, min, h, day.") + ->capture_default_str() + ->transform(CLI::AsNumberWithUnit( + std::map{{"sec", 1}, {"min", 60}, {"h", 3600}, {"day", 24 * 3600}})); + EXPECT_NO_THROW(app.parse(std::vector{"1 day", "--duration"})); + + EXPECT_EQ(duration, std::chrono::seconds(86400)); +} + TEST_F(TApp, AsSizeValue1024) { std::uint64_t value{0}; app.add_option("-s", value)->transform(CLI::AsSizeValue(false));