1 #include "OptionsDB.h"
2
3 #include "Directories.h"
4 #include "i18n.h"
5 #include "Logger.h"
6 #include "OptionValidators.h"
7 #include "XMLDoc.h"
8
9 #include <iostream>
10 #include <iomanip>
11 #include <stdexcept>
12 #include <string>
13
14 #include <boost/algorithm/string/classification.hpp>
15 #include <boost/algorithm/string/erase.hpp>
16 #include <boost/algorithm/string/predicate.hpp>
17 #include <boost/filesystem/fstream.hpp>
18 #include <boost/filesystem/operations.hpp>
19 #include <boost/range/algorithm_ext/erase.hpp>
20 #include <boost/tokenizer.hpp>
21
22 namespace {
OptionsRegistry()23 std::vector<OptionsDBFn>& OptionsRegistry() {
24 static std::vector<OptionsDBFn> options_db_registry;
25 return options_db_registry;
26 }
27
PreviousSectionName(const std::vector<XMLElement * > & elem_stack)28 std::string PreviousSectionName(const std::vector<XMLElement*>& elem_stack) {
29 std::string retval;
30 for (unsigned int i = 1; i < elem_stack.size(); ++i) {
31 retval += elem_stack[i]->Tag();
32 if (i != elem_stack.size() - 1)
33 retval += '.';
34 }
35 return retval;
36 }
37
StripQuotation(std::string & str)38 void StripQuotation(std::string& str) {
39 using namespace boost::algorithm;
40 if (starts_with(str, "\"") && ends_with(str, "\"")) {
41 erase_first(str, "\"");
42 erase_last(str, "\"");
43 }
44 }
45 }
46
47 /////////////////////////////////////////////
48 // Free Functions
49 /////////////////////////////////////////////
RegisterOptions(OptionsDBFn function)50 bool RegisterOptions(OptionsDBFn function) {
51 OptionsRegistry().push_back(function);
52 return true;
53 }
54
GetOptionsDB()55 OptionsDB& GetOptionsDB() {
56 static OptionsDB options_db;
57 if (!OptionsRegistry().empty()) {
58 for (OptionsDBFn fn : OptionsRegistry())
59 fn(options_db);
60 OptionsRegistry().clear();
61 }
62 return options_db;
63 }
64
65
66 /////////////////////////////////////////////
67 // OptionsDB::Option
68 /////////////////////////////////////////////
69 // static(s)
70 std::map<char, std::string> OptionsDB::Option::short_names;
71
Option()72 OptionsDB::Option::Option()
73 {}
74
Option(char short_name_,const std::string & name_,const boost::any & value_,const boost::any & default_value_,const std::string & description_,const ValidatorBase * validator_,bool storable_,bool flag_,bool recognized_,const std::string & section)75 OptionsDB::Option::Option(char short_name_, const std::string& name_, const boost::any& value_,
76 const boost::any& default_value_, const std::string& description_,
77 const ValidatorBase* validator_, bool storable_, bool flag_, bool recognized_,
78 const std::string& section) :
79 name(name_),
80 short_name(short_name_),
81 value(value_),
82 default_value(default_value_),
83 description(description_),
84 validator(validator_),
85 storable(storable_),
86 flag(flag_),
87 recognized(recognized_),
88 option_changed_sig_ptr(new boost::signals2::signal<void ()>())
89 {
90 if (short_name_)
91 short_names[short_name_] = name;
92
93 auto name_it = name.rfind('.');
94 if (name_it != std::string::npos)
95 sections.emplace(name.substr(0, name_it));
96
97 if (!section.empty())
98 sections.emplace(section);
99 else if (sections.empty())
100 sections.emplace("misc");
101 }
102
SetFromString(const std::string & str)103 bool OptionsDB::Option::SetFromString(const std::string& str) {
104 bool changed = false;
105 boost::any value_;
106
107 if (!flag) {
108 value_ = validator->Validate(str);
109 changed = validator->String(value) != validator->String(value_);
110 } else {
111 value_ = boost::lexical_cast<bool>(str); // if a flag, then the str parameter should just indicate true or false with "1" or "0"
112 changed = (boost::lexical_cast<std::string>(boost::any_cast<bool>(value))
113 != boost::lexical_cast<std::string>(boost::any_cast<bool>(value_)));
114 }
115
116 if (changed) {
117 value = std::move(value_);
118 (*option_changed_sig_ptr)();
119 }
120 return changed;
121 }
122
SetToDefault()123 bool OptionsDB::Option::SetToDefault() {
124 bool changed = !ValueIsDefault();
125 if (changed) {
126 value = default_value;
127 (*option_changed_sig_ptr)();
128 }
129 return changed;
130 }
131
ValueToString() const132 std::string OptionsDB::Option::ValueToString() const {
133 if (!flag)
134 return validator->String(value);
135 else
136 return boost::lexical_cast<std::string>(boost::any_cast<bool>(value));
137 }
138
DefaultValueToString() const139 std::string OptionsDB::Option::DefaultValueToString() const {
140 if (!flag)
141 return validator->String(default_value);
142 else
143 return boost::lexical_cast<std::string>(boost::any_cast<bool>(default_value));
144 }
145
ValueIsDefault() const146 bool OptionsDB::Option::ValueIsDefault() const
147 { return ValueToString() == DefaultValueToString(); }
148
149
150 /////////////////////////////////////////////
151 // OptionsDB::OptionSection
152 /////////////////////////////////////////////
153 OptionsDB::OptionSection::OptionSection() = default;
154
OptionSection(const std::string & name_,const std::string & description_,std::function<bool (const std::string &)> option_predicate_)155 OptionsDB::OptionSection::OptionSection(const std::string& name_, const std::string& description_,
156 std::function<bool (const std::string&)> option_predicate_) :
157 name(name_),
158 description(description_),
159 option_predicate(option_predicate_)
160 {}
161
162 /////////////////////////////////////////////
163 // OptionsDB
164 /////////////////////////////////////////////
165 // static(s)
166 OptionsDB* OptionsDB::s_options_db = nullptr;
167
OptionsDB()168 OptionsDB::OptionsDB() : m_dirty(false) {
169 if (s_options_db)
170 throw std::runtime_error("Attempted to create a duplicate instance of singleton class OptionsDB.");
171
172 s_options_db = this;
173 }
174
Commit(bool only_if_dirty,bool only_non_default)175 bool OptionsDB::Commit(bool only_if_dirty, bool only_non_default) {
176 if (only_if_dirty && !m_dirty)
177 return true;
178 boost::filesystem::ofstream ofs(GetConfigPath());
179 if (ofs) {
180 XMLDoc doc;
181 GetOptionsDB().GetXML(doc, only_non_default, true);
182 doc.WriteDoc(ofs);
183 m_dirty = false;
184 return true;
185 } else {
186 std::cerr << UserString("UNABLE_TO_WRITE_CONFIG_XML") << std::endl;
187 std::cerr << PathToString(GetConfigPath()) << std::endl;
188 ErrorLogger() << UserString("UNABLE_TO_WRITE_CONFIG_XML");
189 ErrorLogger() << PathToString(GetConfigPath());
190 return false;
191 }
192 }
193
CommitPersistent()194 bool OptionsDB::CommitPersistent() {
195 bool retval = false;
196 auto config_file = GetPersistentConfigPath();
197 XMLDoc doc;
198 GetOptionsDB().GetXML(doc, true, false); // only output non-default options
199 try {
200 // Remove any previously existing file
201 boost::filesystem::remove(config_file);
202
203 boost::filesystem::ofstream ofs(GetPersistentConfigPath());
204 if (ofs) {
205 doc.WriteDoc(ofs);
206 retval = true;
207 } else {
208 std::string err_msg = UserString("UNABLE_TO_WRITE_PERSISTENT_CONFIG_XML") + " : " + config_file.string();
209 ErrorLogger() << err_msg;
210 std::cerr << err_msg << std::endl;
211 }
212 } catch (const boost::filesystem::filesystem_error& ec) {
213 ErrorLogger() << "Error during file operations when creating persistent config : " << ec.what();
214 } catch (...) {
215 std::string err_msg = "Unknown exception during persistent config creation";
216 ErrorLogger() << err_msg;
217 std::cerr << err_msg << std::endl;
218 }
219
220 return retval;
221 }
222
Validate(const std::string & name,const std::string & value) const223 void OptionsDB::Validate(const std::string& name, const std::string& value) const {
224 auto it = m_options.find(name);
225 if (!OptionExists(it))
226 throw std::runtime_error("Attempted to validate unknown option \"" + name + "\".");
227
228 if (it->second.validator)
229 it->second.validator->Validate(value);
230 else if (it->second.flag)
231 boost::lexical_cast<bool>(value);
232 }
233
GetValueString(const std::string & option_name) const234 std::string OptionsDB::GetValueString(const std::string& option_name) const {
235 auto it = m_options.find(option_name);
236 if (!OptionExists(it))
237 throw std::runtime_error(("OptionsDB::GetValueString(): No option called \"" + option_name + "\" could be found.").c_str());
238 return it->second.ValueToString();
239 }
240
GetDefaultValueString(const std::string & option_name) const241 std::string OptionsDB::GetDefaultValueString(const std::string& option_name) const {
242 auto it = m_options.find(option_name);
243 if (!OptionExists(it))
244 throw std::runtime_error(("OptionsDB::GetDefaultValueString(): No option called \"" + option_name + "\" could be found.").c_str());
245 return it->second.DefaultValueToString();
246 }
247
GetDescription(const std::string & option_name) const248 const std::string& OptionsDB::GetDescription(const std::string& option_name) const {
249 auto it = m_options.find(option_name);
250 if (!OptionExists(it))
251 throw std::runtime_error(("OptionsDB::GetDescription(): No option called \"" + option_name + "\" could be found.").c_str());
252 return it->second.description;
253 }
254
GetValidator(const std::string & option_name) const255 std::shared_ptr<const ValidatorBase> OptionsDB::GetValidator(const std::string& option_name) const {
256 auto it = m_options.find(option_name);
257 if (!OptionExists(it))
258 throw std::runtime_error(("OptionsDB::GetValidator(): No option called \"" + option_name + "\" could be found.").c_str());
259 return it->second.validator;
260 }
261
262 namespace {
263 const std::size_t TERMINAL_LINE_WIDTH = 80;
264
265 /** Breaks and indents text over multiple lines when it exceeds width limits
266 * @param text String to format, tokenized by spaces, tabs, and newlines (newlines retained but potentially indented)
267 * @param indents amount of space prior to text. First for initial line, second for any new lines.
268 * @param widths width to limit the text to. First for initial line, second for any new lines.
269 * @returns string Formatted results of @p text
270 */
SplitText(const std::string & text,std::pair<std::size_t,std::size_t> indents={ 0, 0 },std::pair<std::size_t,std::size_t> widths={ TERMINAL_LINE_WIDTH, TERMINAL_LINE_WIDTH })271 std::string SplitText(const std::string& text, std::pair<std::size_t, std::size_t> indents = { 0, 0 },
272 std::pair<std::size_t, std::size_t> widths = { TERMINAL_LINE_WIDTH, TERMINAL_LINE_WIDTH })
273 {
274 boost::char_separator<char> separator { " \t", "\n" };
275 boost::tokenizer<boost::char_separator<char>> tokens { text, separator };
276
277 std::vector<std::string> lines { "" };
278 for (const auto& token : tokens) {
279 if (token == "\n")
280 lines.push_back("");
281 else if (widths.second < lines.back().size() + token.size() + indents.second)
282 lines.push_back(token + " ");
283 else if (!token.empty())
284 lines.back().append(token + " ");
285 }
286
287 std::string indent { std::string(indents.second, ' ') };
288 std::stringstream retval;
289 auto first_line = std::move(lines.front());
290 retval << std::string(indents.first, ' ') << first_line << std::endl;
291 for (auto line : lines)
292 if (!line.empty())
293 retval << indent << line << std::endl;
294
295 return retval.str();
296 }
297
OptionNameHasParentSection(const std::string & lhs,const std::string & rhs)298 bool OptionNameHasParentSection(const std::string& lhs, const std::string& rhs) {
299 auto it = lhs.find_last_of('.');
300 if (it == std::string::npos)
301 return false;
302 return lhs.substr(0, it) == rhs;
303 }
304 }
305
OptionsBySection(bool allow_unrecognized) const306 std::unordered_map<std::string, std::set<std::string>> OptionsDB::OptionsBySection(bool allow_unrecognized) const {
307 // Determine sections after all predicate calls from known options
308 std::unordered_map<std::string, std::unordered_set<std::string>> sections_by_option;
309 for (const auto& option : m_options) {
310 if (!allow_unrecognized && !option.second.recognized)
311 continue;
312
313 for (const auto& section : option.second.sections)
314 sections_by_option[option.first].emplace(section);
315
316 for (auto& section : m_sections)
317 if (section.second.option_predicate && section.second.option_predicate(option.first))
318 sections_by_option[option.first].emplace(section.first);
319 }
320
321 // tally the total number of options under each section
322 std::unordered_map<std::string, std::size_t> total_options_per_section;
323 for (const auto& option_section : sections_by_option) {
324 auto option_name = option_section.first;
325 auto dot_it = option_name.find_first_of(".");
326 // increment count of each containing parent section
327 while (dot_it != std::string::npos) {
328 total_options_per_section[option_name.substr(0, dot_it)]++;
329 dot_it++;
330 dot_it = option_name.find_first_of(".", dot_it);
331 }
332 }
333
334 // sort options into common sections
335 std::unordered_map<std::string, std::set<std::string>> options_by_section;
336 for (const auto& option : sections_by_option) {
337 for (const auto& section : option.second) {
338 auto section_name = section;
339 auto defined_section_it = m_sections.find(section_name);
340 bool has_descr = defined_section_it != m_sections.end() ?
341 !defined_section_it->second.description.empty() :
342 false;
343
344 // move options from sparse sections to more common parent
345 auto section_count = total_options_per_section[section_name];
346 auto section_end_it = section_name.find_last_of(".");
347 while (!has_descr && section_count < 4 && section_end_it != std::string::npos) {
348 auto new_section_name = section_name.substr(0, section_end_it);
349 // prevent moving into dense sections
350 if (total_options_per_section[new_section_name] > ( 7 - section_count ))
351 break;
352 total_options_per_section[section_name]--;
353 section_name = new_section_name;
354 section_end_it = section_name.find_last_of(".");
355 section_count = total_options_per_section[section_name];
356
357 defined_section_it = m_sections.find(section_name);
358 if (defined_section_it != m_sections.end())
359 has_descr = !defined_section_it->second.description.empty();
360 }
361
362 options_by_section[section_name].emplace(option.first);
363 }
364 }
365
366 // define which section are top level sections ("root"), move top level candidates with single option to misc
367 for (const auto& section_it : total_options_per_section) {
368 auto root_name = section_it.first.substr(0, section_it.first.find_first_of("."));
369 // root_name with no dot element allowed to pass if an option is known, potentially moving to misc section
370 auto total_it = total_options_per_section.find(root_name);
371 if (total_it == total_options_per_section.end())
372 continue;
373
374 if (total_it->second > 1) {
375 options_by_section["root"].emplace(root_name);
376 } else if (section_it.first != "misc" &&
377 section_it.first != "root" &&
378 !m_sections.count(section_it.first))
379 {
380 // move option to misc section
381 auto section_option_it = options_by_section.find(section_it.first);
382 if (section_option_it == options_by_section.end())
383 continue;
384 for (auto&& option : section_option_it->second)
385 options_by_section["misc"].emplace(std::move(option));
386 options_by_section.erase(section_it.first);
387 }
388 }
389
390 return options_by_section;
391 }
392
GetUsage(std::ostream & os,const std::string & command_line,bool allow_unrecognized) const393 void OptionsDB::GetUsage(std::ostream& os, const std::string& command_line, bool allow_unrecognized) const {
394 // Prevent logger output from garbling console display for low severity messages
395 OverrideAllLoggersThresholds(LogLevel::warn);
396
397 auto options_by_section = OptionsBySection(allow_unrecognized);
398 if (!command_line.empty() || command_line == "all" || command_line == "raw") {
399 // remove the root section if unneeded
400 if (options_by_section.count("root"))
401 options_by_section.erase("root");
402 }
403
404 // print description of command_line arg as section
405 if (command_line == "all") {
406 os << UserString("OPTIONS_DB_SECTION_ALL") << " ";
407 } else if (command_line == "raw") {
408 os << UserString("OPTIONS_DB_SECTION_RAW") << " ";
409 } else {
410 auto command_section_it = m_sections.find(command_line);
411 if (command_section_it != m_sections.end() && !command_section_it->second.description.empty())
412 os << UserString(command_section_it->second.description) << " ";
413 }
414
415 bool print_misc_section = command_line.empty();
416 std::set<std::string> section_list {};
417 // print option sections
418 if (command_line != "all" && command_line != "raw") {
419 std::size_t name_col_width = 20;
420 if (command_line.empty()) {
421 auto root_it = options_by_section.find("root");
422 if (root_it != options_by_section.end()) {
423 for (const auto& section : root_it->second)
424 if (section.find_first_of(".") == std::string::npos)
425 if (section_list.emplace(section).second && name_col_width < section.size())
426 name_col_width = section.size();
427 }
428 } else {
429 for (const auto& it : options_by_section)
430 if (OptionNameHasParentSection(it.first, command_line))
431 if (section_list.emplace(it.first).second && name_col_width < it.first.size())
432 name_col_width = it.first.size();
433 }
434 name_col_width += 5;
435
436 if (!section_list.empty())
437 os << UserString("COMMAND_LINE_SECTIONS") << ":" << std::endl;
438
439 auto indents = std::make_pair(2, name_col_width + 4);
440 auto widths = std::make_pair(TERMINAL_LINE_WIDTH - name_col_width, TERMINAL_LINE_WIDTH);
441 for (const auto& section : section_list) {
442 if (section == "misc") {
443 print_misc_section = true;
444 continue;
445 }
446 auto section_it = m_sections.find(section);
447 std::string descr = (section_it == m_sections.end()) ? "" : UserString(section_it->second.description);
448
449 os << std::setw(2) << "" // indent
450 << std::setw(name_col_width) << std::left << section // section name
451 << SplitText(descr, indents, widths); // section description
452 }
453
454 if (print_misc_section) {
455 // Add special miscellaneous section to bottom
456 os << std::setw(2) << "" << std::setw(name_col_width) << std::left << "misc";
457 os << SplitText(UserString("OPTIONS_DB_SECTION_MISC"), indents, widths);
458 }
459
460 // add empty line between groups and options
461 if (!section_list.empty() && !print_misc_section)
462 os << std::endl;
463 }
464
465
466 // print options
467 if (!command_line.empty()) {
468 std::set<std::string> option_list;
469 if (command_line == "all" || command_line == "raw") {
470 for (const auto& option_section_it : options_by_section)
471 for (const auto& option : option_section_it.second)
472 option_list.emplace(option);
473 } else {
474 auto option_section_it = options_by_section.find(command_line);
475 if (option_section_it != options_by_section.end())
476 option_list = option_section_it->second;
477 // allow traversal by node when no other results are found
478 if (option_list.empty() && section_list.empty())
479 FindOptions(option_list, command_line, allow_unrecognized);
480 }
481
482 // insert command_line as option, if it exists
483 if (command_line != "all" && command_line != "raw" && m_options.count(command_line))
484 option_list.emplace(command_line);
485
486 if (!option_list.empty())
487 os << UserString("COMMAND_LINE_OPTIONS") << ":" << std::endl;
488
489 for (const auto& option_name : option_list) {
490 auto option_it = m_options.find(option_name);
491 if (option_it == m_options.end() || (!allow_unrecognized && !option_it->second.recognized))
492 continue;
493
494 if (command_line == "raw") {
495 os << option_name << ", " << option_it->second.description << "," << std::endl;
496 if (option_it->second.short_name)
497 os << option_it->second.short_name << ", " << option_it->second.description << "," << std::endl;
498 } else {
499 // option name(s)
500 if (option_it->second.short_name)
501 os << "-" << option_it->second.short_name << " | --" << option_name;
502 else
503 os << "--" << option_name;
504
505 // option description
506 if (!option_it->second.description.empty())
507 os << std::endl << SplitText(UserString(option_it->second.description), {5, 7});
508 else
509 os << std::endl;
510
511 // option default value
512 if (option_it->second.validator) {
513 auto validator_str = UserString("COMMAND_LINE_DEFAULT") + ": " + option_it->second.DefaultValueToString();
514 os << SplitText(validator_str, {5, 7}, {TERMINAL_LINE_WIDTH - validator_str.size(), 77});
515 }
516 os << std::endl;
517 }
518 }
519
520 if (section_list.empty() && option_list.empty()) {
521 os << UserString("COMMAND_LINE_NOT_FOUND") << ": " << command_line << std::endl << std::endl;
522 os << UserString("COMMAND_LINE_USAGE") << std::endl;
523 }
524 }
525
526 // reset override in case this function is later repurposed
527 OverrideAllLoggersThresholds(boost::none);
528 }
529
GetXML(XMLDoc & doc,bool non_default_only,bool include_version) const530 void OptionsDB::GetXML(XMLDoc& doc, bool non_default_only, bool include_version) const {
531 doc = XMLDoc();
532
533 std::vector<XMLElement*> elem_stack;
534 elem_stack.push_back(&doc.root_node);
535
536 for (const auto& option : m_options) {
537 if (!option.second.storable)
538 continue;
539
540 if (!option.second.recognized)
541 continue;
542
543 std::string::size_type last_dot = option.first.find_last_of('.');
544 std::string section_name = last_dot == std::string::npos ? "" : option.first.substr(0, last_dot);
545 std::string name = option.first.substr(last_dot == std::string::npos ? 0 : last_dot + 1);
546
547 // "version.gl.check.done" is automatically set to true after other logic is performed
548 if (option.first == "version.gl.check.done")
549 continue;
550
551 // Skip unwanted config options
552 // BUG Some windows may be shown as a child of an other window, but not initially visible.
553 // The OptionDB default of "*.visible" in these cases may be false, but setting the option to false
554 // in a config file may prevent such windows from showing when requested.
555 if (name == "visible")
556 continue;
557
558 // Storing "version.string" in persistent config would render all config options invalid after a new build
559 if (!include_version && option.first == "version.string")
560 continue;
561
562 // do want to store version string if requested, regardless of whether
563 // it is default. for other strings, if storing non-default only,
564 // check if option is default and if it is, skip it.
565 if (non_default_only && option.first != "version.string") {
566 bool is_default_nonflag = !option.second.flag && IsDefaultValue(m_options.find(option.first));
567 if (is_default_nonflag)
568 continue;
569
570 // Default value of flag options will throw bad_any_cast, fortunately they always default to false
571 if (option.second.flag && !boost::any_cast<bool>(option.second.value))
572 continue;
573 }
574
575
576 while (1 < elem_stack.size()) {
577 std::string prev_section = PreviousSectionName(elem_stack);
578 if (prev_section == section_name) {
579 section_name.clear();
580 break;
581 } else if (section_name.find(prev_section + '.') == 0) {
582 section_name = section_name.substr(prev_section.size() + 1);
583 break;
584 }
585 elem_stack.pop_back();
586 }
587 if (!section_name.empty()) {
588 std::string::size_type last_pos = 0;
589 std::string::size_type pos = 0;
590 while ((pos = section_name.find('.', last_pos)) != std::string::npos) {
591 XMLElement temp(section_name.substr(last_pos, pos - last_pos));
592 elem_stack.back()->children.push_back(temp);
593 elem_stack.push_back(&elem_stack.back()->Child(temp.Tag()));
594 last_pos = pos + 1;
595 }
596 XMLElement temp(section_name.substr(last_pos));
597 elem_stack.back()->children.push_back(temp);
598 elem_stack.push_back(&elem_stack.back()->Child(temp.Tag()));
599 }
600
601 XMLElement temp(name);
602 if (option.second.validator) {
603 temp.SetText(option.second.ValueToString());
604 } else if (option.second.flag) {
605 if (!boost::any_cast<bool>(option.second.value))
606 continue;
607 }
608 elem_stack.back()->children.push_back(temp);
609 elem_stack.push_back(&elem_stack.back()->Child(temp.Tag()));
610 }
611 }
612
OptionChangedSignal(const std::string & option)613 OptionsDB::OptionChangedSignalType& OptionsDB::OptionChangedSignal(const std::string& option) {
614 auto it = m_options.find(option);
615 if (it == m_options.end())
616 throw std::runtime_error("OptionsDB::OptionChangedSignal() : Attempted to get signal for nonexistent option \"" + option + "\".");
617 return *it->second.option_changed_sig_ptr;
618 }
619
Remove(const std::string & name)620 void OptionsDB::Remove(const std::string& name) {
621 auto it = m_options.find(name);
622 if (it != m_options.end()) {
623 Option::short_names.erase(it->second.short_name);
624 m_options.erase(it);
625 m_dirty = true;
626 }
627 OptionRemovedSignal(name);
628 }
629
RemoveUnrecognized(const std::string & prefix)630 void OptionsDB::RemoveUnrecognized(const std::string& prefix) {
631 auto it = m_options.begin();
632 while (it != m_options.end()) {
633 if (!it->second.recognized && it->first.find(prefix) == 0)
634 Remove((it++)->first); // note postfix operator++
635 else
636 ++it;
637 }
638 }
639
FindOptions(std::set<std::string> & ret,const std::string & prefix,bool allow_unrecognized) const640 void OptionsDB::FindOptions(std::set<std::string>& ret, const std::string& prefix, bool allow_unrecognized) const {
641 ret.clear();
642 for (auto& option : m_options)
643 if ((option.second.recognized || allow_unrecognized) && option.first.find(prefix) == 0)
644 ret.insert(option.first);
645 }
646
SetFromCommandLine(const std::vector<std::string> & args)647 void OptionsDB::SetFromCommandLine(const std::vector<std::string>& args) {
648 //bool option_changed = false;
649
650 for (unsigned int i = 1; i < args.size(); ++i) {
651 std::string current_token(args[i]);
652
653 if (current_token.find("--") == 0) {
654 std::string option_name = current_token.substr(2);
655
656 if (option_name.empty())
657 throw std::runtime_error("A \'--\' was given with no option name.");
658
659 auto it = m_options.find(option_name);
660
661 if (it == m_options.end() || !it->second.recognized) {
662 // unrecognized option: may be registered later on so we'll store it for now
663 // Check for more parameters (if this is the last one, assume that it is a flag).
664 std::string value_str("-");
665 if (i + 1 < static_cast<unsigned int>(args.size())) {
666 value_str = args[i + 1]; // copy assignment
667 StripQuotation(value_str);
668 }
669
670 if (value_str.at(0) == '-') { // this is either the last parameter or the next parameter is another option, assume this one is a flag
671 m_options[option_name] = Option(static_cast<char>(0), option_name, true,
672 boost::lexical_cast<std::string>(false),
673 "", 0, false, true, false, std::string());
674 } else { // the next parameter is the value, store it as a string to be parsed later
675 m_options[option_name] = Option(static_cast<char>(0), option_name,
676 value_str, value_str, "",
677 new Validator<std::string>(),
678 false, false, false, std::string()); // don't attempt to store options that have only been specified on the command line
679 }
680
681 WarnLogger() << "Option \"" << option_name << "\", was specified on the command line but was not recognized. It may not be registered yet or could be a typo.";
682
683 } else {
684 // recognized option
685 Option& option = it->second;
686 if (option.value.empty())
687 throw std::runtime_error("The value member of option \"--" + option.name + "\" is undefined.");
688
689 if (!option.flag) { // non-flag
690 try {
691 // check if parameter exists...
692 if (i + 1 >= static_cast<unsigned int>(args.size())) {
693 m_dirty |= option.SetFromString("");
694 continue;
695 }
696 // get parameter value
697 std::string value_str(args[++i]);
698 StripQuotation(value_str);
699 // ensure parameter is actually a parameter, and not the next option name (which would indicate
700 // that the option was specified without a parameter value, as if it was a flag)
701 if (!value_str.empty() && value_str.at(0) == '-')
702 throw std::runtime_error("the option \"" + option.name +
703 "\" was followed by the parameter \"" + value_str +
704 "\", which appears to be an option flag, not a parameter value, because it begins with a \"-\" character.");
705 m_dirty |= option.SetFromString(value_str);
706 } catch (const std::exception& e) {
707 throw std::runtime_error("OptionsDB::SetFromCommandLine() : the following exception was caught when attempting to set option \"" + option.name + "\": " + e.what() + "\n\n");
708 }
709 } else { // flag
710 option.value = true;
711 }
712 }
713
714 //option_changed = true;
715 } else if (current_token.find('-') == 0
716 #ifdef FREEORION_MACOSX
717 && current_token.find("-psn") != 0 // Mac OS X passes a process serial number to all applications using Carbon or Cocoa, it should be ignored here
718 #endif
719 )
720 {
721 std::string single_char_options = current_token.substr(1);
722
723 if (single_char_options.empty())
724 throw std::runtime_error("A \'-\' was given with no options.");
725
726 for (unsigned int j = 0; j < single_char_options.size(); ++j) {
727 auto short_name_it = Option::short_names.find(single_char_options[j]);
728
729 if (short_name_it == Option::short_names.end())
730 throw std::runtime_error(std::string("Unknown option \"-") + single_char_options[j] + "\" was given.");
731
732 auto name_it = m_options.find(short_name_it->second);
733
734 if (name_it == m_options.end())
735 throw std::runtime_error("Option \"--" + short_name_it->second + "\", abbreviated as \"-" + short_name_it->first + "\", could not be found.");
736
737 Option& option = name_it->second;
738 if (option.value.empty())
739 throw std::runtime_error("The value member of option \"--" + option.name + "\" is undefined.");
740
741 if (!option.flag) {
742 if (j < single_char_options.size() - 1) {
743 throw std::runtime_error(std::string("Option \"-") + single_char_options[j] + "\" was given with no parameter.");
744 } else {
745 if (i + 1 >= static_cast<unsigned int>(args.size()))
746 m_dirty |= option.SetFromString("");
747 else
748 m_dirty |= option.SetFromString(args[++i]);
749 }
750 } else {
751 option.value = true;
752 }
753 }
754 }
755 }
756 }
757
SetFromFile(const boost::filesystem::path & file_path,const std::string & version)758 void OptionsDB::SetFromFile(const boost::filesystem::path& file_path,
759 const std::string& version)
760 {
761 XMLDoc doc;
762 try {
763 boost::filesystem::ifstream ifs(file_path);
764 if (ifs) {
765 doc.ReadDoc(ifs);
766 if (version.empty() || (doc.root_node.ContainsChild("version") &&
767 doc.root_node.Child("version").ContainsChild("string") &&
768 version == doc.root_node.Child("version").Child("string").Text()))
769 { GetOptionsDB().SetFromXML(doc); }
770 }
771 } catch (...) {
772 std::cerr << UserString("UNABLE_TO_READ_CONFIG_XML") << ": "
773 << file_path << std::endl;
774 }
775 }
776
SetFromXML(const XMLDoc & doc)777 void OptionsDB::SetFromXML(const XMLDoc& doc) {
778 for (const XMLElement& child : doc.root_node.children)
779 { SetFromXMLRecursive(child, ""); }
780 }
781
SetFromXMLRecursive(const XMLElement & elem,const std::string & section_name)782 void OptionsDB::SetFromXMLRecursive(const XMLElement& elem, const std::string& section_name) {
783 std::string option_name = section_name + (section_name.empty() ? "" : ".") + elem.Tag();
784 if (option_name == "version.string")
785 return;
786
787 if (!elem.children.empty()) {
788 for (const XMLElement& child : elem.children)
789 SetFromXMLRecursive(child, option_name);
790 }
791
792 auto it = m_options.find(option_name);
793
794 if (it == m_options.end() || !it->second.recognized) {
795 if (elem.Text().length() == 0) {
796 // do not retain empty XML options
797 return;
798 } else {
799 // Store unrecognized option to be parsed later if this options is added.
800 m_options[option_name] = Option(static_cast<char>(0), option_name,
801 elem.Text(), elem.Text(),
802 "", new Validator<std::string>(),
803 true, false, false, section_name);
804 }
805
806 TraceLogger() << "Option \"" << option_name << "\", was in config.xml but was not recognized. It may not be registered yet or you may need to delete your config.xml if it is out of date.";
807 m_dirty = true;
808 return;
809 }
810
811 Option& option = it->second;
812 //if (!option.flag && option.value.empty()) {
813 // ErrorLogger() << "The value member of option \"" << option.name << "\" in config.xml is undefined.";
814 // return;
815 //}
816
817 if (option.flag) {
818 static auto lexical_true_str = boost::lexical_cast<std::string>(true);
819 option.value = static_cast<bool>(elem.Text() == lexical_true_str);
820 } else {
821 try {
822 m_dirty |= option.SetFromString(elem.Text());
823 } catch (const std::exception& e) {
824 ErrorLogger() << "OptionsDB::SetFromXMLRecursive() : while processing config.xml the following exception was caught when attempting to set option \""
825 << option_name << "\" to \"" << elem.Text() << "\": " << e.what();
826 }
827 }
828 }
829
AddSection(const std::string & name,const std::string & description,std::function<bool (const std::string &)> option_predicate)830 void OptionsDB::AddSection(const std::string& name, const std::string& description,
831 std::function<bool (const std::string&)> option_predicate)
832 {
833 auto insert_result = m_sections.emplace(name, OptionSection(name, description, option_predicate));
834 // if previously existing section, update description/predicate if empty/null
835 if (!insert_result.second) {
836 if (!description.empty() && insert_result.first->second.description.empty())
837 insert_result.first->second.description = description;
838 if (option_predicate != nullptr && insert_result.first->second.option_predicate == nullptr)
839 insert_result.first->second.option_predicate = option_predicate;
840 }
841 }
842
843 template <>
Get(const std::string & name) const844 std::vector<std::string> OptionsDB::Get<std::vector<std::string>>(const std::string& name) const
845 {
846 auto it = m_options.find(name);
847 if (!OptionExists(it))
848 throw std::runtime_error("OptionsDB::Get<std::vector<std::string>>() : Attempted to get nonexistent option \"" + name + "\".");
849 try {
850 return boost::any_cast<std::vector<std::string>>(it->second.value);
851 } catch (const boost::bad_any_cast& e) {
852 ErrorLogger() << "bad any cast converting value option named: " << name << ". Returning default value instead";
853 try {
854 return boost::any_cast<std::vector<std::string>>(it->second.default_value);
855 } catch (const boost::bad_any_cast& e) {
856 ErrorLogger() << "bad any cast converting default value of std::vector<std::string> option named: " << name << ". Returning empty vector instead";
857 return std::vector<std::string>();
858 }
859 }
860 }
861
ListToString(const std::vector<std::string> & input_list)862 std::string ListToString(const std::vector<std::string>& input_list) {
863 // list input strings in comma-separated-value format
864 std::string retval;
865 for (auto it = input_list.begin(); it != input_list.end(); ++it) {
866 if (it != input_list.begin())
867 retval += ",";
868 std::string str(*it);
869 boost::remove_erase_if(str, boost::is_any_of("<&>'\",[]|\a\b\f\n\r\t\b")); // remove XML protected characters and a few other semi-randomly chosen characters to avoid corrupting enclosing XML document structure
870 retval += str;
871 }
872 return retval;
873 }
874
StringToList(const std::string & input_string)875 std::vector<std::string> StringToList(const std::string& input_string) {
876 std::vector<std::string> retval;
877 typedef boost::tokenizer<boost::char_separator<char>> Tokenizer;
878 boost::char_separator<char> separator(",");
879 Tokenizer tokens(input_string, separator);
880 for (const auto& token : tokens)
881 retval.push_back(token);
882 return retval;
883 }
884