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 748 To print a configuration file from the passed
749 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 759 ### Inheriting defaults
752 760  
753 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 78  
79 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 91 ## Writing out a configure file
82 92  
83 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 2053 void _process_config_file() {
2054 2054 if(config_ptr_ != nullptr) {
2055 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 2059 if(config_required) {
2060 2060 throw FileError::Missing("no specified config file");
2061 2061 }
2062 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 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 677 TEST_F(TApp, IniRequiredNotFound) {
595 678  
596 679 std::string noini = "TestIniNotExist.ini";
... ...
tests/TransformTest.cpp
... ... @@ -7,6 +7,7 @@
7 7 #include "app_helper.hpp"
8 8  
9 9 #include <array>
  10 +#include <chrono>
10 11 #include <cstdint>
11 12 #include <unordered_map>
12 13  
... ... @@ -850,6 +851,22 @@ TEST_F(TApp, AsSizeValue1000_1024) {
850 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 870 TEST_F(TApp, AsSizeValue1024) {
854 871 std::uint64_t value{0};
855 872 app.add_option("-s", value)->transform(CLI::AsSizeValue(false));
... ...