Skip to content

Commit dc137f0

Browse files
add support for quotes in the config naming to match TOML standard (#967)
This PR is to further support for TOML. To allow and generate quoted names in config files including those separated by the parent separator. like ```toml "sub"."sub2".value=1 'sub'.'sub.sub'.value=2 ``` --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 91220ba commit dc137f0

File tree

5 files changed

+373
-44
lines changed

5 files changed

+373
-44
lines changed

include/CLI/Config.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ std::string ini_join(const std::vector<std::string> &args,
3737
char stringQuote = '"',
3838
char literalQuote = '\'');
3939

40+
void clean_name_string(std::string &name, const std::string &keyChars);
41+
4042
std::vector<std::string> generate_parents(const std::string &section, std::string &name, char parentSeparator);
4143

4244
/// assuming non default segments do a check on the close and open of the segments in a configItem structure

include/CLI/impl/Config_inl.hpp

Lines changed: 62 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -122,23 +122,19 @@ generate_parents(const std::string &section, std::string &name, char parentSepar
122122
std::vector<std::string> parents;
123123
if(detail::to_lower(section) != "default") {
124124
if(section.find(parentSeparator) != std::string::npos) {
125-
parents = detail::split(section, parentSeparator);
125+
parents = detail::split_up(section, parentSeparator);
126126
} else {
127127
parents = {section};
128128
}
129129
}
130130
if(name.find(parentSeparator) != std::string::npos) {
131-
std::vector<std::string> plist = detail::split(name, parentSeparator);
131+
std::vector<std::string> plist = detail::split_up(name, parentSeparator);
132132
name = plist.back();
133-
detail::remove_quotes(name);
134133
plist.pop_back();
135134
parents.insert(parents.end(), plist.begin(), plist.end());
136135
}
137-
138136
// clean up quotes on the parents
139-
for(auto &parent : parents) {
140-
detail::remove_quotes(parent);
141-
}
137+
detail::remove_quotes(parents);
142138
return parents;
143139
}
144140

@@ -218,10 +214,10 @@ inline std::vector<ConfigItem> ConfigBase::from_config(std::istream &input) cons
218214
char aSep = (isINIArray && arraySeparator == ' ') ? ',' : arraySeparator;
219215
int currentSectionIndex{0};
220216

217+
std::string line_sep_chars{parentSeparatorChar, commentChar, valueDelimiter};
221218
while(getline(input, buffer)) {
222219
std::vector<std::string> items_buffer;
223220
std::string name;
224-
bool literalName{false};
225221
line = detail::trim_copy(buffer);
226222
std::size_t len = line.length();
227223
// lines have to be at least 3 characters to have any meaning to CLI just skip the rest
@@ -275,8 +271,21 @@ inline std::vector<ConfigItem> ConfigBase::from_config(std::istream &input) cons
275271
continue;
276272
}
277273
std::size_t search_start = 0;
278-
if(line.front() == stringQuote || line.front() == literalQuote || line.front() == '`') {
279-
search_start = detail::close_sequence(line, 0, line.front());
274+
if(line.find_first_of("\"'`") != std::string::npos) {
275+
while(search_start < line.size()) {
276+
auto test_char = line[search_start];
277+
if(test_char == '\"' || test_char == '\'' || test_char == '`') {
278+
search_start = detail::close_sequence(line, search_start, line[search_start]);
279+
++search_start;
280+
} else if(test_char == valueDelimiter || test_char == commentChar) {
281+
--search_start;
282+
break;
283+
} else if(test_char == ' ' || test_char == '\t' || test_char == parentSeparatorChar) {
284+
++search_start;
285+
} else {
286+
search_start = line.find_first_of(line_sep_chars, search_start);
287+
}
288+
}
280289
}
281290
// Find = in string, split and recombine
282291
auto delimiter_pos = line.find_first_of(valueDelimiter, search_start + 1);
@@ -290,7 +299,7 @@ inline std::vector<ConfigItem> ConfigBase::from_config(std::istream &input) cons
290299
std::string item = detail::trim_copy(line.substr(delimiter_pos + 1, std::string::npos));
291300
bool mlquote =
292301
(item.compare(0, 3, multiline_literal_quote) == 0 || item.compare(0, 3, multiline_string_quote) == 0);
293-
if(!mlquote && comment_pos != std::string::npos && !literalName) {
302+
if(!mlquote && comment_pos != std::string::npos) {
294303
auto citems = detail::split_up(item, commentChar);
295304
item = detail::trim_copy(citems.front());
296305
}
@@ -365,23 +374,18 @@ inline std::vector<ConfigItem> ConfigBase::from_config(std::istream &input) cons
365374
name = detail::trim_copy(line.substr(0, comment_pos));
366375
items_buffer = {"true"};
367376
}
377+
std::vector<std::string> parents;
368378
try {
369-
literalName = detail::process_quoted_string(name, stringQuote, literalQuote);
370-
379+
parents = detail::generate_parents(currentSection, name, parentSeparatorChar);
380+
detail::process_quoted_string(name);
371381
// clean up quotes on the items and check for escaped strings
372382
for(auto &it : items_buffer) {
373383
detail::process_quoted_string(it, stringQuote, literalQuote);
374384
}
375385
} catch(const std::invalid_argument &ia) {
376386
throw CLI::ParseError(ia.what(), CLI::ExitCodes::InvalidError);
377387
}
378-
std::vector<std::string> parents;
379-
if(literalName) {
380-
std::string noname{};
381-
parents = detail::generate_parents(currentSection, noname, parentSeparatorChar);
382-
} else {
383-
parents = detail::generate_parents(currentSection, name, parentSeparatorChar);
384-
}
388+
385389
if(parents.size() > maximumLayers) {
386390
continue;
387391
}
@@ -418,6 +422,23 @@ inline std::vector<ConfigItem> ConfigBase::from_config(std::istream &input) cons
418422
return output;
419423
}
420424

425+
CLI11_INLINE std::string &clean_name_string(std::string &name, const std::string &keyChars) {
426+
if(name.find_first_of(keyChars) != std::string::npos || (name.front() == '[' && name.back() == ']') ||
427+
(name.find_first_of("'`\"\\") != std::string::npos)) {
428+
if(name.find_first_of('\'') == std::string::npos) {
429+
name.insert(0, 1, '\'');
430+
name.push_back('\'');
431+
} else {
432+
if(detail::has_escapable_character(name)) {
433+
name = detail::add_escaped_characters(name);
434+
}
435+
name.insert(0, 1, '\"');
436+
name.push_back('\"');
437+
}
438+
}
439+
return name;
440+
}
441+
421442
CLI11_INLINE std::string
422443
ConfigBase::to_config(const App *app, bool default_also, bool write_description, std::string prefix) const {
423444
std::stringstream out;
@@ -429,6 +450,14 @@ ConfigBase::to_config(const App *app, bool default_also, bool write_description,
429450
commentTest.push_back(commentChar);
430451
commentTest.push_back(parentSeparatorChar);
431452

453+
std::string keyChars = commentTest;
454+
keyChars.push_back(literalQuote);
455+
keyChars.push_back(stringQuote);
456+
keyChars.push_back(arrayStart);
457+
keyChars.push_back(arrayEnd);
458+
keyChars.push_back(valueDelimiter);
459+
keyChars.push_back(arraySeparator);
460+
432461
std::vector<std::string> groups = app->get_groups();
433462
bool defaultUsed = false;
434463
groups.insert(groups.begin(), std::string("Options"));
@@ -498,24 +527,7 @@ ConfigBase::to_config(const App *app, bool default_also, bool write_description,
498527
out << '\n';
499528
out << commentLead << detail::fix_newlines(commentLead, opt->get_description()) << '\n';
500529
}
501-
if(single_name.find_first_of(commentTest) != std::string::npos ||
502-
single_name.compare(0, 3, multiline_string_quote) == 0 ||
503-
single_name.compare(0, 3, multiline_literal_quote) == 0 ||
504-
(single_name.front() == '[' && single_name.back() == ']') ||
505-
(single_name.find_first_of(stringQuote) != std::string::npos) ||
506-
(single_name.find_first_of(literalQuote) != std::string::npos) ||
507-
(single_name.find_first_of('`') != std::string::npos)) {
508-
if(single_name.find_first_of(literalQuote) == std::string::npos) {
509-
single_name.insert(0, 1, literalQuote);
510-
single_name.push_back(literalQuote);
511-
} else {
512-
if(detail::has_escapable_character(single_name)) {
513-
single_name = detail::add_escaped_characters(single_name);
514-
}
515-
single_name.insert(0, 1, stringQuote);
516-
single_name.push_back(stringQuote);
517-
}
518-
}
530+
clean_name_string(single_name, keyChars);
519531

520532
std::string name = prefix + single_name;
521533

@@ -554,22 +566,29 @@ ConfigBase::to_config(const App *app, bool default_also, bool write_description,
554566
if(!default_also && (subcom->count_all() == 0)) {
555567
continue;
556568
}
569+
std::string subname = subcom->get_name();
570+
clean_name_string(subname, keyChars);
571+
557572
if(subcom->get_configurable() && app->got_subcommand(subcom)) {
558573
if(!prefix.empty() || app->get_parent() == nullptr) {
559-
out << '[' << prefix << subcom->get_name() << "]\n";
574+
575+
out << '[' << prefix << subname << "]\n";
560576
} else {
561-
std::string subname = app->get_name() + parentSeparatorChar + subcom->get_name();
577+
std::string appname = app->get_name();
578+
clean_name_string(appname, keyChars);
579+
subname = appname + parentSeparatorChar + subname;
562580
const auto *p = app->get_parent();
563581
while(p->get_parent() != nullptr) {
564-
subname = p->get_name() + parentSeparatorChar + subname;
582+
std::string pname = p->get_name();
583+
clean_name_string(pname, keyChars);
584+
subname = pname + parentSeparatorChar + subname;
565585
p = p->get_parent();
566586
}
567587
out << '[' << subname << "]\n";
568588
}
569589
out << to_config(subcom, default_also, write_description, "");
570590
} else {
571-
out << to_config(
572-
subcom, default_also, write_description, prefix + subcom->get_name() + parentSeparatorChar);
591+
out << to_config(subcom, default_also, write_description, prefix + subname + parentSeparatorChar);
573592
}
574593
}
575594
}

0 commit comments

Comments
 (0)