Skip to content

Commit 8b785a6

Browse files
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>
1 parent 19047d8 commit 8b785a6

File tree

6 files changed

+515
-35
lines changed

6 files changed

+515
-35
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -766,7 +766,7 @@ sub.subcommand = true
766766
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.
767767

768768
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.
770770

771771
If it is desired that multiple configuration be allowed. Use
772772

book/chapters/config.md

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ app.allow_config_extras(CLI::config_extras_mode::error);
3939

4040
is equivalent to `app.allow_config_extras(false);`
4141

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+
4248
### Getting the used configuration file name
4349

4450
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
118124
119125
### Customization of configure file output
120126
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
122128
123129
```cpp
124130
/// the character used for comments
@@ -131,6 +137,18 @@ char arrayEnd = ']';
131137
char arraySeparator = ',';
132138
/// the character used separate the name from the value
133139
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;
134152
```
135153

136154
These can be modified via setter functions
@@ -139,6 +157,11 @@ These can be modified via setter functions
139157
* `ConfigBase *arrayBounds(char aStart, char aEnd)`: Specify the start and end characters for an array
140158
* `ConfigBase *arrayDelimiter(char aSep)`: Specify the delimiter character for an array
141159
* `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]]`
142165

143166
For example, to specify reading a configure file that used `:` to separate name and values:
144167

@@ -174,15 +197,37 @@ app.config_formatter(std::make_shared<NewConfig>());
174197

175198
See [`examples/json.cpp`](https://github.com/CLIUtils/CLI11/blob/master/examples/json.cpp) for a complete JSON config example.
176199

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+
177218
## Triggering Subcommands
178219
179220
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.
180221
181222
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.
182223
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+
183228
## Implementation Notes
184229
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.
186231
187232
1. First long name
188233
2. Positional name

include/CLI/App.hpp

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ std::string help(const App *app, const Error &e);
5555

5656
/// enumeration of modes of how to deal with extras in config files
5757

58-
enum class config_extras_mode : char { error = 0, ignore, capture };
58+
enum class config_extras_mode : char { error = 0, ignore, ignore_all, capture };
5959

6060
class App;
6161

@@ -1290,6 +1290,16 @@ class App {
12901290
run_callback();
12911291
}
12921292

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+
}
12931303
/// Provide a function to print a help message. The function gets access to the App pointer and error.
12941304
void failure_message(std::function<std::string(const App *, const Error &e)> function) {
12951305
failure_message_ = function;
@@ -2349,6 +2359,18 @@ class App {
23492359
_process_extras();
23502360
}
23512361

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+
23522374
/// Parse one config param, return false if not found in any subcommand, remove if it is
23532375
///
23542376
/// If this has more than one dot.separated.name, go into the subcommand matching it
@@ -2409,8 +2431,12 @@ class App {
24092431
return false;
24102432
}
24112433

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+
}
24132438
throw ConfigError::NotConfigurable(item.fullname());
2439+
}
24142440

24152441
if(op->empty()) {
24162442
// Flag parsing

include/CLI/Config.hpp

Lines changed: 65 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -94,17 +94,17 @@ inline std::string ini_join(const std::vector<std::string> &args,
9494
return joined;
9595
}
9696

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) {
9898
std::vector<std::string> parents;
9999
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);
102102
} else {
103103
parents = {section};
104104
}
105105
}
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);
108108
name = plist.back();
109109
detail::remove_quotes(name);
110110
plist.pop_back();
@@ -119,10 +119,11 @@ inline std::vector<std::string> generate_parents(const std::string &section, std
119119
}
120120

121121
/// 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) {
123124

124125
std::string estring;
125-
auto parents = detail::generate_parents(currentSection, estring);
126+
auto parents = detail::generate_parents(currentSection, estring, parentSeparator);
126127
if(!output.empty() && output.back().name == "--") {
127128
std::size_t msize = (parents.size() > 1U) ? parents.size() : 2;
128129
while(output.back().parents.size() >= msize) {
@@ -170,43 +171,53 @@ inline void checkParentSegments(std::vector<ConfigItem> &output, const std::stri
170171

171172
inline std::vector<ConfigItem> ConfigBase::from_config(std::istream &input) const {
172173
std::string line;
173-
std::string section = "default";
174-
174+
std::string currentSection = "default";
175+
std::string previousSection = "default";
175176
std::vector<ConfigItem> output;
176177
bool isDefaultArray = (arrayStart == '[' && arrayEnd == ']' && arraySeparator == ',');
177178
bool isINIArray = (arrayStart == '\0' || arrayStart == ' ') && arrayStart == arrayEnd;
179+
bool inSection{false};
178180
char aStart = (isINIArray) ? '[' : arrayStart;
179181
char aEnd = (isINIArray) ? ']' : arrayEnd;
180182
char aSep = (isINIArray && arraySeparator == ' ') ? ',' : arraySeparator;
181-
183+
int currentSectionIndex{0};
182184
while(getline(input, line)) {
183185
std::vector<std::string> items_buffer;
184186
std::string name;
185187

186188
detail::trim(line);
187189
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") {
190196
// insert a section end which is just an empty items_buffer
191197
output.emplace_back();
192-
output.back().parents = detail::generate_parents(section, name);
198+
output.back().parents = detail::generate_parents(currentSection, name, parentSeparatorChar);
193199
output.back().name = "--";
194200
}
195-
section = line.substr(1, len - 2);
201+
currentSection = line.substr(1, len - 2);
196202
// 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);
199205
}
200-
if(detail::to_lower(section) == "default") {
201-
section = "default";
206+
if(detail::to_lower(currentSection) == "default") {
207+
currentSection = "default";
202208
} 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;
204217
}
205218
continue;
206219
}
207-
if(len == 0) {
208-
continue;
209-
}
220+
210221
// comment lines
211222
if(line.front() == ';' || line.front() == '#' || line.front() == commentChar) {
212223
continue;
@@ -217,6 +228,11 @@ inline std::vector<ConfigItem> ConfigBase::from_config(std::istream &input) cons
217228
if(pos != std::string::npos) {
218229
name = detail::trim_copy(line.substr(0, pos));
219230
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+
}
220236
if(item.size() > 1 && item.front() == aStart) {
221237
for(std::string multiline; item.back() != aEnd && std::getline(input, multiline);) {
222238
detail::trim(multiline);
@@ -232,18 +248,36 @@ inline std::vector<ConfigItem> ConfigBase::from_config(std::istream &input) cons
232248
}
233249
} else {
234250
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+
235257
items_buffer = {"true"};
236258
}
237-
if(name.find('.') == std::string::npos) {
259+
if(name.find(parentSeparatorChar) == std::string::npos) {
238260
detail::remove_quotes(name);
239261
}
240262
// clean up quotes on the items
241263
for(auto &it : items_buffer) {
242264
detail::remove_quotes(it);
243265
}
244266

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+
}
247281
if(!output.empty() && name == output.back().name && parents == output.back().parents) {
248282
output.back().inputs.insert(output.back().inputs.end(), items_buffer.begin(), items_buffer.end());
249283
} else {
@@ -253,11 +287,11 @@ inline std::vector<ConfigItem> ConfigBase::from_config(std::istream &input) cons
253287
output.back().inputs = std::move(items_buffer);
254288
}
255289
}
256-
if(section != "default") {
290+
if(currentSection != "default") {
257291
// insert a section end which is just an empty items_buffer
258292
std::string ename;
259293
output.emplace_back();
260-
output.back().parents = detail::generate_parents(section, ename);
294+
output.back().parents = detail::generate_parents(currentSection, ename, parentSeparatorChar);
261295
output.back().name = "--";
262296
while(output.back().parents.size() > 1) {
263297
output.push_back(output.back());
@@ -339,17 +373,18 @@ ConfigBase::to_config(const App *app, bool default_also, bool write_description,
339373
if(!prefix.empty() || app->get_parent() == nullptr) {
340374
out << '[' << prefix << subcom->get_name() << "]\n";
341375
} else {
342-
std::string subname = app->get_name() + "." + subcom->get_name();
376+
std::string subname = app->get_name() + parentSeparatorChar + subcom->get_name();
343377
auto p = app->get_parent();
344378
while(p->get_parent() != nullptr) {
345-
subname = p->get_name() + "." + subname;
379+
subname = p->get_name() + parentSeparatorChar + subname;
346380
p = p->get_parent();
347381
}
348382
out << '[' << subname << "]\n";
349383
}
350384
out << to_config(subcom, default_also, write_description, "");
351385
} 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);
353388
}
354389
}
355390
}

0 commit comments

Comments
 (0)