Commit 8b785a6c7d5b0af5f3ba0003d7a9e43d08d6f3be

Authored by Philip Top
Committed by GitHub
1 parent 19047d8d

feat: add some capabilities to the config parser and a stream parser (#630)

* add some capabilities to the config parser and a stream parser

* style: pre-commit.ci fixes

* add additional tests for the config parser

* additional tests of config sections and indexing

* style: pre-commit.ci fixes

* add initialization for member variables

* warning and error fixes

* add test for `parse_from_stream`

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
README.md
... ... @@ -766,7 +766,7 @@ sub.subcommand = true
766 766 Spaces before and after the name and argument are ignored. Multiple arguments are separated by spaces. One set of quotes will be removed, preserving spaces (the same way the command line works). Boolean options can be `true`, `on`, `1`, `yes`, `enable`; or `false`, `off`, `0`, `no`, `disable` (case insensitive). Sections (and `.` separated names) are treated as subcommands (note: this does not necessarily mean that subcommand was passed, it just sets the "defaults"). You cannot set positional-only arguments. Subcommands can be triggered from configuration files if the `configurable` flag was set on the subcommand. Then the use of `[subcommand]` notation will trigger a subcommand and cause it to act as if it were on the command line.
767 767  
768 768 To print a configuration file from the passed
769   -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.
  769 +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 and customization points.
770 770  
771 771 If it is desired that multiple configuration be allowed. Use
772 772  
... ...
book/chapters/config.md
... ... @@ -39,6 +39,12 @@ app.allow_config_extras(CLI::config_extras_mode::error);
39 39  
40 40 is equivalent to `app.allow_config_extras(false);`
41 41  
  42 +```cpp
  43 +app.allow_config_extras(CLI::config_extras_mode::ignore_all);
  44 +```
  45 +
  46 +will completely ignore any mismatches, extras, or other issues with the config file
  47 +
42 48 ### Getting the used configuration file name
43 49  
44 50 If it is needed to get the configuration file name used this can be obtained via
... ... @@ -118,7 +124,7 @@ if a prefix is needed to print before the options, for example to print a config
118 124  
119 125 ### Customization of configure file output
120 126  
121   -The default config parser/generator has some customization points that allow variations on the TOML format. The default formatter has a base configuration that matches the TOML format. It defines 5 characters that define how different aspects of the configuration are handled
  127 +The default config parser/generator has some customization points that allow variations on the TOML format. The default formatter has a base configuration that matches the TOML format. It defines 5 characters that define how different aspects of the configuration are handled. You must use `get_config_formatter_base()` to have access to these fields
122 128  
123 129 ```cpp
124 130 /// the character used for comments
... ... @@ -131,6 +137,18 @@ char arrayEnd = &#39;]&#39;;
131 137 char arraySeparator = ',';
132 138 /// the character used separate the name from the value
133 139 char valueDelimiter = '=';
  140 +/// the character to use around strings
  141 +char stringQuote = '"';
  142 +/// the character to use around single characters
  143 +char characterQuote = '\'';
  144 +/// the maximum number of layers to allow
  145 +uint8_t maximumLayers{255};
  146 +/// the separator used to separator parent layers
  147 +char parentSeparatorChar{'.'};
  148 +/// Specify the configuration index to use for arrayed sections
  149 +uint16_t configIndex{0};
  150 +/// Specify the configuration section that should be used
  151 +std::string configSection;
134 152 ```
135 153  
136 154 These can be modified via setter functions
... ... @@ -139,6 +157,11 @@ These can be modified via setter functions
139 157 * `ConfigBase *arrayBounds(char aStart, char aEnd)`: Specify the start and end characters for an array
140 158 * `ConfigBase *arrayDelimiter(char aSep)`: Specify the delimiter character for an array
141 159 * `ConfigBase *valueSeparator(char vSep)`: Specify the delimiter between a name and value
  160 +* `ConfigBase *quoteCharacter(char qString, char qChar)` :specify the characters to use around strings and single characters
  161 +* `ConfigBase *maxLayers(uint8_t layers)` : specify the maximum number of parent layers to process. This is useful to limit processing for larger config files
  162 +* `ConfigBase *parentSeparator(char sep)` : specify the character to separate parent layers from options
  163 +* `ConfigBase *section(const std::string &sectionName)` : specify the section name to use to get the option values, only this section will be processed
  164 +* `ConfigBase *index(uint16_t sectionIndex)` : specify an index section to use for processing if multiple TOML sections of the same name are present `[[section]]`
142 165  
143 166 For example, to specify reading a configure file that used `:` to separate name and values:
144 167  
... ... @@ -174,15 +197,37 @@ app.config_formatter(std::make_shared&lt;NewConfig&gt;());
174 197  
175 198 See [`examples/json.cpp`](https://github.com/CLIUtils/CLI11/blob/master/examples/json.cpp) for a complete JSON config example.
176 199  
  200 +### Trivial JSON configuration example
  201 +
  202 +```JSON
  203 +{
  204 + "test": 56,
  205 + "testb": "test",
  206 + "flag": true
  207 +}
  208 +```
  209 +
  210 +The parser can handle these structures with only a minor tweak
  211 +
  212 +```cpp
  213 +app.get_config_formatter_base()->valueSeparator(':');
  214 +```
  215 +
  216 +The open and close brackets must be on a separate line and the comma gets interpreted as an array separator but since no values are after the comma they get ignored as well. This will not support multiple layers or sections or any other moderately complex JSON, but can work if the input file is simple.
  217 +
177 218 ## Triggering Subcommands
178 219  
179 220 Configuration files can be used to trigger subcommands if a subcommand is set to configure. By default configuration file just set the default values of a subcommand. But if the `configure()` option is set on a subcommand then the if the subcommand is utilized via a `[subname]` block in the configuration file it will act as if it were called from the command line. Subsubcommands can be triggered via `[subname.subsubname]`. Using the `[[subname]]` will be as if the subcommand were triggered multiple times from the command line. This functionality can allow the configuration file to act as a scripting file.
180 221  
181 222 For custom configuration files this behavior can be triggered by specifying the parent subcommands in the structure and `++` as the name to open a new subcommand scope and `--` to close it. These names trigger the different callbacks of configurable subcommands.
182 223  
  224 +## Stream parsing
  225 +
  226 +In addition to the regular parse functions a `parse_from_stream(std::istream &input)` is available to directly parse a stream operator. For example to process some arguments in an already open file stream. The stream is fed directly in the config parser so bypasses the normal command line parsing.
  227 +
183 228 ## Implementation Notes
184 229  
185   -The config file input works with any form of the option given: Long, short, positional, or the environment variable name. When generating a config file it will create a name in following priority.
  230 +The config file input works with any form of the option given: Long, short, positional, or the environment variable name. When generating a config file it will create an option name in following priority.
186 231  
187 232 1. First long name
188 233 2. Positional name
... ...
include/CLI/App.hpp
... ... @@ -55,7 +55,7 @@ std::string help(const App *app, const Error &amp;e);
55 55  
56 56 /// enumeration of modes of how to deal with extras in config files
57 57  
58   -enum class config_extras_mode : char { error = 0, ignore, capture };
  58 +enum class config_extras_mode : char { error = 0, ignore, ignore_all, capture };
59 59  
60 60 class App;
61 61  
... ... @@ -1290,6 +1290,16 @@ class App {
1290 1290 run_callback();
1291 1291 }
1292 1292  
  1293 + void parse_from_stream(std::istream &input) {
  1294 + if(parsed_ == 0) {
  1295 + _validate();
  1296 + _configure();
  1297 + // set the parent as nullptr as this object should be the top now
  1298 + }
  1299 +
  1300 + _parse_stream(input);
  1301 + run_callback();
  1302 + }
1293 1303 /// Provide a function to print a help message. The function gets access to the App pointer and error.
1294 1304 void failure_message(std::function<std::string(const App *, const Error &e)> function) {
1295 1305 failure_message_ = function;
... ... @@ -2349,6 +2359,18 @@ class App {
2349 2359 _process_extras();
2350 2360 }
2351 2361  
  2362 + /// Internal function to parse a stream
  2363 + void _parse_stream(std::istream &input) {
  2364 + auto values = config_formatter_->from_config(input);
  2365 + _parse_config(values);
  2366 + increment_parsed();
  2367 + _trigger_pre_parse(values.size());
  2368 + _process();
  2369 +
  2370 + // Throw error if any items are left over (depending on settings)
  2371 + _process_extras();
  2372 + }
  2373 +
2352 2374 /// Parse one config param, return false if not found in any subcommand, remove if it is
2353 2375 ///
2354 2376 /// If this has more than one dot.separated.name, go into the subcommand matching it
... ... @@ -2409,8 +2431,12 @@ class App {
2409 2431 return false;
2410 2432 }
2411 2433  
2412   - if(!op->get_configurable())
  2434 + if(!op->get_configurable()) {
  2435 + if(get_allow_config_extras() == config_extras_mode::ignore_all) {
  2436 + return false;
  2437 + }
2413 2438 throw ConfigError::NotConfigurable(item.fullname());
  2439 + }
2414 2440  
2415 2441 if(op->empty()) {
2416 2442 // Flag parsing
... ...
include/CLI/Config.hpp
... ... @@ -94,17 +94,17 @@ inline std::string ini_join(const std::vector&lt;std::string&gt; &amp;args,
94 94 return joined;
95 95 }
96 96  
97   -inline std::vector<std::string> generate_parents(const std::string &section, std::string &name) {
  97 +inline std::vector<std::string> generate_parents(const std::string &section, std::string &name, char parentSeparator) {
98 98 std::vector<std::string> parents;
99 99 if(detail::to_lower(section) != "default") {
100   - if(section.find('.') != std::string::npos) {
101   - parents = detail::split(section, '.');
  100 + if(section.find(parentSeparator) != std::string::npos) {
  101 + parents = detail::split(section, parentSeparator);
102 102 } else {
103 103 parents = {section};
104 104 }
105 105 }
106   - if(name.find('.') != std::string::npos) {
107   - std::vector<std::string> plist = detail::split(name, '.');
  106 + if(name.find(parentSeparator) != std::string::npos) {
  107 + std::vector<std::string> plist = detail::split(name, parentSeparator);
108 108 name = plist.back();
109 109 detail::remove_quotes(name);
110 110 plist.pop_back();
... ... @@ -119,10 +119,11 @@ inline std::vector&lt;std::string&gt; generate_parents(const std::string &amp;section, std
119 119 }
120 120  
121 121 /// assuming non default segments do a check on the close and open of the segments in a configItem structure
122   -inline void checkParentSegments(std::vector<ConfigItem> &output, const std::string &currentSection) {
  122 +inline void
  123 +checkParentSegments(std::vector<ConfigItem> &output, const std::string &currentSection, char parentSeparator) {
123 124  
124 125 std::string estring;
125   - auto parents = detail::generate_parents(currentSection, estring);
  126 + auto parents = detail::generate_parents(currentSection, estring, parentSeparator);
126 127 if(!output.empty() && output.back().name == "--") {
127 128 std::size_t msize = (parents.size() > 1U) ? parents.size() : 2;
128 129 while(output.back().parents.size() >= msize) {
... ... @@ -170,43 +171,53 @@ inline void checkParentSegments(std::vector&lt;ConfigItem&gt; &amp;output, const std::stri
170 171  
171 172 inline std::vector<ConfigItem> ConfigBase::from_config(std::istream &input) const {
172 173 std::string line;
173   - std::string section = "default";
174   -
  174 + std::string currentSection = "default";
  175 + std::string previousSection = "default";
175 176 std::vector<ConfigItem> output;
176 177 bool isDefaultArray = (arrayStart == '[' && arrayEnd == ']' && arraySeparator == ',');
177 178 bool isINIArray = (arrayStart == '\0' || arrayStart == ' ') && arrayStart == arrayEnd;
  179 + bool inSection{false};
178 180 char aStart = (isINIArray) ? '[' : arrayStart;
179 181 char aEnd = (isINIArray) ? ']' : arrayEnd;
180 182 char aSep = (isINIArray && arraySeparator == ' ') ? ',' : arraySeparator;
181   -
  183 + int currentSectionIndex{0};
182 184 while(getline(input, line)) {
183 185 std::vector<std::string> items_buffer;
184 186 std::string name;
185 187  
186 188 detail::trim(line);
187 189 std::size_t len = line.length();
188   - if(len > 1 && line.front() == '[' && line.back() == ']') {
189   - if(section != "default") {
  190 + // lines have to be at least 3 characters to have any meaning to CLI just skip the rest
  191 + if(len < 3) {
  192 + continue;
  193 + }
  194 + if(line.front() == '[' && line.back() == ']') {
  195 + if(currentSection != "default") {
190 196 // insert a section end which is just an empty items_buffer
191 197 output.emplace_back();
192   - output.back().parents = detail::generate_parents(section, name);
  198 + output.back().parents = detail::generate_parents(currentSection, name, parentSeparatorChar);
193 199 output.back().name = "--";
194 200 }
195   - section = line.substr(1, len - 2);
  201 + currentSection = line.substr(1, len - 2);
196 202 // deal with double brackets for TOML
197   - if(section.size() > 1 && section.front() == '[' && section.back() == ']') {
198   - section = section.substr(1, section.size() - 2);
  203 + if(currentSection.size() > 1 && currentSection.front() == '[' && currentSection.back() == ']') {
  204 + currentSection = currentSection.substr(1, currentSection.size() - 2);
199 205 }
200   - if(detail::to_lower(section) == "default") {
201   - section = "default";
  206 + if(detail::to_lower(currentSection) == "default") {
  207 + currentSection = "default";
202 208 } else {
203   - detail::checkParentSegments(output, section);
  209 + detail::checkParentSegments(output, currentSection, parentSeparatorChar);
  210 + }
  211 + inSection = false;
  212 + if(currentSection == previousSection) {
  213 + ++currentSectionIndex;
  214 + } else {
  215 + currentSectionIndex = 0;
  216 + previousSection = currentSection;
204 217 }
205 218 continue;
206 219 }
207   - if(len == 0) {
208   - continue;
209   - }
  220 +
210 221 // comment lines
211 222 if(line.front() == ';' || line.front() == '#' || line.front() == commentChar) {
212 223 continue;
... ... @@ -217,6 +228,11 @@ inline std::vector&lt;ConfigItem&gt; ConfigBase::from_config(std::istream &amp;input) cons
217 228 if(pos != std::string::npos) {
218 229 name = detail::trim_copy(line.substr(0, pos));
219 230 std::string item = detail::trim_copy(line.substr(pos + 1));
  231 + auto cloc = item.find(commentChar);
  232 + if(cloc != std::string::npos) {
  233 + item.erase(cloc, std::string::npos);
  234 + detail::trim(item);
  235 + }
220 236 if(item.size() > 1 && item.front() == aStart) {
221 237 for(std::string multiline; item.back() != aEnd && std::getline(input, multiline);) {
222 238 detail::trim(multiline);
... ... @@ -232,9 +248,15 @@ inline std::vector&lt;ConfigItem&gt; ConfigBase::from_config(std::istream &amp;input) cons
232 248 }
233 249 } else {
234 250 name = detail::trim_copy(line);
  251 + auto cloc = name.find(commentChar);
  252 + if(cloc != std::string::npos) {
  253 + name.erase(cloc, std::string::npos);
  254 + detail::trim(name);
  255 + }
  256 +
235 257 items_buffer = {"true"};
236 258 }
237   - if(name.find('.') == std::string::npos) {
  259 + if(name.find(parentSeparatorChar) == std::string::npos) {
238 260 detail::remove_quotes(name);
239 261 }
240 262 // clean up quotes on the items
... ... @@ -242,8 +264,20 @@ inline std::vector&lt;ConfigItem&gt; ConfigBase::from_config(std::istream &amp;input) cons
242 264 detail::remove_quotes(it);
243 265 }
244 266  
245   - std::vector<std::string> parents = detail::generate_parents(section, name);
246   -
  267 + std::vector<std::string> parents = detail::generate_parents(currentSection, name, parentSeparatorChar);
  268 + if(parents.size() > maximumLayers) {
  269 + continue;
  270 + }
  271 + if(!configSection.empty() && !inSection) {
  272 + if(parents.empty() || parents.front() != configSection) {
  273 + continue;
  274 + }
  275 + if(configIndex >= 0 && currentSectionIndex != configIndex) {
  276 + continue;
  277 + }
  278 + parents.erase(parents.begin());
  279 + inSection = true;
  280 + }
247 281 if(!output.empty() && name == output.back().name && parents == output.back().parents) {
248 282 output.back().inputs.insert(output.back().inputs.end(), items_buffer.begin(), items_buffer.end());
249 283 } else {
... ... @@ -253,11 +287,11 @@ inline std::vector&lt;ConfigItem&gt; ConfigBase::from_config(std::istream &amp;input) cons
253 287 output.back().inputs = std::move(items_buffer);
254 288 }
255 289 }
256   - if(section != "default") {
  290 + if(currentSection != "default") {
257 291 // insert a section end which is just an empty items_buffer
258 292 std::string ename;
259 293 output.emplace_back();
260   - output.back().parents = detail::generate_parents(section, ename);
  294 + output.back().parents = detail::generate_parents(currentSection, ename, parentSeparatorChar);
261 295 output.back().name = "--";
262 296 while(output.back().parents.size() > 1) {
263 297 output.push_back(output.back());
... ... @@ -339,17 +373,18 @@ ConfigBase::to_config(const App *app, bool default_also, bool write_description,
339 373 if(!prefix.empty() || app->get_parent() == nullptr) {
340 374 out << '[' << prefix << subcom->get_name() << "]\n";
341 375 } else {
342   - std::string subname = app->get_name() + "." + subcom->get_name();
  376 + std::string subname = app->get_name() + parentSeparatorChar + subcom->get_name();
343 377 auto p = app->get_parent();
344 378 while(p->get_parent() != nullptr) {
345   - subname = p->get_name() + "." + subname;
  379 + subname = p->get_name() + parentSeparatorChar + subname;
346 380 p = p->get_parent();
347 381 }
348 382 out << '[' << subname << "]\n";
349 383 }
350 384 out << to_config(subcom, default_also, write_description, "");
351 385 } else {
352   - out << to_config(subcom, default_also, write_description, prefix + subcom->get_name() + ".");
  386 + out << to_config(
  387 + subcom, default_also, write_description, prefix + subcom->get_name() + parentSeparatorChar);
353 388 }
354 389 }
355 390 }
... ...
include/CLI/ConfigFwd.hpp
... ... @@ -91,6 +91,14 @@ class ConfigBase : public Config {
91 91 char stringQuote = '"';
92 92 /// the character to use around single characters
93 93 char characterQuote = '\'';
  94 + /// the maximum number of layers to allow
  95 + uint8_t maximumLayers{255};
  96 + /// the separator used to separator parent layers
  97 + char parentSeparatorChar{'.'};
  98 + /// Specify the configuration index to use for arrayed sections
  99 + int16_t configIndex{-1};
  100 + /// Specify the configuration section that should be used
  101 + std::string configSection{};
94 102  
95 103 public:
96 104 std::string
... ... @@ -124,6 +132,35 @@ class ConfigBase : public Config {
124 132 characterQuote = qChar;
125 133 return this;
126 134 }
  135 + /// Specify the maximum number of parents
  136 + ConfigBase *maxLayers(uint8_t layers) {
  137 + maximumLayers = layers;
  138 + return this;
  139 + }
  140 + /// Specify the separator to use for parent layers
  141 + ConfigBase *parentSeparator(char sep) {
  142 + parentSeparatorChar = sep;
  143 + return this;
  144 + }
  145 + /// get a reference to the configuration section
  146 + std::string &sectionRef() { return configSection; }
  147 + /// get the section
  148 + const std::string &section() const { return configSection; }
  149 + /// specify a particular section of the configuration file to use
  150 + ConfigBase *section(const std::string &sectionName) {
  151 + configSection = sectionName;
  152 + return this;
  153 + }
  154 +
  155 + /// get a reference to the configuration index
  156 + int16_t &indexRef() { return configIndex; }
  157 + /// get the section index
  158 + int16_t index() const { return configIndex; }
  159 + /// specify a particular index in the section to use (-1) for all sections to use
  160 + ConfigBase *index(int16_t sectionIndex) {
  161 + configIndex = sectionIndex;
  162 + return this;
  163 + }
127 164 };
128 165  
129 166 /// the default Config is the TOML file format
... ...
tests/ConfigFileTest.cpp
... ... @@ -798,6 +798,114 @@ TEST_CASE_METHOD(TApp, &quot;IniRequired&quot;, &quot;[config]&quot;) {
798 798 CHECK_THROWS_AS(run(), CLI::RequiredError);
799 799 }
800 800  
  801 +TEST_CASE_METHOD(TApp, "IniInlineComment", "[config]") {
  802 +
  803 + TempFile tmpini{"TestIniTmp.ini"};
  804 +
  805 + app.set_config("--config", tmpini, "", true);
  806 + app.config_formatter(std::make_shared<CLI::ConfigINI>());
  807 +
  808 + {
  809 + std::ofstream out{tmpini};
  810 + out << "[default]" << std::endl;
  811 + out << "two=99 ; this is a two" << std::endl;
  812 + out << "three=3; this is a three" << std::endl;
  813 + }
  814 +
  815 + int one{0}, two{0}, three{0};
  816 + app.add_option("--one", one)->required();
  817 + app.add_option("--two", two)->required();
  818 + app.add_option("--three", three)->required();
  819 +
  820 + args = {"--one=1"};
  821 +
  822 + run();
  823 + CHECK(1 == one);
  824 + CHECK(99 == two);
  825 + CHECK(3 == three);
  826 +
  827 + one = two = three = 0;
  828 + args = {"--one=1", "--two=2"};
  829 +
  830 + CHECK_NOTHROW(run());
  831 + CHECK(1 == one);
  832 + CHECK(2 == two);
  833 + CHECK(3 == three);
  834 +
  835 + args = {};
  836 +
  837 + CHECK_THROWS_AS(run(), CLI::RequiredError);
  838 +
  839 + args = {"--two=2"};
  840 +
  841 + CHECK_THROWS_AS(run(), CLI::RequiredError);
  842 +}
  843 +
  844 +TEST_CASE_METHOD(TApp, "TomlInlineComment", "[config]") {
  845 +
  846 + TempFile tmpini{"TestIniTmp.ini"};
  847 +
  848 + app.set_config("--config", tmpini, "", true);
  849 +
  850 + {
  851 + std::ofstream out{tmpini};
  852 + out << "[default]" << std::endl;
  853 + out << "two=99 # this is a two" << std::endl;
  854 + out << "three=3# this is a three" << std::endl;
  855 + }
  856 +
  857 + int one{0}, two{0}, three{0};
  858 + app.add_option("--one", one)->required();
  859 + app.add_option("--two", two)->required();
  860 + app.add_option("--three", three)->required();
  861 +
  862 + args = {"--one=1"};
  863 +
  864 + run();
  865 + CHECK(1 == one);
  866 + CHECK(99 == two);
  867 + CHECK(3 == three);
  868 +
  869 + one = two = three = 0;
  870 + args = {"--one=1", "--two=2"};
  871 +
  872 + CHECK_NOTHROW(run());
  873 + CHECK(1 == one);
  874 + CHECK(2 == two);
  875 + CHECK(3 == three);
  876 +
  877 + args = {};
  878 +
  879 + CHECK_THROWS_AS(run(), CLI::RequiredError);
  880 +
  881 + args = {"--two=2"};
  882 +
  883 + CHECK_THROWS_AS(run(), CLI::RequiredError);
  884 +}
  885 +
  886 +TEST_CASE_METHOD(TApp, "ConfigModifiers", "[config]") {
  887 +
  888 + app.set_config("--config", "test.ini", "", true);
  889 +
  890 + auto cfgptr = app.get_config_formatter_base();
  891 +
  892 + cfgptr->section("test");
  893 + CHECK(cfgptr->section() == "test");
  894 +
  895 + CHECK(cfgptr->sectionRef() == "test");
  896 + auto &sref = cfgptr->sectionRef();
  897 + sref = "this";
  898 + CHECK(cfgptr->section() == "this");
  899 +
  900 + cfgptr->index(5);
  901 + CHECK(cfgptr->index() == 5);
  902 +
  903 + CHECK(cfgptr->indexRef() == 5);
  904 + auto &iref = cfgptr->indexRef();
  905 + iref = 7;
  906 + CHECK(cfgptr->index() == 7);
  907 +}
  908 +
801 909 TEST_CASE_METHOD(TApp, "IniVector", "[config]") {
802 910  
803 911 TempFile tmpini{"TestIniTmp.ini"};
... ... @@ -1003,6 +1111,39 @@ TEST_CASE_METHOD(TApp, &quot;IniLayered&quot;, &quot;[config]&quot;) {
1003 1111 CHECK(!*subcom);
1004 1112 }
1005 1113  
  1114 +TEST_CASE_METHOD(TApp, "IniLayeredStream", "[config]") {
  1115 +
  1116 + TempFile tmpini{"TestIniTmp.ini"};
  1117 +
  1118 + app.set_config("--config", tmpini);
  1119 +
  1120 + {
  1121 + std::ofstream out{tmpini};
  1122 + out << "[default]" << std::endl;
  1123 + out << "val=1" << std::endl;
  1124 + out << "[subcom]" << std::endl;
  1125 + out << "val=2" << std::endl;
  1126 + out << "subsubcom.val=3" << std::endl;
  1127 + }
  1128 +
  1129 + int one{0}, two{0}, three{0};
  1130 + app.add_option("--val", one);
  1131 + auto subcom = app.add_subcommand("subcom");
  1132 + subcom->add_option("--val", two);
  1133 + auto subsubcom = subcom->add_subcommand("subsubcom");
  1134 + subsubcom->add_option("--val", three);
  1135 +
  1136 + std::ifstream in{tmpini};
  1137 + app.parse_from_stream(in);
  1138 +
  1139 + CHECK(one == 1);
  1140 + CHECK(two == 2);
  1141 + CHECK(three == 3);
  1142 +
  1143 + CHECK(0U == subcom->count());
  1144 + CHECK(!*subcom);
  1145 +}
  1146 +
1006 1147 TEST_CASE_METHOD(TApp, "IniLayeredDotSection", "[config]") {
1007 1148  
1008 1149 TempFile tmpini{"TestIniTmp.ini"};
... ... @@ -1034,6 +1175,45 @@ TEST_CASE_METHOD(TApp, &quot;IniLayeredDotSection&quot;, &quot;[config]&quot;) {
1034 1175  
1035 1176 CHECK(0U == subcom->count());
1036 1177 CHECK(!*subcom);
  1178 +
  1179 + three = 0;
  1180 + // check maxlayers
  1181 + app.get_config_formatter_base()->maxLayers(1);
  1182 + run();
  1183 + CHECK(three == 0);
  1184 +}
  1185 +
  1186 +TEST_CASE_METHOD(TApp, "IniLayeredCustomSectionSeparator", "[config]") {
  1187 +
  1188 + TempFile tmpini{"TestIniTmp.ini"};
  1189 +
  1190 + app.set_config("--config", tmpini);
  1191 +
  1192 + {
  1193 + std::ofstream out{tmpini};
  1194 + out << "[default]" << std::endl;
  1195 + out << "val=1" << std::endl;
  1196 + out << "[subcom]" << std::endl;
  1197 + out << "val=2" << std::endl;
  1198 + out << "[subcom|subsubcom]" << std::endl;
  1199 + out << "val=3" << std::endl;
  1200 + }
  1201 + app.get_config_formatter_base()->parentSeparator('|');
  1202 + int one{0}, two{0}, three{0};
  1203 + app.add_option("--val", one);
  1204 + auto subcom = app.add_subcommand("subcom");
  1205 + subcom->add_option("--val", two);
  1206 + auto subsubcom = subcom->add_subcommand("subsubcom");
  1207 + subsubcom->add_option("--val", three);
  1208 +
  1209 + run();
  1210 +
  1211 + CHECK(one == 1);
  1212 + CHECK(two == 2);
  1213 + CHECK(three == 3);
  1214 +
  1215 + CHECK(0U == subcom->count());
  1216 + CHECK(!*subcom);
1037 1217 }
1038 1218  
1039 1219 TEST_CASE_METHOD(TApp, "IniSubcommandConfigurable", "[config]") {
... ... @@ -1111,6 +1291,130 @@ TEST_CASE_METHOD(TApp, &quot;IniSubcommandConfigurablePreParse&quot;, &quot;[config]&quot;) {
1111 1291 CHECK(0U == subcom2->count());
1112 1292 }
1113 1293  
  1294 +TEST_CASE_METHOD(TApp, "IniSection", "[config]") {
  1295 +
  1296 + TempFile tmpini{"TestIniTmp.ini"};
  1297 +
  1298 + app.set_config("--config", tmpini);
  1299 + app.get_config_formatter_base()->section("config");
  1300 +
  1301 + {
  1302 + std::ofstream out{tmpini};
  1303 + out << "[config]" << std::endl;
  1304 + out << "val=2" << std::endl;
  1305 + out << "subsubcom.val=3" << std::endl;
  1306 + out << "[default]" << std::endl;
  1307 + out << "val=1" << std::endl;
  1308 + }
  1309 +
  1310 + int val{0};
  1311 + app.add_option("--val", val);
  1312 +
  1313 + run();
  1314 +
  1315 + CHECK(2 == val);
  1316 +}
  1317 +
  1318 +TEST_CASE_METHOD(TApp, "IniSection2", "[config]") {
  1319 +
  1320 + TempFile tmpini{"TestIniTmp.ini"};
  1321 +
  1322 + app.set_config("--config", tmpini);
  1323 + app.get_config_formatter_base()->section("config");
  1324 +
  1325 + {
  1326 + std::ofstream out{tmpini};
  1327 + out << "[default]" << std::endl;
  1328 + out << "val=1" << std::endl;
  1329 + out << "[config]" << std::endl;
  1330 + out << "val=2" << std::endl;
  1331 + out << "subsubcom.val=3" << std::endl;
  1332 + }
  1333 +
  1334 + int val{0};
  1335 + app.add_option("--val", val);
  1336 +
  1337 + run();
  1338 +
  1339 + CHECK(2 == val);
  1340 +}
  1341 +
  1342 +TEST_CASE_METHOD(TApp, "jsonLikeParsing", "[config]") {
  1343 +
  1344 + TempFile tmpjson{"TestJsonTmp.json"};
  1345 +
  1346 + app.set_config("--config", tmpjson);
  1347 + app.get_config_formatter_base()->valueSeparator(':');
  1348 +
  1349 + {
  1350 + std::ofstream out{tmpjson};
  1351 + out << "{" << std::endl;
  1352 + out << "\"val\":1," << std::endl;
  1353 + out << "\"val2\":\"test\"," << std::endl;
  1354 + out << "\"flag\":true" << std::endl;
  1355 + out << "}" << std::endl;
  1356 + }
  1357 +
  1358 + int val{0};
  1359 + app.add_option("--val", val);
  1360 + std::string val2{0};
  1361 + app.add_option("--val2", val2);
  1362 +
  1363 + bool flag{false};
  1364 + app.add_flag("--flag", flag);
  1365 +
  1366 + run();
  1367 +
  1368 + CHECK(1 == val);
  1369 + CHECK(val2 == "test");
  1370 + CHECK(flag);
  1371 +}
  1372 +
  1373 +TEST_CASE_METHOD(TApp, "TomlSectionNumber", "[config]") {
  1374 +
  1375 + TempFile tmpini{"TestTomlTmp.toml"};
  1376 +
  1377 + app.set_config("--config", tmpini);
  1378 + app.get_config_formatter_base()->section("config")->index(0);
  1379 +
  1380 + {
  1381 + std::ofstream out{tmpini};
  1382 + out << "[default]" << std::endl;
  1383 + out << "val=1" << std::endl;
  1384 + out << "[[config]]" << std::endl;
  1385 + out << "val=2" << std::endl;
  1386 + out << "subsubcom.val=3" << std::endl;
  1387 + out << "[[config]]" << std::endl;
  1388 + out << "val=4" << std::endl;
  1389 + out << "subsubcom.val=3" << std::endl;
  1390 + out << "[[config]]" << std::endl;
  1391 + out << "val=6" << std::endl;
  1392 + out << "subsubcom.val=3" << std::endl;
  1393 + }
  1394 +
  1395 + int val{0};
  1396 + app.add_option("--val", val);
  1397 +
  1398 + run();
  1399 +
  1400 + CHECK(2 == val);
  1401 +
  1402 + auto &index = app.get_config_formatter_base()->indexRef();
  1403 + index = 1;
  1404 + run();
  1405 +
  1406 + CHECK(4 == val);
  1407 +
  1408 + index = -1;
  1409 + run();
  1410 + // Take the first section in this case
  1411 + CHECK(2 == val);
  1412 + index = 2;
  1413 + run();
  1414 +
  1415 + CHECK(6 == val);
  1416 +}
  1417 +
1114 1418 TEST_CASE_METHOD(TApp, "IniSubcommandConfigurableParseComplete", "[config]") {
1115 1419  
1116 1420 TempFile tmpini{"TestIniTmp.ini"};
... ... @@ -2308,6 +2612,21 @@ TEST_CASE_METHOD(TApp, &quot;IniOutputSubcom&quot;, &quot;[config]&quot;) {
2308 2612 CHECK_THAT(str, Contains("other.newer=true"));
2309 2613 }
2310 2614  
  2615 +TEST_CASE_METHOD(TApp, "IniOutputSubcomCustomSep", "[config]") {
  2616 +
  2617 + app.add_flag("--simple");
  2618 + auto subcom = app.add_subcommand("other");
  2619 + subcom->add_flag("--newer");
  2620 + app.config_formatter(std::make_shared<CLI::ConfigINI>());
  2621 + app.get_config_formatter_base()->parentSeparator(':');
  2622 + args = {"--simple", "other", "--newer"};
  2623 + run();
  2624 +
  2625 + std::string str = app.config_to_str();
  2626 + CHECK_THAT(str, Contains("simple=true"));
  2627 + CHECK_THAT(str, Contains("other:newer=true"));
  2628 +}
  2629 +
2311 2630 TEST_CASE_METHOD(TApp, "IniOutputSubcomConfigurable", "[config]") {
2312 2631  
2313 2632 app.add_flag("--simple");
... ... @@ -2341,6 +2660,24 @@ TEST_CASE_METHOD(TApp, &quot;IniOutputSubsubcom&quot;, &quot;[config]&quot;) {
2341 2660 CHECK_THAT(str, Contains("other.sub2.newest=true"));
2342 2661 }
2343 2662  
  2663 +TEST_CASE_METHOD(TApp, "IniOutputSubsubcomCustomSep", "[config]") {
  2664 +
  2665 + app.add_flag("--simple");
  2666 + auto subcom = app.add_subcommand("other");
  2667 + subcom->add_flag("--newer");
  2668 + auto subsubcom = subcom->add_subcommand("sub2");
  2669 + subsubcom->add_flag("--newest");
  2670 + app.config_formatter(std::make_shared<CLI::ConfigINI>());
  2671 + app.get_config_formatter_base()->parentSeparator('|');
  2672 + args = {"--simple", "other", "--newer", "sub2", "--newest"};
  2673 + run();
  2674 +
  2675 + std::string str = app.config_to_str();
  2676 + CHECK_THAT(str, Contains("simple=true"));
  2677 + CHECK_THAT(str, Contains("other|newer=true"));
  2678 + CHECK_THAT(str, Contains("other|sub2|newest=true"));
  2679 +}
  2680 +
2344 2681 TEST_CASE_METHOD(TApp, "IniOutputSubsubcomConfigurable", "[config]") {
2345 2682  
2346 2683 app.add_flag("--simple");
... ...