1 // Copyright (c) 2017-2021, University of Cincinnati, developed by Henry Schreiner
2 // under NSF AWARD 1414736 and by the respective contributors.
3 // All rights reserved.
4 //
5 // SPDX-License-Identifier: BSD-3-Clause
6 
7 #pragma once
8 
9 // [CLI11:public_includes:set]
10 #include <algorithm>
11 #include <fstream>
12 #include <iostream>
13 #include <string>
14 #include <utility>
15 #include <vector>
16 // [CLI11:public_includes:set]
17 
18 #include "App.hpp"
19 #include "ConfigFwd.hpp"
20 #include "StringTools.hpp"
21 
22 namespace CLI {
23 // [CLI11:config_hpp:verbatim]
24 namespace detail {
25 
convert_arg_for_ini(const std::string & arg,char stringQuote='"',char characterQuote='\\'')26 inline std::string convert_arg_for_ini(const std::string &arg, char stringQuote = '"', char characterQuote = '\'') {
27     if(arg.empty()) {
28         return std::string(2, stringQuote);
29     }
30     // some specifically supported strings
31     if(arg == "true" || arg == "false" || arg == "nan" || arg == "inf") {
32         return arg;
33     }
34     // floating point conversion can convert some hex codes, but don't try that here
35     if(arg.compare(0, 2, "0x") != 0 && arg.compare(0, 2, "0X") != 0) {
36         double val;
37         if(detail::lexical_cast(arg, val)) {
38             return arg;
39         }
40     }
41     // just quote a single non numeric character
42     if(arg.size() == 1) {
43         return std::string(1, characterQuote) + arg + characterQuote;
44     }
45     // handle hex, binary or octal arguments
46     if(arg.front() == '0') {
47         if(arg[1] == 'x') {
48             if(std::all_of(arg.begin() + 2, arg.end(), [](char x) {
49                    return (x >= '0' && x <= '9') || (x >= 'A' && x <= 'F') || (x >= 'a' && x <= 'f');
50                })) {
51                 return arg;
52             }
53         } else if(arg[1] == 'o') {
54             if(std::all_of(arg.begin() + 2, arg.end(), [](char x) { return (x >= '0' && x <= '7'); })) {
55                 return arg;
56             }
57         } else if(arg[1] == 'b') {
58             if(std::all_of(arg.begin() + 2, arg.end(), [](char x) { return (x == '0' || x == '1'); })) {
59                 return arg;
60             }
61         }
62     }
63     if(arg.find_first_of(stringQuote) == std::string::npos) {
64         return std::string(1, stringQuote) + arg + stringQuote;
65     } else {
66         return characterQuote + arg + characterQuote;
67     }
68 }
69 
70 /// Comma separated join, adds quotes if needed
ini_join(const std::vector<std::string> & args,char sepChar=',',char arrayStart='[',char arrayEnd=']',char stringQuote='"',char characterQuote='\\'')71 inline std::string ini_join(const std::vector<std::string> &args,
72                             char sepChar = ',',
73                             char arrayStart = '[',
74                             char arrayEnd = ']',
75                             char stringQuote = '"',
76                             char characterQuote = '\'') {
77     std::string joined;
78     if(args.size() > 1 && arrayStart != '\0') {
79         joined.push_back(arrayStart);
80     }
81     std::size_t start = 0;
82     for(const auto &arg : args) {
83         if(start++ > 0) {
84             joined.push_back(sepChar);
85             if(isspace(sepChar) == 0) {
86                 joined.push_back(' ');
87             }
88         }
89         joined.append(convert_arg_for_ini(arg, stringQuote, characterQuote));
90     }
91     if(args.size() > 1 && arrayEnd != '\0') {
92         joined.push_back(arrayEnd);
93     }
94     return joined;
95 }
96 
generate_parents(const std::string & section,std::string & name,char parentSeparator)97 inline std::vector<std::string> generate_parents(const std::string &section, std::string &name, char parentSeparator) {
98     std::vector<std::string> parents;
99     if(detail::to_lower(section) != "default") {
100         if(section.find(parentSeparator) != std::string::npos) {
101             parents = detail::split(section, parentSeparator);
102         } else {
103             parents = {section};
104         }
105     }
106     if(name.find(parentSeparator) != std::string::npos) {
107         std::vector<std::string> plist = detail::split(name, parentSeparator);
108         name = plist.back();
109         detail::remove_quotes(name);
110         plist.pop_back();
111         parents.insert(parents.end(), plist.begin(), plist.end());
112     }
113 
114     // clean up quotes on the parents
115     for(auto &parent : parents) {
116         detail::remove_quotes(parent);
117     }
118     return parents;
119 }
120 
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,char parentSeparator)123 checkParentSegments(std::vector<ConfigItem> &output, const std::string &currentSection, char parentSeparator) {
124 
125     std::string estring;
126     auto parents = detail::generate_parents(currentSection, estring, parentSeparator);
127     if(!output.empty() && output.back().name == "--") {
128         std::size_t msize = (parents.size() > 1U) ? parents.size() : 2;
129         while(output.back().parents.size() >= msize) {
130             output.push_back(output.back());
131             output.back().parents.pop_back();
132         }
133 
134         if(parents.size() > 1) {
135             std::size_t common = 0;
136             std::size_t mpair = (std::min)(output.back().parents.size(), parents.size() - 1);
137             for(std::size_t ii = 0; ii < mpair; ++ii) {
138                 if(output.back().parents[ii] != parents[ii]) {
139                     break;
140                 }
141                 ++common;
142             }
143             if(common == mpair) {
144                 output.pop_back();
145             } else {
146                 while(output.back().parents.size() > common + 1) {
147                     output.push_back(output.back());
148                     output.back().parents.pop_back();
149                 }
150             }
151             for(std::size_t ii = common; ii < parents.size() - 1; ++ii) {
152                 output.emplace_back();
153                 output.back().parents.assign(parents.begin(), parents.begin() + static_cast<std::ptrdiff_t>(ii) + 1);
154                 output.back().name = "++";
155             }
156         }
157     } else if(parents.size() > 1) {
158         for(std::size_t ii = 0; ii < parents.size() - 1; ++ii) {
159             output.emplace_back();
160             output.back().parents.assign(parents.begin(), parents.begin() + static_cast<std::ptrdiff_t>(ii) + 1);
161             output.back().name = "++";
162         }
163     }
164 
165     // insert a section end which is just an empty items_buffer
166     output.emplace_back();
167     output.back().parents = std::move(parents);
168     output.back().name = "++";
169 }
170 }  // namespace detail
171 
from_config(std::istream & input) const172 inline std::vector<ConfigItem> ConfigBase::from_config(std::istream &input) const {
173     std::string line;
174     std::string currentSection = "default";
175     std::string previousSection = "default";
176     std::vector<ConfigItem> output;
177     bool isDefaultArray = (arrayStart == '[' && arrayEnd == ']' && arraySeparator == ',');
178     bool isINIArray = (arrayStart == '\0' || arrayStart == ' ') && arrayStart == arrayEnd;
179     bool inSection{false};
180     char aStart = (isINIArray) ? '[' : arrayStart;
181     char aEnd = (isINIArray) ? ']' : arrayEnd;
182     char aSep = (isINIArray && arraySeparator == ' ') ? ',' : arraySeparator;
183     int currentSectionIndex{0};
184     while(getline(input, line)) {
185         std::vector<std::string> items_buffer;
186         std::string name;
187 
188         detail::trim(line);
189         std::size_t len = line.length();
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") {
196                 // insert a section end which is just an empty items_buffer
197                 output.emplace_back();
198                 output.back().parents = detail::generate_parents(currentSection, name, parentSeparatorChar);
199                 output.back().name = "--";
200             }
201             currentSection = line.substr(1, len - 2);
202             // deal with double brackets for TOML
203             if(currentSection.size() > 1 && currentSection.front() == '[' && currentSection.back() == ']') {
204                 currentSection = currentSection.substr(1, currentSection.size() - 2);
205             }
206             if(detail::to_lower(currentSection) == "default") {
207                 currentSection = "default";
208             } else {
209                 detail::checkParentSegments(output, currentSection, parentSeparatorChar);
210             }
211             inSection = false;
212             if(currentSection == previousSection) {
213                 ++currentSectionIndex;
214             } else {
215                 currentSectionIndex = 0;
216                 previousSection = currentSection;
217             }
218             continue;
219         }
220 
221         // comment lines
222         if(line.front() == ';' || line.front() == '#' || line.front() == commentChar) {
223             continue;
224         }
225 
226         // Find = in string, split and recombine
227         auto pos = line.find(valueDelimiter);
228         if(pos != std::string::npos) {
229             name = detail::trim_copy(line.substr(0, pos));
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             }
236             if(item.size() > 1 && item.front() == aStart) {
237                 for(std::string multiline; item.back() != aEnd && std::getline(input, multiline);) {
238                     detail::trim(multiline);
239                     item += multiline;
240                 }
241                 items_buffer = detail::split_up(item.substr(1, item.length() - 2), aSep);
242             } else if((isDefaultArray || isINIArray) && item.find_first_of(aSep) != std::string::npos) {
243                 items_buffer = detail::split_up(item, aSep);
244             } else if((isDefaultArray || isINIArray) && item.find_first_of(' ') != std::string::npos) {
245                 items_buffer = detail::split_up(item);
246             } else {
247                 items_buffer = {item};
248             }
249         } else {
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 
257             items_buffer = {"true"};
258         }
259         if(name.find(parentSeparatorChar) == std::string::npos) {
260             detail::remove_quotes(name);
261         }
262         // clean up quotes on the items
263         for(auto &it : items_buffer) {
264             detail::remove_quotes(it);
265         }
266 
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         }
281         if(!output.empty() && name == output.back().name && parents == output.back().parents) {
282             output.back().inputs.insert(output.back().inputs.end(), items_buffer.begin(), items_buffer.end());
283         } else {
284             output.emplace_back();
285             output.back().parents = std::move(parents);
286             output.back().name = std::move(name);
287             output.back().inputs = std::move(items_buffer);
288         }
289     }
290     if(currentSection != "default") {
291         // insert a section end which is just an empty items_buffer
292         std::string ename;
293         output.emplace_back();
294         output.back().parents = detail::generate_parents(currentSection, ename, parentSeparatorChar);
295         output.back().name = "--";
296         while(output.back().parents.size() > 1) {
297             output.push_back(output.back());
298             output.back().parents.pop_back();
299         }
300     }
301     return output;
302 }
303 
304 inline std::string
to_config(const App * app,bool default_also,bool write_description,std::string prefix) const305 ConfigBase::to_config(const App *app, bool default_also, bool write_description, std::string prefix) const {
306     std::stringstream out;
307     std::string commentLead;
308     commentLead.push_back(commentChar);
309     commentLead.push_back(' ');
310 
311     std::vector<std::string> groups = app->get_groups();
312     bool defaultUsed = false;
313     groups.insert(groups.begin(), std::string("Options"));
314     if(write_description && (app->get_configurable() || app->get_parent() == nullptr || app->get_name().empty())) {
315         out << commentLead << detail::fix_newlines(commentLead, app->get_description()) << '\n';
316     }
317     for(auto &group : groups) {
318         if(group == "Options" || group.empty()) {
319             if(defaultUsed) {
320                 continue;
321             }
322             defaultUsed = true;
323         }
324         if(write_description && group != "Options" && !group.empty()) {
325             out << '\n' << commentLead << group << " Options\n";
326         }
327         for(const Option *opt : app->get_options({})) {
328 
329             // Only process options that are configurable
330             if(opt->get_configurable()) {
331                 if(opt->get_group() != group) {
332                     if(!(group == "Options" && opt->get_group().empty())) {
333                         continue;
334                     }
335                 }
336                 std::string name = prefix + opt->get_single_name();
337                 std::string value = detail::ini_join(
338                     opt->reduced_results(), arraySeparator, arrayStart, arrayEnd, stringQuote, characterQuote);
339 
340                 if(value.empty() && default_also) {
341                     if(!opt->get_default_str().empty()) {
342                         value = detail::convert_arg_for_ini(opt->get_default_str(), stringQuote, characterQuote);
343                     } else if(opt->get_expected_min() == 0) {
344                         value = "false";
345                     } else if(opt->get_run_callback_for_default()) {
346                         value = "\"\"";  // empty string default value
347                     }
348                 }
349 
350                 if(!value.empty()) {
351                     if(write_description && opt->has_description()) {
352                         out << '\n';
353                         out << commentLead << detail::fix_newlines(commentLead, opt->get_description()) << '\n';
354                     }
355                     out << name << valueDelimiter << value << '\n';
356                 }
357             }
358         }
359     }
360     auto subcommands = app->get_subcommands({});
361     for(const App *subcom : subcommands) {
362         if(subcom->get_name().empty()) {
363             if(write_description && !subcom->get_group().empty()) {
364                 out << '\n' << commentLead << subcom->get_group() << " Options\n";
365             }
366             out << to_config(subcom, default_also, write_description, prefix);
367         }
368     }
369 
370     for(const App *subcom : subcommands) {
371         if(!subcom->get_name().empty()) {
372             if(subcom->get_configurable() && app->got_subcommand(subcom)) {
373                 if(!prefix.empty() || app->get_parent() == nullptr) {
374                     out << '[' << prefix << subcom->get_name() << "]\n";
375                 } else {
376                     std::string subname = app->get_name() + parentSeparatorChar + subcom->get_name();
377                     auto p = app->get_parent();
378                     while(p->get_parent() != nullptr) {
379                         subname = p->get_name() + parentSeparatorChar + subname;
380                         p = p->get_parent();
381                     }
382                     out << '[' << subname << "]\n";
383                 }
384                 out << to_config(subcom, default_also, write_description, "");
385             } else {
386                 out << to_config(
387                     subcom, default_also, write_description, prefix + subcom->get_name() + parentSeparatorChar);
388             }
389         }
390     }
391 
392     return out.str();
393 }
394 
395 // [CLI11:config_hpp:end]
396 }  // namespace CLI
397