Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multiline string config #935

Merged
merged 1 commit into from
Oct 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1210,9 +1210,10 @@ file will be read if it exists, and does not throw an error unless `required` is
`true`. Configuration files are in [TOML][] format by default, though the
default reader can also accept files in INI format as well. It should be noted
that CLI11 does not contain a full TOML parser but can read strings from most
TOML file and run them through the CLI11 parser. Other formats can be added by
an adept user, some variations are available through customization points in the
default formatter. An example of a TOML file:
TOML files, including multi-line strings 🚧, and run them through the CLI11
parser. Other formats can be added by an adept user, some variations are
available through customization points in the default formatter. An example of a
TOML file:

```toml
# Comments are supported, using a #
Expand Down
43 changes: 42 additions & 1 deletion book/chapters/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ getting the last `N` files given.
Here is an example configuration file, in
[TOML](https://github.com/toml-lang/toml) format:

```ini
```toml
# Comments are supported, using a #
# The default section is [default], case insensitive

Expand Down Expand Up @@ -165,6 +165,47 @@ The main differences are in vector notation and comment character. Note: CLI11
is not a full TOML parser as it just reads values as strings. It is possible
(but not recommended) to mix notation.

### Multi-line strings

The default config file parser supports multi-line strings like the toml
standard [TOML](https://toml.io/en/). It also supports multiline comments like
python doc strings.

```toml
"""
this is a multine
comment
"""

""" this is also
a multiline comment"""

''' and so is
this
'''

value = 1
str = """
this is a multiline string value
the first \n is removed and so is the last
"""

str2 = ''' this is also a mu-
ltiline value '''

str3 = """\
a line continuation \
will skip \
all white space between the '\' \
and the next non-whitespace character \
making this into a single line
"""

```

The key is that the closing of the multiline string must be at the end of a line
and match the starting 3 quote sequence.

## Multiple configuration files

If it is desired that multiple configuration be allowed. Use
Expand Down
91 changes: 87 additions & 4 deletions include/CLI/impl/Config_inl.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
namespace CLI {
// [CLI11:config_inl_hpp:verbatim]

static constexpr auto tquote = R"(""")";

namespace detail {

CLI11_INLINE std::string convert_arg_for_ini(const std::string &arg, char stringQuote, char characterQuote) {
Expand Down Expand Up @@ -59,6 +61,9 @@
}
}
}
if(arg.find_first_of('\n') != std::string::npos) {
return std::string(tquote) + arg + tquote;
}
if(arg.find_first_of(stringQuote) == std::string::npos) {
return std::string(1, stringQuote) + arg + stringQuote;
}
Expand Down Expand Up @@ -164,30 +169,57 @@
output.back().parents = std::move(parents);
output.back().name = "++";
}

CLI11_INLINE bool hasMLString(std::string const &fullString, char check) {
if(fullString.length() < 3) {
return false;
}
auto it = fullString.rbegin();
return (*it == check) && (*(it + 1) == check) && (*(it + 2) == check);
}
} // namespace detail

inline std::vector<ConfigItem> ConfigBase::from_config(std::istream &input) const {
std::string line;
std::string buffer;
std::string currentSection = "default";
std::string previousSection = "default";
std::vector<ConfigItem> output;
bool isDefaultArray = (arrayStart == '[' && arrayEnd == ']' && arraySeparator == ',');
bool isINIArray = (arrayStart == '\0' || arrayStart == ' ') && arrayStart == arrayEnd;
bool inSection{false};
bool inMLineComment{false};
bool inMLineValue{false};
char aStart = (isINIArray) ? '[' : arrayStart;
char aEnd = (isINIArray) ? ']' : arrayEnd;
char aSep = (isINIArray && arraySeparator == ' ') ? ',' : arraySeparator;
int currentSectionIndex{0};
while(getline(input, line)) {

while(getline(input, buffer)) {
std::vector<std::string> items_buffer;
std::string name;

detail::trim(line);
line = detail::trim_copy(buffer);
std::size_t len = line.length();
// lines have to be at least 3 characters to have any meaning to CLI just skip the rest
if(len < 3) {
continue;
}
if(line.compare(0, 3, tquote) == 0 || line.compare(0, 3, "'''") == 0) {
inMLineComment = true;
auto cchar = line.front();
while(inMLineComment) {
if(getline(input, line)) {
detail::trim(line);
} else {
break;
}
if(detail::hasMLString(line, cchar)) {
inMLineComment = false;
}
}
continue;
}
if(line.front() == '[' && line.back() == ']') {
if(currentSection != "default") {
// insert a section end which is just an empty items_buffer
Expand Down Expand Up @@ -227,10 +259,61 @@
delimiter_pos = std::string::npos;
}
if(delimiter_pos != std::string::npos) {

name = detail::trim_copy(line.substr(0, delimiter_pos));
std::string item = detail::trim_copy(line.substr(delimiter_pos + 1, comment_pos - delimiter_pos - 1));

if(item.size() > 1 && item.front() == aStart) {
if(item.compare(0, 3, "'''") == 0 || item.compare(0, 3, tquote) == 0) {
// mutliline string
auto keyChar = item.front();
item = buffer.substr(delimiter_pos + 1, std::string::npos);
detail::ltrim(item);
item.erase(0, 3);
inMLineValue = true;
bool lineExtension{false};
bool firstLine = true;
if(!item.empty() && item.back() == '\\') {
item.pop_back();
lineExtension = true;
}
while(inMLineValue) {
std::string l2;
if(!std::getline(input, l2)) {
break;

Check warning on line 281 in include/CLI/impl/Config_inl.hpp

View check run for this annotation

Codecov / codecov/patch

include/CLI/impl/Config_inl.hpp#L281

Added line #L281 was not covered by tests
}
line = l2;
detail::rtrim(line);
if(detail::hasMLString(line, keyChar)) {
line.pop_back();
line.pop_back();
line.pop_back();
if(lineExtension) {
detail::ltrim(line);
} else if(!(firstLine && item.empty())) {
item.push_back('\n');
}
firstLine = false;
item += line;
inMLineValue = false;
if(!item.empty() && item.back() == '\n') {
item.pop_back();
}
} else {
if(lineExtension) {
detail::trim(l2);
} else if(!(firstLine && item.empty())) {
item.push_back('\n');
}
lineExtension = false;
firstLine = false;
if(!l2.empty() && l2.back() == '\\') {
lineExtension = true;
l2.pop_back();
}
item += l2;
}
}
items_buffer = {item};
} else if(item.size() > 1 && item.front() == aStart) {
for(std::string multiline; item.back() != aEnd && std::getline(input, multiline);) {
detail::trim(multiline);
item += multiline;
Expand Down
Loading