From a1135bb30c1ec887e096cd5f05e92ee337ac6fe1 Mon Sep 17 00:00:00 2001 From: Philip Top Date: Tue, 27 Jun 2023 11:40:22 -0700 Subject: [PATCH] Add environment variable processing to the configuration pointer. (#891) Fixes #890 Add parsing of environmental variables when supplied for the config file option. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- include/CLI/StringTools.hpp | 2 ++ include/CLI/TypeTools.hpp | 3 +++ include/CLI/impl/App_inl.hpp | 34 +++++++++++----------------- include/CLI/impl/StringTools_inl.hpp | 21 +++++++++++++++++ tests/ConfigFileTest.cpp | 30 ++++++++++++++++++++++++ tests/HelpersTest.cpp | 11 +++++++++ 6 files changed, 80 insertions(+), 21 deletions(-) diff --git a/include/CLI/StringTools.hpp b/include/CLI/StringTools.hpp index 2a31005c..4a035b32 100644 --- a/include/CLI/StringTools.hpp +++ b/include/CLI/StringTools.hpp @@ -223,6 +223,8 @@ CLI11_INLINE std::size_t escape_detect(std::string &str, std::size_t offset); /// Add quotes if the string contains spaces CLI11_INLINE std::string &add_quotes_if_needed(std::string &str); +/// get the value of an environmental variable or empty string if empty +CLI11_INLINE std::string get_environment_value(const std::string &env_name); } // namespace detail // [CLI11:string_tools_hpp:end] diff --git a/include/CLI/TypeTools.hpp b/include/CLI/TypeTools.hpp index 558c5d21..3979e0cb 100644 --- a/include/CLI/TypeTools.hpp +++ b/include/CLI/TypeTools.hpp @@ -1333,6 +1333,9 @@ template = detail::dummy> bool lexical_conversion(const std::vector &strings, AssignTo &output) { output.erase(output.begin(), output.end()); + if(strings.empty()) { + return true; + } if(strings.size() == 1 && strings[0] == "{}") { return true; } diff --git a/include/CLI/impl/App_inl.hpp b/include/CLI/impl/App_inl.hpp index 7d487442..d59019d5 100644 --- a/include/CLI/impl/App_inl.hpp +++ b/include/CLI/impl/App_inl.hpp @@ -315,8 +315,11 @@ CLI11_INLINE Option *App::set_config(std::string option_name, } if(!default_filename.empty()) { config_ptr_->default_str(std::move(default_filename)); + config_ptr_->force_callback_ = true; } config_ptr_->configurable(false); + // set the option to take the last value given by default + config_ptr_->take_last(); } return config_ptr_; @@ -1014,10 +1017,18 @@ CLI11_INLINE void App::_process_config_file() { if(config_ptr_ != nullptr) { bool config_required = config_ptr_->get_required(); auto file_given = config_ptr_->count() > 0; + if(!(file_given || config_ptr_->envname_.empty())) { + std::string ename_string = detail::get_environment_value(config_ptr_->envname_); + if(!ename_string.empty()) { + config_ptr_->add_result(ename_string); + } + } + config_ptr_->run_callback(); + auto config_files = config_ptr_->as>(); if(config_files.empty() || config_files.front().empty()) { if(config_required) { - throw FileError::Missing("no specified config file"); + throw FileError("config file is required but none was given"); } return; } @@ -1028,9 +1039,6 @@ CLI11_INLINE void App::_process_config_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; @@ -1045,23 +1053,7 @@ CLI11_INLINE void App::_process_config_file() { CLI11_INLINE void App::_process_env() { for(const Option_p &opt : options_) { if(opt->count() == 0 && !opt->envname_.empty()) { - char *buffer = nullptr; - std::string ename_string; - -#ifdef _MSC_VER - // Windows version - std::size_t sz = 0; - if(_dupenv_s(&buffer, &sz, opt->envname_.c_str()) == 0 && buffer != nullptr) { - ename_string = std::string(buffer); - free(buffer); - } -#else - // This also works on Windows, but gives a warning - buffer = std::getenv(opt->envname_.c_str()); - if(buffer != nullptr) - ename_string = std::string(buffer); -#endif - + std::string ename_string = detail::get_environment_value(opt->envname_); if(!ename_string.empty()) { opt->add_result(ename_string); } diff --git a/include/CLI/impl/StringTools_inl.hpp b/include/CLI/impl/StringTools_inl.hpp index 9b81fbde..fd3dcc2f 100644 --- a/include/CLI/impl/StringTools_inl.hpp +++ b/include/CLI/impl/StringTools_inl.hpp @@ -255,6 +255,27 @@ CLI11_INLINE std::string &add_quotes_if_needed(std::string &str) { return str; } +std::string get_environment_value(const std::string &env_name) { + char *buffer = nullptr; + std::string ename_string; + +#ifdef _MSC_VER + // Windows version + std::size_t sz = 0; + if(_dupenv_s(&buffer, &sz, env_name.c_str()) == 0 && buffer != nullptr) { + ename_string = std::string(buffer); + free(buffer); + } +#else + // This also works on Windows, but gives a warning + buffer = std::getenv(env_name.c_str()); + if(buffer != nullptr) { + ename_string = std::string(buffer); + } +#endif + return ename_string; +} + } // namespace detail // [CLI11:string_tools_inl_hpp:end] } // namespace CLI diff --git a/tests/ConfigFileTest.cpp b/tests/ConfigFileTest.cpp index 20687272..9bdf5939 100644 --- a/tests/ConfigFileTest.cpp +++ b/tests/ConfigFileTest.cpp @@ -673,6 +673,36 @@ TEST_CASE_METHOD(TApp, "IniNotRequiredNotDefault", "[config]") { CHECK(tmpini2.c_str() == app.get_config_ptr()->as()); } +TEST_CASE_METHOD(TApp, "IniEnvironmentalFileName", "[config]") { + + TempFile tmpini{"TestIniTmp.ini"}; + + app.set_config("--config", "")->envname("CONFIG")->required(); + + { + std::ofstream out{tmpini}; + out << "[default]" << std::endl; + out << "two=99" << std::endl; + out << "three=3" << std::endl; + } + + int one{0}, two{0}, three{0}; + app.add_option("--one", one); + app.add_option("--two", two); + app.add_option("--three", three); + + put_env("CONFIG", tmpini); + + CHECK_NOTHROW(run()); + + CHECK(two == 99); + CHECK(three == 3); + + unset_env("CONFIG"); + + CHECK_THROWS_AS(run(), CLI::FileError); +} + TEST_CASE_METHOD(TApp, "MultiConfig", "[config]") { TempFile tmpini{"TestIniTmp.ini"}; diff --git a/tests/HelpersTest.cpp b/tests/HelpersTest.cpp index e3d68c9c..d92d3fce 100644 --- a/tests/HelpersTest.cpp +++ b/tests/HelpersTest.cpp @@ -1354,3 +1354,14 @@ TEST_CASE("FixNewLines: EdgesCheck", "[helpers]") { std::string result = CLI::detail::fix_newlines("; ", input); CHECK(output == result); } + +TEST_CASE("String: environment", "[helpers]") { + put_env("TEST1", "TESTS"); + + auto value = CLI::detail::get_environment_value("TEST1"); + CHECK(value == "TESTS"); + unset_env("TEST1"); + + value = CLI::detail::get_environment_value("TEST2"); + CHECK(value.empty()); +}