CLI11  1.9.1
Config.hpp
Go to the documentation of this file.
1 // Copyright (c) 2017-2020, 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 #include <algorithm>
10 #include <fstream>
11 #include <iostream>
12 #include <string>
13 #include <utility>
14 #include <vector>
15 
16 #include "App.hpp"
17 #include "ConfigFwd.hpp"
18 #include "StringTools.hpp"
19 
20 namespace CLI {
21 
22 namespace detail {
23 
24 inline std::string convert_arg_for_ini(const std::string &arg) {
25  if(arg.empty()) {
26  return std::string(2, '"');
27  }
28  // some specifically supported strings
29  if(arg == "true" || arg == "false" || arg == "nan" || arg == "inf") {
30  return arg;
31  }
32  // floating point conversion can convert some hex codes, but don't try that here
33  if(arg.compare(0, 2, "0x") != 0 && arg.compare(0, 2, "0X") != 0) {
34  double val;
35  if(detail::lexical_cast(arg, val)) {
36  return arg;
37  }
38  }
39  // just quote a single non numeric character
40  if(arg.size() == 1) {
41  return std::string("'") + arg + '\'';
42  }
43  // handle hex, binary or octal arguments
44  if(arg.front() == '0') {
45  if(arg[1] == 'x') {
46  if(std::all_of(arg.begin() + 2, arg.end(), [](char x) {
47  return (x >= '0' && x <= '9') || (x >= 'A' && x <= 'F') || (x >= 'a' && x <= 'f');
48  })) {
49  return arg;
50  }
51  } else if(arg[1] == 'o') {
52  if(std::all_of(arg.begin() + 2, arg.end(), [](char x) { return (x >= '0' && x <= '7'); })) {
53  return arg;
54  }
55  } else if(arg[1] == 'b') {
56  if(std::all_of(arg.begin() + 2, arg.end(), [](char x) { return (x == '0' || x == '1'); })) {
57  return arg;
58  }
59  }
60  }
61  if(arg.find_first_of('"') == std::string::npos) {
62  return std::string("\"") + arg + '"';
63  } else {
64  return std::string("'") + arg + '\'';
65  }
66 }
67 
69 inline std::string
70 ini_join(const std::vector<std::string> &args, char sepChar = ',', char arrayStart = '[', char arrayEnd = ']') {
71  std::string joined;
72  if(args.size() > 1 && arrayStart != '\0') {
73  joined.push_back(arrayStart);
74  }
75  std::size_t start = 0;
76  for(const auto &arg : args) {
77  if(start++ > 0) {
78  joined.push_back(sepChar);
79  if(isspace(sepChar) == 0) {
80  joined.push_back(' ');
81  }
82  }
83  joined.append(convert_arg_for_ini(arg));
84  }
85  if(args.size() > 1 && arrayEnd != '\0') {
86  joined.push_back(arrayEnd);
87  }
88  return joined;
89 }
90 
91 inline std::vector<std::string> generate_parents(const std::string &section, std::string &name) {
92  std::vector<std::string> parents;
93  if(detail::to_lower(section) != "default") {
94  if(section.find('.') != std::string::npos) {
95  parents = detail::split(section, '.');
96  } else {
97  parents = {section};
98  }
99  }
100  if(name.find('.') != std::string::npos) {
101  std::vector<std::string> plist = detail::split(name, '.');
102  name = plist.back();
103  detail::remove_quotes(name);
104  plist.pop_back();
105  parents.insert(parents.end(), plist.begin(), plist.end());
106  }
107 
108  // clean up quotes on the parents
109  for(auto &parent : parents) {
110  detail::remove_quotes(parent);
111  }
112  return parents;
113 }
114 
116 inline void checkParentSegments(std::vector<ConfigItem> &output, const std::string &currentSection) {
117 
118  std::string estring;
119  auto parents = detail::generate_parents(currentSection, estring);
120  if(!output.empty() && output.back().name == "--") {
121  std::size_t msize = (parents.size() > 1U) ? parents.size() : 2;
122  while(output.back().parents.size() >= msize) {
123  output.push_back(output.back());
124  output.back().parents.pop_back();
125  }
126 
127  if(parents.size() > 1) {
128  std::size_t common = 0;
129  std::size_t mpair = (std::min)(output.back().parents.size(), parents.size() - 1);
130  for(std::size_t ii = 0; ii < mpair; ++ii) {
131  if(output.back().parents[ii] != parents[ii]) {
132  break;
133  }
134  ++common;
135  }
136  if(common == mpair) {
137  output.pop_back();
138  } else {
139  while(output.back().parents.size() > common + 1) {
140  output.push_back(output.back());
141  output.back().parents.pop_back();
142  }
143  }
144  for(std::size_t ii = common; ii < parents.size() - 1; ++ii) {
145  output.emplace_back();
146  output.back().parents.assign(parents.begin(), parents.begin() + static_cast<std::ptrdiff_t>(ii) + 1);
147  output.back().name = "++";
148  }
149  }
150  } else if(parents.size() > 1) {
151  for(std::size_t ii = 0; 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 
158  // insert a section end which is just an empty items_buffer
159  output.emplace_back();
160  output.back().parents = std::move(parents);
161  output.back().name = "++";
162 }
163 } // namespace detail
164 
165 inline std::vector<ConfigItem> ConfigBase::from_config(std::istream &input) const {
166  std::string line;
167  std::string section = "default";
168 
169  std::vector<ConfigItem> output;
170  bool defaultArray = (arrayStart == '\0' || arrayStart == ' ') && arrayStart == arrayEnd;
171  char aStart = (defaultArray) ? '[' : arrayStart;
172  char aEnd = (defaultArray) ? ']' : arrayEnd;
173  char aSep = (defaultArray && arraySeparator == ' ') ? ',' : arraySeparator;
174 
175  while(getline(input, line)) {
176  std::vector<std::string> items_buffer;
177  std::string name;
178 
179  detail::trim(line);
180  std::size_t len = line.length();
181  if(len > 1 && line.front() == '[' && line.back() == ']') {
182  if(section != "default") {
183  // insert a section end which is just an empty items_buffer
184  output.emplace_back();
185  output.back().parents = detail::generate_parents(section, name);
186  output.back().name = "--";
187  }
188  section = line.substr(1, len - 2);
189  // deal with double brackets for TOML
190  if(section.size() > 1 && section.front() == '[' && section.back() == ']') {
191  section = section.substr(1, section.size() - 2);
192  }
193  if(detail::to_lower(section) == "default") {
194  section = "default";
195  } else {
196  detail::checkParentSegments(output, section);
197  }
198  continue;
199  }
200  if(len == 0) {
201  continue;
202  }
203  // comment lines
204  if(line.front() == ';' || line.front() == '#' || line.front() == commentChar) {
205  continue;
206  }
207 
208  // Find = in string, split and recombine
209  auto pos = line.find(valueDelimiter);
210  if(pos != std::string::npos) {
211  name = detail::trim_copy(line.substr(0, pos));
212  std::string item = detail::trim_copy(line.substr(pos + 1));
213  if(item.size() > 1 && item.front() == aStart && item.back() == aEnd) {
214  items_buffer = detail::split_up(item.substr(1, item.length() - 2), aSep);
215  } else if(defaultArray && item.find_first_of(aSep) != std::string::npos) {
216  items_buffer = detail::split_up(item, aSep);
217  } else if(defaultArray && item.find_first_of(' ') != std::string::npos) {
218  items_buffer = detail::split_up(item);
219  } else {
220  items_buffer = {item};
221  }
222  } else {
223  name = detail::trim_copy(line);
224  items_buffer = {"true"};
225  }
226  if(name.find('.') == std::string::npos) {
227  detail::remove_quotes(name);
228  }
229  // clean up quotes on the items
230  for(auto &it : items_buffer) {
232  }
233 
234  std::vector<std::string> parents = detail::generate_parents(section, name);
235 
236  if(!output.empty() && name == output.back().name && parents == output.back().parents) {
237  output.back().inputs.insert(output.back().inputs.end(), items_buffer.begin(), items_buffer.end());
238  } else {
239  output.emplace_back();
240  output.back().parents = std::move(parents);
241  output.back().name = std::move(name);
242  output.back().inputs = std::move(items_buffer);
243  }
244  }
245  if(section != "default") {
246  // insert a section end which is just an empty items_buffer
247  std::string ename;
248  output.emplace_back();
249  output.back().parents = detail::generate_parents(section, ename);
250  output.back().name = "--";
251  while(output.back().parents.size() > 1) {
252  output.push_back(output.back());
253  output.back().parents.pop_back();
254  }
255  }
256  return output;
257 }
258 
259 inline std::string
260 ConfigBase::to_config(const App *app, bool default_also, bool write_description, std::string prefix) const {
261  std::stringstream out;
262  std::string commentLead;
263  commentLead.push_back(commentChar);
264  commentLead.push_back(' ');
265 
266  std::vector<std::string> groups = app->get_groups();
267  bool defaultUsed = false;
268  groups.insert(groups.begin(), std::string("Options"));
269  if(write_description) {
270  out << commentLead << app->get_description() << '\n';
271  }
272  for(auto &group : groups) {
273  if(group == "Options" || group.empty()) {
274  if(defaultUsed) {
275  continue;
276  }
277  defaultUsed = true;
278  }
279  if(write_description && group != "Options" && !group.empty()) {
280  out << '\n' << commentLead << group << " Options\n";
281  }
282  for(const Option *opt : app->get_options({})) {
283 
284  // Only process option with a long-name and configurable
285  if(!opt->get_lnames().empty() && opt->get_configurable()) {
286  if(opt->get_group() != group) {
287  if(!(group == "Options" && opt->get_group().empty())) {
288  continue;
289  }
290  }
291  std::string name = prefix + opt->get_lnames()[0];
292  std::string value = detail::ini_join(opt->reduced_results(), arraySeparator, arrayStart, arrayEnd);
293 
294  if(value.empty() && default_also) {
295  if(!opt->get_default_str().empty()) {
296  value = detail::convert_arg_for_ini(opt->get_default_str());
297  } else if(opt->get_expected_min() == 0) {
298  value = "false";
299  }
300  }
301 
302  if(!value.empty()) {
303  if(write_description && opt->has_description()) {
304  out << '\n';
305  out << commentLead << detail::fix_newlines(commentLead, opt->get_description()) << '\n';
306  }
307  out << name << valueDelimiter << value << '\n';
308  }
309  }
310  }
311  }
312  auto subcommands = app->get_subcommands({});
313  for(const App *subcom : subcommands) {
314  if(subcom->get_name().empty()) {
315  if(write_description && !subcom->get_group().empty()) {
316  out << '\n' << commentLead << subcom->get_group() << " Options\n";
317  }
318  out << to_config(subcom, default_also, write_description, prefix);
319  }
320  }
321 
322  for(const App *subcom : subcommands) {
323  if(!subcom->get_name().empty()) {
324  if(subcom->get_configurable() && app->got_subcommand(subcom)) {
325  if(!prefix.empty() || app->get_parent() == nullptr) {
326  out << '[' << prefix << subcom->get_name() << "]\n";
327  } else {
328  std::string subname = app->get_name() + "." + subcom->get_name();
329  auto p = app->get_parent();
330  while(p->get_parent() != nullptr) {
331  subname = p->get_name() + "." + subname;
332  p = p->get_parent();
333  }
334  out << '[' << subname << "]\n";
335  }
336  out << to_config(subcom, default_also, write_description, "");
337  } else {
338  out << to_config(subcom, default_also, write_description, prefix + subcom->get_name() + ".");
339  }
340  }
341  }
342 
343  return out.str();
344 }
345 
346 } // namespace CLI
CLI::ConfigBase::arrayEnd
char arrayEnd
the character used to end an array '\0' is a default to not use
Definition: ConfigFwd.hpp:82
CLI::detail::generate_parents
std::vector< std::string > generate_parents(const std::string &section, std::string &name)
Definition: Config.hpp:91
CLI::App::get_options
std::vector< const Option * > get_options(const std::function< bool(const Option *)> filter={}) const
Get the list of options (user facing function, so returns raw pointers), has optional filter function...
Definition: App.hpp:1545
CLI::detail::split
std::vector< std::string > split(const std::string &s, char delim)
Split a string by a delim.
Definition: StringTools.hpp:42
CLI::detail::trim_copy
std::string trim_copy(const std::string &str)
Make a copy of the string and then trim it.
Definition: StringTools.hpp:136
CLI::App::got_subcommand
bool got_subcommand(const App *subcom) const
Check to see if given subcommand was selected.
Definition: App.hpp:1385
CLI::App::get_name
const std::string & get_name() const
Get the name of the current app.
Definition: App.hpp:1723
CLI::ConfigBase::arrayStart
char arrayStart
the character used to start an array '\0' is a default to not use
Definition: ConfigFwd.hpp:80
CLI::detail::lexical_cast
bool lexical_cast(const std::string &input, T &output)
Signed integers.
Definition: TypeTools.hpp:609
CLI
Definition: App.hpp:32
CLI::detail::split_up
std::vector< std::string > split_up(std::string str, char delimiter='\0')
Definition: StringTools.hpp:286
CLI::detail::ini_join
std::string ini_join(const std::vector< std::string > &args, char sepChar=',', char arrayStart='[', char arrayEnd=']')
Comma separated join, adds quotes if needed.
Definition: Config.hpp:70
CLI::detail::convert_arg_for_ini
std::string convert_arg_for_ini(const std::string &arg)
Definition: Config.hpp:24
CLI::ConfigBase::valueDelimiter
char valueDelimiter
the character used separate the name from the value
Definition: ConfigFwd.hpp:86
CLI::App::get_groups
std::vector< std::string > get_groups() const
Get the groups available directly from this option (in order)
Definition: App.hpp:1767
CLI::ConfigBase::to_config
std::string to_config(const App *, bool default_also, bool write_description, std::string prefix) const override
Convert an app into a configuration.
Definition: Config.hpp:260
CLI::ConfigBase::commentChar
char commentChar
the character used for comments
Definition: ConfigFwd.hpp:78
ConfigFwd.hpp
CLI::App::get_description
std::string get_description() const
Get the app or subcommand description.
Definition: App.hpp:1536
CLI::Option
Definition: Option.hpp:231
CLI::detail::to_lower
std::string to_lower(std::string str)
Return a lower case version of a string.
Definition: StringTools.hpp:199
App.hpp
CLI::detail::fix_newlines
std::string fix_newlines(const std::string &leader, std::string input)
Definition: StringTools.hpp:337
StringTools.hpp
CLI::detail::remove_quotes
std::string & remove_quotes(std::string &str)
remove quotes at the front and back of a string either '"' or '\''
Definition: StringTools.hpp:142
CLI::App::get_subcommands
std::vector< App * > get_subcommands() const
Definition: App.hpp:1347
CLI::detail::trim
std::string & trim(std::string &str)
Trim whitespace from string.
Definition: StringTools.hpp:130
CLI::App
Creates a command line program, with very few defaults.
Definition: App.hpp:66
CLI::ConfigBase::arraySeparator
char arraySeparator
the character used to separate elements in an array
Definition: ConfigFwd.hpp:84
CLI::ConfigBase::from_config
std::vector< ConfigItem > from_config(std::istream &input) const override
Convert a configuration into an app.
Definition: Config.hpp:165
CLI::App::get_parent
App * get_parent()
Get the parent of this subcommand (or nullptr if master app)
Definition: App.hpp:1717
CLI::detail::checkParentSegments
void checkParentSegments(std::vector< ConfigItem > &output, const std::string &currentSection)
assuming non default segments do a check on the close and open of the segments in a configItem struct...
Definition: Config.hpp:116