Commit 6aa58d58285a6c655f21b57c2f8be13dc1f0983c

Authored by Philip Top
Committed by GitHub
1 parent 8ce1594e

Add an ability to deal handle multiple config files (#494)

README.md
@@ -748,6 +748,14 @@ Spaces before and after the name and argument are ignored. Multiple arguments ar @@ -748,6 +748,14 @@ Spaces before and after the name and argument are ignored. Multiple arguments ar
748 To print a configuration file from the passed 748 To print a configuration file from the passed
749 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. 749 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.
750 750
  751 +If it is desired that multiple configuration be allowed. Use
  752 +
  753 +```cpp
  754 +app.set_config("--config")->expected(1, X);
  755 +```
  756 +
  757 +Where X is some positive number and will allow up to `X` configuration files to be specified by separate `--config` arguments.
  758 +
751 ### Inheriting defaults 759 ### Inheriting defaults
752 760
753 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. 761 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.
book/chapters/config.md
@@ -78,6 +78,16 @@ sub.subcommand = true @@ -78,6 +78,16 @@ sub.subcommand = true
78 78
79 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. 79 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.
80 80
  81 +## Multiple configuration files
  82 +
  83 +If it is desired that multiple configuration be allowed. Use
  84 +
  85 +```cpp
  86 +app.set_config("--config")->expected(1, X);
  87 +```
  88 +
  89 +Where X is some positive integer and will allow up to `X` configuration files to be specified by separate `--config` arguments.
  90 +
81 ## Writing out a configure file 91 ## Writing out a configure file
82 92
83 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 93 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
include/CLI/App.hpp
@@ -2053,29 +2053,31 @@ class App { @@ -2053,29 +2053,31 @@ class App {
2053 void _process_config_file() { 2053 void _process_config_file() {
2054 if(config_ptr_ != nullptr) { 2054 if(config_ptr_ != nullptr) {
2055 bool config_required = config_ptr_->get_required(); 2055 bool config_required = config_ptr_->get_required();
2056 - bool file_given = config_ptr_->count() > 0;  
2057 - auto config_file = config_ptr_->as<std::string>();  
2058 - if(config_file.empty()) { 2056 + auto file_given = config_ptr_->count() > 0;
  2057 + auto config_files = config_ptr_->as<std::vector<std::string>>();
  2058 + if(config_files.empty() || config_files.front().empty()) {
2059 if(config_required) { 2059 if(config_required) {
2060 throw FileError::Missing("no specified config file"); 2060 throw FileError::Missing("no specified config file");
2061 } 2061 }
2062 return; 2062 return;
2063 } 2063 }
2064 -  
2065 - auto path_result = detail::check_path(config_file.c_str());  
2066 - if(path_result == detail::path_type::file) {  
2067 - try {  
2068 - std::vector<ConfigItem> values = config_formatter_->from_file(config_file);  
2069 - _parse_config(values);  
2070 - if(!file_given) {  
2071 - config_ptr_->add_result(config_file); 2064 + for(auto rit = config_files.rbegin(); rit != config_files.rend(); ++rit) {
  2065 + const auto &config_file = *rit;
  2066 + auto path_result = detail::check_path(config_file.c_str());
  2067 + if(path_result == detail::path_type::file) {
  2068 + try {
  2069 + std::vector<ConfigItem> values = config_formatter_->from_file(config_file);
  2070 + _parse_config(values);
  2071 + if(!file_given) {
  2072 + config_ptr_->add_result(config_file);
  2073 + }
  2074 + } catch(const FileError &) {
  2075 + if(config_required || file_given)
  2076 + throw;
2072 } 2077 }
2073 - } catch(const FileError &) {  
2074 - if(config_required || file_given)  
2075 - throw; 2078 + } else if(config_required || file_given) {
  2079 + throw FileError::Missing(config_file);
2076 } 2080 }
2077 - } else if(config_required || file_given) {  
2078 - throw FileError::Missing(config_file);  
2079 } 2081 }
2080 } 2082 }
2081 } 2083 }
tests/ConfigFileTest.cpp
@@ -591,6 +591,89 @@ TEST_F(TApp, IniNotRequiredNotDefault) { @@ -591,6 +591,89 @@ TEST_F(TApp, IniNotRequiredNotDefault) {
591 EXPECT_EQ(app.get_config_ptr()->as<std::string>(), tmpini2.c_str()); 591 EXPECT_EQ(app.get_config_ptr()->as<std::string>(), tmpini2.c_str());
592 } 592 }
593 593
  594 +TEST_F(TApp, MultiConfig) {
  595 +
  596 + TempFile tmpini{"TestIniTmp.ini"};
  597 + TempFile tmpini2{"TestIniTmp2.ini"};
  598 +
  599 + app.set_config("--config")->expected(1, 3);
  600 +
  601 + {
  602 + std::ofstream out{tmpini};
  603 + out << "[default]" << std::endl;
  604 + out << "two=99" << std::endl;
  605 + out << "three=3" << std::endl;
  606 + }
  607 +
  608 + {
  609 + std::ofstream out{tmpini2};
  610 + out << "[default]" << std::endl;
  611 + out << "one=55" << std::endl;
  612 + out << "three=4" << std::endl;
  613 + }
  614 +
  615 + int one{0}, two{0}, three{0};
  616 + app.add_option("--one", one);
  617 + app.add_option("--two", two);
  618 + app.add_option("--three", three);
  619 +
  620 + args = {"--config", tmpini2, "--config", tmpini};
  621 + run();
  622 +
  623 + EXPECT_EQ(99, two);
  624 + EXPECT_EQ(3, three);
  625 + EXPECT_EQ(55, one);
  626 +
  627 + args = {"--config", tmpini, "--config", tmpini2};
  628 + run();
  629 +
  630 + EXPECT_EQ(99, two);
  631 + EXPECT_EQ(4, three);
  632 + EXPECT_EQ(55, one);
  633 +}
  634 +
  635 +TEST_F(TApp, MultiConfig_single) {
  636 +
  637 + TempFile tmpini{"TestIniTmp.ini"};
  638 + TempFile tmpini2{"TestIniTmp2.ini"};
  639 +
  640 + app.set_config("--config")->multi_option_policy(CLI::MultiOptionPolicy::TakeLast);
  641 +
  642 + {
  643 + std::ofstream out{tmpini};
  644 + out << "[default]" << std::endl;
  645 + out << "two=99" << std::endl;
  646 + out << "three=3" << std::endl;
  647 + }
  648 +
  649 + {
  650 + std::ofstream out{tmpini2};
  651 + out << "[default]" << std::endl;
  652 + out << "one=55" << std::endl;
  653 + out << "three=4" << std::endl;
  654 + }
  655 +
  656 + int one{0}, two{0}, three{0};
  657 + app.add_option("--one", one);
  658 + app.add_option("--two", two);
  659 + app.add_option("--three", three);
  660 +
  661 + args = {"--config", tmpini2, "--config", tmpini};
  662 + run();
  663 +
  664 + EXPECT_EQ(99, two);
  665 + EXPECT_EQ(3, three);
  666 + EXPECT_EQ(0, one);
  667 +
  668 + two = 0;
  669 + args = {"--config", tmpini, "--config", tmpini2};
  670 + run();
  671 +
  672 + EXPECT_EQ(0, two);
  673 + EXPECT_EQ(4, three);
  674 + EXPECT_EQ(55, one);
  675 +}
  676 +
594 TEST_F(TApp, IniRequiredNotFound) { 677 TEST_F(TApp, IniRequiredNotFound) {
595 678
596 std::string noini = "TestIniNotExist.ini"; 679 std::string noini = "TestIniNotExist.ini";
tests/TransformTest.cpp
@@ -7,6 +7,7 @@ @@ -7,6 +7,7 @@
7 #include "app_helper.hpp" 7 #include "app_helper.hpp"
8 8
9 #include <array> 9 #include <array>
  10 +#include <chrono>
10 #include <cstdint> 11 #include <cstdint>
11 #include <unordered_map> 12 #include <unordered_map>
12 13
@@ -850,6 +851,22 @@ TEST_F(TApp, AsSizeValue1000_1024) { @@ -850,6 +851,22 @@ TEST_F(TApp, AsSizeValue1000_1024) {
850 EXPECT_EQ(value, ki_value); 851 EXPECT_EQ(value, ki_value);
851 } 852 }
852 853
  854 +TEST_F(TApp, duration_test) {
  855 + std::chrono::seconds duration{1};
  856 +
  857 + app.option_defaults()->ignore_case();
  858 + app.add_option_function<std::size_t>(
  859 + "--duration",
  860 + [&](size_t a_value) { duration = std::chrono::seconds{a_value}; },
  861 + "valid units: sec, min, h, day.")
  862 + ->capture_default_str()
  863 + ->transform(CLI::AsNumberWithUnit(
  864 + std::map<std::string, std::size_t>{{"sec", 1}, {"min", 60}, {"h", 3600}, {"day", 24 * 3600}}));
  865 + EXPECT_NO_THROW(app.parse(std::vector<std::string>{"1 day", "--duration"}));
  866 +
  867 + EXPECT_EQ(duration, std::chrono::seconds(86400));
  868 +}
  869 +
853 TEST_F(TApp, AsSizeValue1024) { 870 TEST_F(TApp, AsSizeValue1024) {
854 std::uint64_t value{0}; 871 std::uint64_t value{0};
855 app.add_option("-s", value)->transform(CLI::AsSizeValue(false)); 872 app.add_option("-s", value)->transform(CLI::AsSizeValue(false));