From 1b979b8234f526003693349f6654470e4b527db0 Mon Sep 17 00:00:00 2001 From: Philip Top Date: Mon, 2 Aug 2021 07:31:36 -0700 Subject: [PATCH 1/8] add some capabilities to the config parser and a stream parser --- include/CLI/App.hpp | 30 ++++++++++++++++++++++++++++-- include/CLI/Config.hpp | 21 ++++++++++++++++++++- include/CLI/ConfigFwd.hpp | 30 ++++++++++++++++++++++++++++++ tests/ConfigFileTest.cpp | 24 ++++++++++++++++++++++++ 4 files changed, 102 insertions(+), 3 deletions(-) diff --git a/include/CLI/App.hpp b/include/CLI/App.hpp index b445e49d4..5da1bdf8b 100644 --- a/include/CLI/App.hpp +++ b/include/CLI/App.hpp @@ -55,7 +55,7 @@ std::string help(const App *app, const Error &e); /// enumeration of modes of how to deal with extras in config files -enum class config_extras_mode : char { error = 0, ignore, capture }; +enum class config_extras_mode : char { error = 0, ignore, ignore_all, capture }; class App; @@ -1301,6 +1301,16 @@ class App { run_callback(); } + void parse_from_stream(std::istream &input) { + if(parsed_ == 0) { + _validate(); + _configure(); + // set the parent as nullptr as this object should be the top now + } + + _parse_stream(input); + run_callback(); + } /// Provide a function to print a help message. The function gets access to the App pointer and error. void failure_message(std::function function) { failure_message_ = function; @@ -2360,6 +2370,18 @@ class App { _process_extras(); } + /// Internal function to parse a stream + void _parse_stream(std::istream &input) { + auto values = config_formatter_->from_config(input); + _parse_config(values); + increment_parsed(); + _trigger_pre_parse(values.size()); + _process(); + + // Throw error if any items are left over (depending on settings) + _process_extras(); + } + /// Parse one config param, return false if not found in any subcommand, remove if it is /// /// If this has more than one dot.separated.name, go into the subcommand matching it @@ -2420,8 +2442,12 @@ class App { return false; } - if(!op->get_configurable()) + if(!op->get_configurable()) { + if(get_allow_config_extras() == config_extras_mode::ignore_all) { + return false; + } throw ConfigError::NotConfigurable(item.fullname()); + } if(op->empty()) { // Flag parsing diff --git a/include/CLI/Config.hpp b/include/CLI/Config.hpp index 654c9f205..0ef7a8d55 100644 --- a/include/CLI/Config.hpp +++ b/include/CLI/Config.hpp @@ -217,6 +217,11 @@ inline std::vector ConfigBase::from_config(std::istream &input) cons if(pos != std::string::npos) { name = detail::trim_copy(line.substr(0, pos)); std::string item = detail::trim_copy(line.substr(pos + 1)); + auto cloc = item.find(commentChar); + if(cloc != std::string::npos) { + item.erase(cloc, std::string::npos); + detail::trim(item); + } if(item.size() > 1 && item.front() == aStart) { for(std::string multiline; item.back() != aEnd && std::getline(input, multiline);) { detail::trim(multiline); @@ -232,6 +237,12 @@ inline std::vector ConfigBase::from_config(std::istream &input) cons } } else { name = detail::trim_copy(line); + auto cloc = name.find(commentChar); + if(cloc != std::string::npos) { + name.erase(cloc, std::string::npos); + detail::trim(name); + } + items_buffer = {"true"}; } if(name.find('.') == std::string::npos) { @@ -243,7 +254,15 @@ inline std::vector ConfigBase::from_config(std::istream &input) cons } std::vector parents = detail::generate_parents(section, name); - + if(parents.size() > maxLayers_) { + continue; + } + if(!configSection.empty()) { + if(parents.empty() || parents.front() != configSection) { + continue; + } + parents.erase(parents.begin()); + } if(!output.empty() && name == output.back().name && parents == output.back().parents) { output.back().inputs.insert(output.back().inputs.end(), items_buffer.begin(), items_buffer.end()); } else { diff --git a/include/CLI/ConfigFwd.hpp b/include/CLI/ConfigFwd.hpp index 35eed52da..596c23572 100644 --- a/include/CLI/ConfigFwd.hpp +++ b/include/CLI/ConfigFwd.hpp @@ -91,6 +91,12 @@ class ConfigBase : public Config { char stringQuote = '"'; /// the character to use around single characters char characterQuote = '\''; + /// the maximum number of layers to allow + uint8_t maxLayers_{255}; + /// Specify the configuration index to use for arrayed sections + uint16_t configIndex{0}; + /// Specify the configuration section that should be used + std::string configSection; public: std::string @@ -124,6 +130,30 @@ class ConfigBase : public Config { characterQuote = qChar; return this; } + /// Specify the maximum number of parents + ConfigBase *maxLayers(uint8_t layers) { + maxLayers_ = layers; + return this; + } + /// get a reference to the configuration section + std::string §ionRef() { return configSection; } + /// get the section + const std::string& section() const { return configSection; } + /// specify a particular section of the configuration file to use + ConfigBase *section(const std::string §ionName) { + configSection = sectionName; + return this; + } + + /// get a reference to the configuration index + uint16_t& indexRef() { return configIndex; } + /// get the section index + uint16_t index() const { return configIndex; } + /// specify a particular index in the section to use + ConfigBase *index(uint16_t sectionIndex) { + configIndex = sectionIndex; + return this; + } }; /// the default Config is the TOML file format diff --git a/tests/ConfigFileTest.cpp b/tests/ConfigFileTest.cpp index 9c1381533..25c115c7a 100644 --- a/tests/ConfigFileTest.cpp +++ b/tests/ConfigFileTest.cpp @@ -1111,6 +1111,30 @@ TEST_CASE_METHOD(TApp, "IniSubcommandConfigurablePreParse", "[config]") { CHECK(0U == subcom2->count()); } +TEST_CASE_METHOD(TApp, "IniSection", "[config]") { + + TempFile tmpini{ "TestIniTmp.ini" }; + + app.set_config("--config", tmpini); + app.get_config_formatter_base()->section("config"); + + { + std::ofstream out{ tmpini }; + out << "[default]" << std::endl; + out << "val=1" << std::endl; + out << "[config]" << std::endl; + out << "val=2" << std::endl; + out << "subsubcom.val=3" << std::endl; + } + + int val{ 0 }; + app.add_option("--val", val); + + run(); + + CHECK(2==val); + +} TEST_CASE_METHOD(TApp, "IniSubcommandConfigurableParseComplete", "[config]") { TempFile tmpini{"TestIniTmp.ini"}; From 8ed5f500bcdb9450368a0319ce9d2ff87b8d078e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 2 Aug 2021 17:19:32 +0000 Subject: [PATCH 2/8] style: pre-commit.ci fixes --- tests/ConfigFileTest.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ConfigFileTest.cpp b/tests/ConfigFileTest.cpp index 25c115c7a..bbf1a2c14 100644 --- a/tests/ConfigFileTest.cpp +++ b/tests/ConfigFileTest.cpp @@ -1133,7 +1133,7 @@ TEST_CASE_METHOD(TApp, "IniSection", "[config]") { run(); CHECK(2==val); - + } TEST_CASE_METHOD(TApp, "IniSubcommandConfigurableParseComplete", "[config]") { From 425786a5a52ac8a94dac27ae55265298ea8e6a22 Mon Sep 17 00:00:00 2001 From: Philip Top Date: Tue, 3 Aug 2021 17:35:12 -0700 Subject: [PATCH 3/8] add additional tests for the config parser --- include/CLI/Config.hpp | 22 +++--- include/CLI/ConfigFwd.hpp | 13 +++- tests/ConfigFileTest.cpp | 156 +++++++++++++++++++++++++++++++++++++- 3 files changed, 176 insertions(+), 15 deletions(-) diff --git a/include/CLI/Config.hpp b/include/CLI/Config.hpp index 0ef7a8d55..02b6d2e1e 100644 --- a/include/CLI/Config.hpp +++ b/include/CLI/Config.hpp @@ -94,17 +94,17 @@ inline std::string ini_join(const std::vector &args, return joined; } -inline std::vector generate_parents(const std::string §ion, std::string &name) { +inline std::vector generate_parents(const std::string §ion, std::string &name, char parentSeparator) { std::vector parents; if(detail::to_lower(section) != "default") { - if(section.find('.') != std::string::npos) { - parents = detail::split(section, '.'); + if(section.find(parentSeparator) != std::string::npos) { + parents = detail::split(section, parentSeparator); } else { parents = {section}; } } if(name.find('.') != std::string::npos) { - std::vector plist = detail::split(name, '.'); + std::vector plist = detail::split(name, parentSeparator); name = plist.back(); detail::remove_quotes(name); plist.pop_back(); @@ -119,10 +119,10 @@ inline std::vector generate_parents(const std::string §ion, std } /// assuming non default segments do a check on the close and open of the segments in a configItem structure -inline void checkParentSegments(std::vector &output, const std::string ¤tSection) { +inline void checkParentSegments(std::vector &output, const std::string ¤tSection, char parentSeparator) { std::string estring; - auto parents = detail::generate_parents(currentSection, estring); + auto parents = detail::generate_parents(currentSection, estring,parentSeparator); if(!output.empty() && output.back().name == "--") { std::size_t msize = (parents.size() > 1U) ? parents.size() : 2; while(output.back().parents.size() >= msize) { @@ -189,7 +189,7 @@ inline std::vector ConfigBase::from_config(std::istream &input) cons if(section != "default") { // insert a section end which is just an empty items_buffer output.emplace_back(); - output.back().parents = detail::generate_parents(section, name); + output.back().parents = detail::generate_parents(section, name,parentSeparatorChar); output.back().name = "--"; } section = line.substr(1, len - 2); @@ -200,7 +200,7 @@ inline std::vector ConfigBase::from_config(std::istream &input) cons if(detail::to_lower(section) == "default") { section = "default"; } else { - detail::checkParentSegments(output, section); + detail::checkParentSegments(output, section,parentSeparatorChar); } continue; } @@ -253,8 +253,8 @@ inline std::vector ConfigBase::from_config(std::istream &input) cons detail::remove_quotes(it); } - std::vector parents = detail::generate_parents(section, name); - if(parents.size() > maxLayers_) { + std::vector parents = detail::generate_parents(section, name,parentSeparatorChar); + if(parents.size() > maximumLayers) { continue; } if(!configSection.empty()) { @@ -276,7 +276,7 @@ inline std::vector ConfigBase::from_config(std::istream &input) cons // insert a section end which is just an empty items_buffer std::string ename; output.emplace_back(); - output.back().parents = detail::generate_parents(section, ename); + output.back().parents = detail::generate_parents(section, ename,parentSeparatorChar); output.back().name = "--"; while(output.back().parents.size() > 1) { output.push_back(output.back()); diff --git a/include/CLI/ConfigFwd.hpp b/include/CLI/ConfigFwd.hpp index 596c23572..2e51bc2d4 100644 --- a/include/CLI/ConfigFwd.hpp +++ b/include/CLI/ConfigFwd.hpp @@ -92,7 +92,9 @@ class ConfigBase : public Config { /// the character to use around single characters char characterQuote = '\''; /// the maximum number of layers to allow - uint8_t maxLayers_{255}; + uint8_t maximumLayers{255}; + /// the separator used to separator parent layers + char parentSeparatorChar{'.'}; /// Specify the configuration index to use for arrayed sections uint16_t configIndex{0}; /// Specify the configuration section that should be used @@ -132,7 +134,12 @@ class ConfigBase : public Config { } /// Specify the maximum number of parents ConfigBase *maxLayers(uint8_t layers) { - maxLayers_ = layers; + maximumLayers = layers; + return this; + } + /// Specify the separator to use for parent layers + ConfigBase *parentSeparator(char sep) { + parentSeparatorChar = sep; return this; } /// get a reference to the configuration section @@ -144,6 +151,8 @@ class ConfigBase : public Config { configSection = sectionName; return this; } + // The section index is more for extended parsers like JSON, it is does not have + // an effect on the current TOML/INI parser /// get a reference to the configuration index uint16_t& indexRef() { return configIndex; } diff --git a/tests/ConfigFileTest.cpp b/tests/ConfigFileTest.cpp index bbf1a2c14..57611dff7 100644 --- a/tests/ConfigFileTest.cpp +++ b/tests/ConfigFileTest.cpp @@ -208,6 +208,7 @@ TEST_CASE("StringBased: Spaces", "[config]") { CHECK(output.at(1).inputs.at(0) == "four"); } + TEST_CASE("StringBased: Sections", "[config]") { std::stringstream ofile; @@ -798,6 +799,117 @@ TEST_CASE_METHOD(TApp, "IniRequired", "[config]") { CHECK_THROWS_AS(run(), CLI::RequiredError); } + +TEST_CASE_METHOD(TApp, "IniInlineComment", "[config]") { + + TempFile tmpini{"TestIniTmp.ini"}; + + app.set_config("--config", tmpini, "", true); + app.config_formatter(std::make_shared()); + + { + std::ofstream out{tmpini}; + out << "[default]" << std::endl; + out << "two=99 ; this is a two" << std::endl; + out << "three=3; this is a three" << std::endl; + } + + int one{0}, two{0}, three{0}; + app.add_option("--one", one)->required(); + app.add_option("--two", two)->required(); + app.add_option("--three", three)->required(); + + args = {"--one=1"}; + + run(); + CHECK(1 == one); + CHECK(99 == two); + CHECK(3 == three); + + one = two = three = 0; + args = {"--one=1", "--two=2"}; + + CHECK_NOTHROW(run()); + CHECK(1 == one); + CHECK(2 == two); + CHECK(3 == three); + + args = {}; + + CHECK_THROWS_AS(run(), CLI::RequiredError); + + args = {"--two=2"}; + + CHECK_THROWS_AS(run(), CLI::RequiredError); +} + +TEST_CASE_METHOD(TApp, "TomlInlineComment", "[config]") { + + TempFile tmpini{"TestIniTmp.ini"}; + + app.set_config("--config", tmpini, "", true); + + { + std::ofstream out{tmpini}; + out << "[default]" << std::endl; + out << "two=99 # this is a two" << std::endl; + out << "three=3# this is a three" << std::endl; + } + + int one{0}, two{0}, three{0}; + app.add_option("--one", one)->required(); + app.add_option("--two", two)->required(); + app.add_option("--three", three)->required(); + + args = {"--one=1"}; + + run(); + CHECK(1 == one); + CHECK(99 == two); + CHECK(3 == three); + + one = two = three = 0; + args = {"--one=1", "--two=2"}; + + CHECK_NOTHROW(run()); + CHECK(1 == one); + CHECK(2 == two); + CHECK(3 == three); + + args = {}; + + CHECK_THROWS_AS(run(), CLI::RequiredError); + + args = {"--two=2"}; + + CHECK_THROWS_AS(run(), CLI::RequiredError); +} + + +TEST_CASE_METHOD(TApp, "ConfigModifiers", "[config]") { + + + app.set_config("--config", "test.ini", "", true); + + auto cfgptr = app.get_config_formatter_base(); + + cfgptr->section("test"); + CHECK(cfgptr->section()=="test"); + + CHECK(cfgptr->sectionRef() == "test"); + auto &sref = cfgptr->sectionRef(); + sref= "this"; + CHECK(cfgptr->section() == "this"); + + cfgptr->index(5); + CHECK(cfgptr->index() == 5); + + CHECK(cfgptr->indexRef() == 5); + auto &iref = cfgptr->indexRef(); + iref = 7; + CHECK(cfgptr->index() == 7); +} + TEST_CASE_METHOD(TApp, "IniVector", "[config]") { TempFile tmpini{"TestIniTmp.ini"}; @@ -1034,6 +1146,46 @@ TEST_CASE_METHOD(TApp, "IniLayeredDotSection", "[config]") { CHECK(0U == subcom->count()); CHECK(!*subcom); + + three = 0; + // check maxlayers + app.get_config_formatter_base()->maxLayers(1); + run(); + CHECK(three == 0); + +} + +TEST_CASE_METHOD(TApp, "IniLayeredCustomSectionSeparator", "[config]") { + + TempFile tmpini{"TestIniTmp.ini"}; + + app.set_config("--config", tmpini); + + { + std::ofstream out{tmpini}; + out << "[default]" << std::endl; + out << "val=1" << std::endl; + out << "[subcom]" << std::endl; + out << "val=2" << std::endl; + out << "[subcom|subsubcom]" << std::endl; + out << "val=3" << std::endl; + } + app.get_config_formatter_base()->parentSeparator('|'); + int one{0}, two{0}, three{0}; + app.add_option("--val", one); + auto subcom = app.add_subcommand("subcom"); + subcom->add_option("--val", two); + auto subsubcom = subcom->add_subcommand("subsubcom"); + subsubcom->add_option("--val", three); + + run(); + + CHECK(one == 1); + CHECK(two == 2); + CHECK(three == 3); + + CHECK(0U == subcom->count()); + CHECK(!*subcom); } TEST_CASE_METHOD(TApp, "IniSubcommandConfigurable", "[config]") { @@ -1120,11 +1272,11 @@ TEST_CASE_METHOD(TApp, "IniSection", "[config]") { { std::ofstream out{ tmpini }; - out << "[default]" << std::endl; - out << "val=1" << std::endl; out << "[config]" << std::endl; out << "val=2" << std::endl; out << "subsubcom.val=3" << std::endl; + out << "[default]" << std::endl; + out << "val=1" << std::endl; } int val{ 0 }; From 33047a2c51658ea9841852726516058532302352 Mon Sep 17 00:00:00 2001 From: Philip Top Date: Wed, 4 Aug 2021 09:47:25 -0700 Subject: [PATCH 4/8] additional tests of config sections and indexing --- README.md | 2 +- book/chapters/config.md | 48 +++++++++++++- include/CLI/Config.hpp | 52 ++++++++++----- include/CLI/ConfigFwd.hpp | 12 ++-- tests/ConfigFileTest.cpp | 135 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 221 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 3fb84f7e8..2c31768ee 100644 --- a/README.md +++ b/README.md @@ -766,7 +766,7 @@ sub.subcommand = true 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. 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. +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. If it is desired that multiple configuration be allowed. Use diff --git a/book/chapters/config.md b/book/chapters/config.md index a94fc75f6..d755833a3 100644 --- a/book/chapters/config.md +++ b/book/chapters/config.md @@ -39,6 +39,12 @@ app.allow_config_extras(CLI::config_extras_mode::error); is equivalent to `app.allow_config_extras(false);` +```cpp +app.allow_config_extras(CLI::config_extras_mode::ignore_all); +``` + +will completely ignore any mismatches, extras, or other issues with the config file + ### Getting the used configuration file name 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 ### Customization of configure file output -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 +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 ```cpp /// the character used for comments @@ -131,6 +137,18 @@ char arrayEnd = ']'; char arraySeparator = ','; /// the character used separate the name from the value char valueDelimiter = '='; +/// the character to use around strings +char stringQuote = '"'; +/// the character to use around single characters +char characterQuote = '\''; +/// the maximum number of layers to allow +uint8_t maximumLayers{255}; +/// the separator used to separator parent layers +char parentSeparatorChar{'.'}; +/// Specify the configuration index to use for arrayed sections +uint16_t configIndex{0}; +/// Specify the configuration section that should be used +std::string configSection; ``` These can be modified via setter functions @@ -139,6 +157,12 @@ These can be modified via setter functions * `ConfigBase *arrayBounds(char aStart, char aEnd)`: Specify the start and end characters for an array * `ConfigBase *arrayDelimiter(char aSep)`: Specify the delimiter character for an array * `ConfigBase *valueSeparator(char vSep)`: Specify the delimiter between a name and value +* `ConfigBase *quoteCharacter(char qString, char qChar)` :specify the characters to use around strings and single characters +* `ConfigBase *maxLayers(uint8_t layers)` : specify the maximum number of parent layers to process. This is useful to limit processing for larger config files +* `ConfigBase *parentSeparator(char sep)` : specify the character to separate parent layers from options +* `ConfigBase *section(const std::string §ionName)` : specify the section name to use to get the option values, only this section will be processed +* `ConfigBase *index(uint16_t sectionIndex)` : specify an index section to use for processing if multiple TOML sections of the same name are present `[[section]]` + For example, to specify reading a configure file that used `:` to separate name and values: @@ -174,15 +198,35 @@ app.config_formatter(std::make_shared()); See [`examples/json.cpp`](https://github.com/CLIUtils/CLI11/blob/master/examples/json.cpp) for a complete JSON config example. +#### Trivial JSON configuration example + +```JSON +{ + "test": 56, + "testb": "test", + "flag": true +} +``` +The parser can handle these structures with only a minor tweak +```cpp +app.get_config_formatter_base()->valueSeparator(':'); +``` +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. + + ## Triggering Subcommands 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. 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. +## Stream parsing + +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. + ## Implementation Notes -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. +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. 1. First long name 2. Positional name diff --git a/include/CLI/Config.hpp b/include/CLI/Config.hpp index 02b6d2e1e..6106cb0bc 100644 --- a/include/CLI/Config.hpp +++ b/include/CLI/Config.hpp @@ -103,7 +103,7 @@ inline std::vector generate_parents(const std::string §ion, std parents = {section}; } } - if(name.find('.') != std::string::npos) { + if(name.find(parentSeparator) != std::string::npos) { std::vector plist = detail::split(name, parentSeparator); name = plist.back(); detail::remove_quotes(name); @@ -119,10 +119,11 @@ inline std::vector generate_parents(const std::string §ion, std } /// assuming non default segments do a check on the close and open of the segments in a configItem structure -inline void checkParentSegments(std::vector &output, const std::string ¤tSection, char parentSeparator) { +inline void +checkParentSegments(std::vector &output, const std::string ¤tSection, char parentSeparator) { std::string estring; - auto parents = detail::generate_parents(currentSection, estring,parentSeparator); + auto parents = detail::generate_parents(currentSection, estring, parentSeparator); if(!output.empty() && output.back().name == "--") { std::size_t msize = (parents.size() > 1U) ? parents.size() : 2; while(output.back().parents.size() >= msize) { @@ -171,25 +172,30 @@ inline void checkParentSegments(std::vector &output, const std::stri inline std::vector ConfigBase::from_config(std::istream &input) const { std::string line; std::string section = "default"; - + std::string previousSection = "default"; std::vector output; bool isDefaultArray = (arrayStart == '[' && arrayEnd == ']' && arraySeparator == ','); bool isINIArray = (arrayStart == '\0' || arrayStart == ' ') && arrayStart == arrayEnd; + bool inSection{false}; char aStart = (isINIArray) ? '[' : arrayStart; char aEnd = (isINIArray) ? ']' : arrayEnd; char aSep = (isINIArray && arraySeparator == ' ') ? ',' : arraySeparator; - + int currentSectionIndex{0}; while(getline(input, line)) { std::vector items_buffer; std::string name; detail::trim(line); std::size_t len = line.length(); - if(len > 1 && line.front() == '[' && line.back() == ']') { + // lines have to be at least 3 characters to have any meaning to CLI just skip the rest + if(len < 3) { + continue; + } + if(line.front() == '[' && line.back() == ']') { if(section != "default") { // insert a section end which is just an empty items_buffer output.emplace_back(); - output.back().parents = detail::generate_parents(section, name,parentSeparatorChar); + output.back().parents = detail::generate_parents(section, name, parentSeparatorChar); output.back().name = "--"; } section = line.substr(1, len - 2); @@ -200,13 +206,18 @@ inline std::vector ConfigBase::from_config(std::istream &input) cons if(detail::to_lower(section) == "default") { section = "default"; } else { - detail::checkParentSegments(output, section,parentSeparatorChar); + detail::checkParentSegments(output, section, parentSeparatorChar); + } + inSection = false; + if(section == previousSection) { + ++currentSectionIndex; + } else { + currentSectionIndex = 0; + previousSection = section; } continue; } - if(len == 0) { - continue; - } + // comment lines if(line.front() == ';' || line.front() == '#' || line.front() == commentChar) { continue; @@ -245,7 +256,7 @@ inline std::vector ConfigBase::from_config(std::istream &input) cons items_buffer = {"true"}; } - if(name.find('.') == std::string::npos) { + if(name.find(parentSeparatorChar) == std::string::npos) { detail::remove_quotes(name); } // clean up quotes on the items @@ -253,15 +264,19 @@ inline std::vector ConfigBase::from_config(std::istream &input) cons detail::remove_quotes(it); } - std::vector parents = detail::generate_parents(section, name,parentSeparatorChar); + std::vector parents = detail::generate_parents(section, name, parentSeparatorChar); if(parents.size() > maximumLayers) { continue; } - if(!configSection.empty()) { + if(!configSection.empty() && !inSection) { if(parents.empty() || parents.front() != configSection) { continue; } + if(configIndex >= 0 && currentSectionIndex != configIndex) { + continue; + } parents.erase(parents.begin()); + inSection = true; } if(!output.empty() && name == output.back().name && parents == output.back().parents) { output.back().inputs.insert(output.back().inputs.end(), items_buffer.begin(), items_buffer.end()); @@ -276,7 +291,7 @@ inline std::vector ConfigBase::from_config(std::istream &input) cons // insert a section end which is just an empty items_buffer std::string ename; output.emplace_back(); - output.back().parents = detail::generate_parents(section, ename,parentSeparatorChar); + output.back().parents = detail::generate_parents(section, ename, parentSeparatorChar); output.back().name = "--"; while(output.back().parents.size() > 1) { output.push_back(output.back()); @@ -358,17 +373,18 @@ ConfigBase::to_config(const App *app, bool default_also, bool write_description, if(!prefix.empty() || app->get_parent() == nullptr) { out << '[' << prefix << subcom->get_name() << "]\n"; } else { - std::string subname = app->get_name() + "." + subcom->get_name(); + std::string subname = app->get_name() + parentSeparatorChar + subcom->get_name(); auto p = app->get_parent(); while(p->get_parent() != nullptr) { - subname = p->get_name() + "." + subname; + subname = p->get_name() + parentSeparatorChar + subname; p = p->get_parent(); } out << '[' << subname << "]\n"; } out << to_config(subcom, default_also, write_description, ""); } else { - out << to_config(subcom, default_also, write_description, prefix + subcom->get_name() + "."); + out << to_config( + subcom, default_also, write_description, prefix + subcom->get_name() + parentSeparatorChar); } } } diff --git a/include/CLI/ConfigFwd.hpp b/include/CLI/ConfigFwd.hpp index 2e51bc2d4..4d8ba9c48 100644 --- a/include/CLI/ConfigFwd.hpp +++ b/include/CLI/ConfigFwd.hpp @@ -96,7 +96,7 @@ class ConfigBase : public Config { /// the separator used to separator parent layers char parentSeparatorChar{'.'}; /// Specify the configuration index to use for arrayed sections - uint16_t configIndex{0}; + int16_t configIndex{-1}; /// Specify the configuration section that should be used std::string configSection; @@ -151,15 +151,13 @@ class ConfigBase : public Config { configSection = sectionName; return this; } - // The section index is more for extended parsers like JSON, it is does not have - // an effect on the current TOML/INI parser /// get a reference to the configuration index - uint16_t& indexRef() { return configIndex; } + int16_t& indexRef() { return configIndex; } /// get the section index - uint16_t index() const { return configIndex; } - /// specify a particular index in the section to use - ConfigBase *index(uint16_t sectionIndex) { + int16_t index() const { return configIndex; } + /// specify a particular index in the section to use (-1) for all sections to use + ConfigBase *index(int16_t sectionIndex) { configIndex = sectionIndex; return this; } diff --git a/tests/ConfigFileTest.cpp b/tests/ConfigFileTest.cpp index 57611dff7..c43fb937b 100644 --- a/tests/ConfigFileTest.cpp +++ b/tests/ConfigFileTest.cpp @@ -1287,6 +1287,108 @@ TEST_CASE_METHOD(TApp, "IniSection", "[config]") { CHECK(2==val); } + +TEST_CASE_METHOD(TApp, "IniSection2", "[config]") { + + TempFile tmpini{"TestIniTmp.ini"}; + + app.set_config("--config", tmpini); + app.get_config_formatter_base()->section("config"); + + { + std::ofstream out{tmpini}; + out << "[default]" << std::endl; + out << "val=1" << std::endl; + out << "[config]" << std::endl; + out << "val=2" << std::endl; + out << "subsubcom.val=3" << std::endl; + } + + int val{0}; + app.add_option("--val", val); + + run(); + + CHECK(2 == val); +} + +TEST_CASE_METHOD(TApp, "jsonLikeParsing", "[config]") { + + TempFile tmpjson{"TestJsonTmp.json"}; + + app.set_config("--config", tmpjson); + app.get_config_formatter_base()->valueSeparator(':'); + + { + std::ofstream out{tmpjson}; + out << "{" << std::endl; + out << "\"val\":1," << std::endl; + out << "\"val2\":\"test\"," << std::endl; + out << "\"flag\":true" << std::endl; + out << "}" << std::endl; + } + + int val{0}; + app.add_option("--val", val); + std::string val2{0}; + app.add_option("--val2", val2); + + bool flag{false}; + app.add_flag("--flag", flag); + + run(); + + CHECK(1 == val); + CHECK(val2 == "test"); + CHECK(flag); +} + +TEST_CASE_METHOD(TApp, "TomlSectionNumber", "[config]") { + + TempFile tmpini{"TestTomlTmp.toml"}; + + app.set_config("--config", tmpini); + app.get_config_formatter_base()->section("config")->index(0); + + { + std::ofstream out{tmpini}; + out << "[default]" << std::endl; + out << "val=1" << std::endl; + out << "[[config]]" << std::endl; + out << "val=2" << std::endl; + out << "subsubcom.val=3" << std::endl; + out << "[[config]]" << std::endl; + out << "val=4" << std::endl; + out << "subsubcom.val=3" << std::endl; + out << "[[config]]" << std::endl; + out << "val=6" << std::endl; + out << "subsubcom.val=3" << std::endl; + } + + int val{0}; + app.add_option("--val", val); + + run(); + + CHECK(2 == val); + + auto &index=app.get_config_formatter_base()->indexRef(); + index = 1; + run(); + + CHECK(4 == val); + + index = -1; + run(); + //Take the first section in this case + CHECK(2 == val); + index = 2; + run(); + + CHECK(6 == val); +} + + TEST_CASE_METHOD(TApp, "IniSubcommandConfigurableParseComplete", "[config]") { TempFile tmpini{"TestIniTmp.ini"}; @@ -2484,6 +2586,21 @@ TEST_CASE_METHOD(TApp, "IniOutputSubcom", "[config]") { CHECK_THAT(str, Contains("other.newer=true")); } +TEST_CASE_METHOD(TApp, "IniOutputSubcomCustomSep", "[config]") { + + app.add_flag("--simple"); + auto subcom = app.add_subcommand("other"); + subcom->add_flag("--newer"); + app.config_formatter(std::make_shared()); + app.get_config_formatter_base()->parentSeparator(':'); + args = {"--simple", "other", "--newer"}; + run(); + + std::string str = app.config_to_str(); + CHECK_THAT(str, Contains("simple=true")); + CHECK_THAT(str, Contains("other:newer=true")); +} + TEST_CASE_METHOD(TApp, "IniOutputSubcomConfigurable", "[config]") { app.add_flag("--simple"); @@ -2517,6 +2634,24 @@ TEST_CASE_METHOD(TApp, "IniOutputSubsubcom", "[config]") { CHECK_THAT(str, Contains("other.sub2.newest=true")); } +TEST_CASE_METHOD(TApp, "IniOutputSubsubcomCustomSep", "[config]") { + + app.add_flag("--simple"); + auto subcom = app.add_subcommand("other"); + subcom->add_flag("--newer"); + auto subsubcom = subcom->add_subcommand("sub2"); + subsubcom->add_flag("--newest"); + app.config_formatter(std::make_shared()); + app.get_config_formatter_base()->parentSeparator('|'); + args = {"--simple", "other", "--newer", "sub2", "--newest"}; + run(); + + std::string str = app.config_to_str(); + CHECK_THAT(str, Contains("simple=true")); + CHECK_THAT(str, Contains("other|newer=true")); + CHECK_THAT(str, Contains("other|sub2|newest=true")); +} + TEST_CASE_METHOD(TApp, "IniOutputSubsubcomConfigurable", "[config]") { app.add_flag("--simple"); From cd380edd25dc74a0e779d9c9b567bada4de3bb8e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 4 Aug 2021 16:48:00 +0000 Subject: [PATCH 5/8] style: pre-commit.ci fixes --- book/chapters/config.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/book/chapters/config.md b/book/chapters/config.md index d755833a3..39385062e 100644 --- a/book/chapters/config.md +++ b/book/chapters/config.md @@ -211,7 +211,7 @@ The parser can handle these structures with only a minor tweak ```cpp app.get_config_formatter_base()->valueSeparator(':'); ``` -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. +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. ## Triggering Subcommands @@ -222,7 +222,7 @@ For custom configuration files this behavior can be triggered by specifying the ## Stream parsing -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. +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. ## Implementation Notes From 396e3506cb3b59adf3dc6f0d7cc0be027d41084f Mon Sep 17 00:00:00 2001 From: Philip Top Date: Wed, 4 Aug 2021 10:22:06 -0700 Subject: [PATCH 6/8] add initialization for member variables --- include/CLI/ConfigFwd.hpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/include/CLI/ConfigFwd.hpp b/include/CLI/ConfigFwd.hpp index 4d8ba9c48..ef2ac345f 100644 --- a/include/CLI/ConfigFwd.hpp +++ b/include/CLI/ConfigFwd.hpp @@ -98,7 +98,7 @@ class ConfigBase : public Config { /// Specify the configuration index to use for arrayed sections int16_t configIndex{-1}; /// Specify the configuration section that should be used - std::string configSection; + std::string configSection{}; public: std::string @@ -145,7 +145,7 @@ class ConfigBase : public Config { /// get a reference to the configuration section std::string §ionRef() { return configSection; } /// get the section - const std::string& section() const { return configSection; } + const std::string §ion() const { return configSection; } /// specify a particular section of the configuration file to use ConfigBase *section(const std::string §ionName) { configSection = sectionName; @@ -153,7 +153,7 @@ class ConfigBase : public Config { } /// get a reference to the configuration index - int16_t& indexRef() { return configIndex; } + int16_t &indexRef() { return configIndex; } /// get the section index int16_t index() const { return configIndex; } /// specify a particular index in the section to use (-1) for all sections to use From e6c878c5fe4a1342a51e9bcf4bc741207b805757 Mon Sep 17 00:00:00 2001 From: Philip Top Date: Wed, 4 Aug 2021 11:06:39 -0700 Subject: [PATCH 7/8] warning and error fixes --- book/chapters/config.md | 13 ++++++------ include/CLI/Config.hpp | 28 +++++++++++++------------- tests/ConfigFileTest.cpp | 43 +++++++++++++++++----------------------- 3 files changed, 39 insertions(+), 45 deletions(-) diff --git a/book/chapters/config.md b/book/chapters/config.md index 39385062e..df004ce75 100644 --- a/book/chapters/config.md +++ b/book/chapters/config.md @@ -163,7 +163,6 @@ These can be modified via setter functions * `ConfigBase *section(const std::string §ionName)` : specify the section name to use to get the option values, only this section will be processed * `ConfigBase *index(uint16_t sectionIndex)` : specify an index section to use for processing if multiple TOML sections of the same name are present `[[section]]` - For example, to specify reading a configure file that used `:` to separate name and values: ```cpp @@ -198,21 +197,23 @@ app.config_formatter(std::make_shared()); See [`examples/json.cpp`](https://github.com/CLIUtils/CLI11/blob/master/examples/json.cpp) for a complete JSON config example. -#### Trivial JSON configuration example +### Trivial JSON configuration example ```JSON { - "test": 56, - "testb": "test", - "flag": true + "test": 56, + "testb": "test", + "flag": true } ``` + The parser can handle these structures with only a minor tweak + ```cpp app.get_config_formatter_base()->valueSeparator(':'); ``` -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. +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. ## Triggering Subcommands diff --git a/include/CLI/Config.hpp b/include/CLI/Config.hpp index 6106cb0bc..57944ecae 100644 --- a/include/CLI/Config.hpp +++ b/include/CLI/Config.hpp @@ -171,7 +171,7 @@ checkParentSegments(std::vector &output, const std::string ¤tS inline std::vector ConfigBase::from_config(std::istream &input) const { std::string line; - std::string section = "default"; + std::string currentSection = "default"; std::string previousSection = "default"; std::vector output; bool isDefaultArray = (arrayStart == '[' && arrayEnd == ']' && arraySeparator == ','); @@ -192,28 +192,28 @@ inline std::vector ConfigBase::from_config(std::istream &input) cons continue; } if(line.front() == '[' && line.back() == ']') { - if(section != "default") { + if(currentSection != "default") { // insert a section end which is just an empty items_buffer output.emplace_back(); - output.back().parents = detail::generate_parents(section, name, parentSeparatorChar); + output.back().parents = detail::generate_parents(currentSection, name, parentSeparatorChar); output.back().name = "--"; } - section = line.substr(1, len - 2); + currentSection = line.substr(1, len - 2); // deal with double brackets for TOML - if(section.size() > 1 && section.front() == '[' && section.back() == ']') { - section = section.substr(1, section.size() - 2); + if(currentSection.size() > 1 && currentSection.front() == '[' && currentSection.back() == ']') { + currentSection = currentSection.substr(1, currentSection.size() - 2); } - if(detail::to_lower(section) == "default") { - section = "default"; + if(detail::to_lower(currentSection) == "default") { + currentSection = "default"; } else { - detail::checkParentSegments(output, section, parentSeparatorChar); + detail::checkParentSegments(output, currentSection, parentSeparatorChar); } inSection = false; - if(section == previousSection) { + if(currentSection == previousSection) { ++currentSectionIndex; } else { currentSectionIndex = 0; - previousSection = section; + previousSection = currentSection; } continue; } @@ -264,7 +264,7 @@ inline std::vector ConfigBase::from_config(std::istream &input) cons detail::remove_quotes(it); } - std::vector parents = detail::generate_parents(section, name, parentSeparatorChar); + std::vector parents = detail::generate_parents(currentSection, name, parentSeparatorChar); if(parents.size() > maximumLayers) { continue; } @@ -287,11 +287,11 @@ inline std::vector ConfigBase::from_config(std::istream &input) cons output.back().inputs = std::move(items_buffer); } } - if(section != "default") { + if(currentSection != "default") { // insert a section end which is just an empty items_buffer std::string ename; output.emplace_back(); - output.back().parents = detail::generate_parents(section, ename, parentSeparatorChar); + output.back().parents = detail::generate_parents(currentSection, ename, parentSeparatorChar); output.back().name = "--"; while(output.back().parents.size() > 1) { output.push_back(output.back()); diff --git a/tests/ConfigFileTest.cpp b/tests/ConfigFileTest.cpp index c43fb937b..1115371b9 100644 --- a/tests/ConfigFileTest.cpp +++ b/tests/ConfigFileTest.cpp @@ -208,7 +208,6 @@ TEST_CASE("StringBased: Spaces", "[config]") { CHECK(output.at(1).inputs.at(0) == "four"); } - TEST_CASE("StringBased: Sections", "[config]") { std::stringstream ofile; @@ -799,7 +798,6 @@ TEST_CASE_METHOD(TApp, "IniRequired", "[config]") { CHECK_THROWS_AS(run(), CLI::RequiredError); } - TEST_CASE_METHOD(TApp, "IniInlineComment", "[config]") { TempFile tmpini{"TestIniTmp.ini"}; @@ -885,29 +883,27 @@ TEST_CASE_METHOD(TApp, "TomlInlineComment", "[config]") { CHECK_THROWS_AS(run(), CLI::RequiredError); } - TEST_CASE_METHOD(TApp, "ConfigModifiers", "[config]") { - app.set_config("--config", "test.ini", "", true); - auto cfgptr = app.get_config_formatter_base(); + auto cfgptr = app.get_config_formatter_base(); - cfgptr->section("test"); - CHECK(cfgptr->section()=="test"); + cfgptr->section("test"); + CHECK(cfgptr->section() == "test"); - CHECK(cfgptr->sectionRef() == "test"); - auto &sref = cfgptr->sectionRef(); - sref= "this"; - CHECK(cfgptr->section() == "this"); + CHECK(cfgptr->sectionRef() == "test"); + auto &sref = cfgptr->sectionRef(); + sref = "this"; + CHECK(cfgptr->section() == "this"); cfgptr->index(5); - CHECK(cfgptr->index() == 5); + CHECK(cfgptr->index() == 5); - CHECK(cfgptr->indexRef() == 5); - auto &iref = cfgptr->indexRef(); - iref = 7; - CHECK(cfgptr->index() == 7); + CHECK(cfgptr->indexRef() == 5); + auto &iref = cfgptr->indexRef(); + iref = 7; + CHECK(cfgptr->index() == 7); } TEST_CASE_METHOD(TApp, "IniVector", "[config]") { @@ -1152,7 +1148,6 @@ TEST_CASE_METHOD(TApp, "IniLayeredDotSection", "[config]") { app.get_config_formatter_base()->maxLayers(1); run(); CHECK(three == 0); - } TEST_CASE_METHOD(TApp, "IniLayeredCustomSectionSeparator", "[config]") { @@ -1265,13 +1260,13 @@ TEST_CASE_METHOD(TApp, "IniSubcommandConfigurablePreParse", "[config]") { TEST_CASE_METHOD(TApp, "IniSection", "[config]") { - TempFile tmpini{ "TestIniTmp.ini" }; + TempFile tmpini{"TestIniTmp.ini"}; app.set_config("--config", tmpini); app.get_config_formatter_base()->section("config"); { - std::ofstream out{ tmpini }; + std::ofstream out{tmpini}; out << "[config]" << std::endl; out << "val=2" << std::endl; out << "subsubcom.val=3" << std::endl; @@ -1279,13 +1274,12 @@ TEST_CASE_METHOD(TApp, "IniSection", "[config]") { out << "val=1" << std::endl; } - int val{ 0 }; + int val{0}; app.add_option("--val", val); run(); - CHECK(2==val); - + CHECK(2 == val); } TEST_CASE_METHOD(TApp, "IniSection2", "[config]") { @@ -1372,7 +1366,7 @@ TEST_CASE_METHOD(TApp, "TomlSectionNumber", "[config]") { CHECK(2 == val); - auto &index=app.get_config_formatter_base()->indexRef(); + auto &index = app.get_config_formatter_base()->indexRef(); index = 1; run(); @@ -1380,7 +1374,7 @@ TEST_CASE_METHOD(TApp, "TomlSectionNumber", "[config]") { index = -1; run(); - //Take the first section in this case + // Take the first section in this case CHECK(2 == val); index = 2; run(); @@ -1388,7 +1382,6 @@ TEST_CASE_METHOD(TApp, "TomlSectionNumber", "[config]") { CHECK(6 == val); } - TEST_CASE_METHOD(TApp, "IniSubcommandConfigurableParseComplete", "[config]") { TempFile tmpini{"TestIniTmp.ini"}; From 8796d7b2281f00fb1dd521c1a216b9c53138a4c8 Mon Sep 17 00:00:00 2001 From: Philip Top Date: Wed, 4 Aug 2021 12:30:44 -0700 Subject: [PATCH 8/8] add test for `parse_from_stream` --- tests/ConfigFileTest.cpp | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/ConfigFileTest.cpp b/tests/ConfigFileTest.cpp index 1115371b9..61c066eed 100644 --- a/tests/ConfigFileTest.cpp +++ b/tests/ConfigFileTest.cpp @@ -1111,6 +1111,39 @@ TEST_CASE_METHOD(TApp, "IniLayered", "[config]") { CHECK(!*subcom); } +TEST_CASE_METHOD(TApp, "IniLayeredStream", "[config]") { + + TempFile tmpini{"TestIniTmp.ini"}; + + app.set_config("--config", tmpini); + + { + std::ofstream out{tmpini}; + out << "[default]" << std::endl; + out << "val=1" << std::endl; + out << "[subcom]" << std::endl; + out << "val=2" << std::endl; + out << "subsubcom.val=3" << std::endl; + } + + int one{0}, two{0}, three{0}; + app.add_option("--val", one); + auto subcom = app.add_subcommand("subcom"); + subcom->add_option("--val", two); + auto subsubcom = subcom->add_subcommand("subsubcom"); + subsubcom->add_option("--val", three); + + std::ifstream in{tmpini}; + app.parse_from_stream(in); + + CHECK(one == 1); + CHECK(two == 2); + CHECK(three == 3); + + CHECK(0U == subcom->count()); + CHECK(!*subcom); +} + TEST_CASE_METHOD(TApp, "IniLayeredDotSection", "[config]") { TempFile tmpini{"TestIniTmp.ini"};