openMSX
CommandLineParser.cc
Go to the documentation of this file.
1 #include "CommandLineParser.hh"
3 #include "Interpreter.hh"
4 #include "SettingsConfig.hh"
5 #include "File.hh"
6 #include "FileContext.hh"
7 #include "FileOperations.hh"
8 #include "GlobalCliComm.hh"
9 #include "StdioMessages.hh"
10 #include "Version.hh"
11 #include "CliConnection.hh"
12 #include "ConfigException.hh"
13 #include "FileException.hh"
14 #include "EnumSetting.hh"
15 #include "XMLException.hh"
16 #include "StringOp.hh"
17 #include "xrange.hh"
18 #include "Reactor.hh"
19 #include "RomInfo.hh"
20 #include "hash_map.hh"
21 #include "one_of.hh"
22 #include "outer.hh"
23 #include "ranges.hh"
24 #include "stl.hh"
25 #include "view.hh"
26 #include "xxhash.hh"
27 #include "build-info.hh"
28 #include <cassert>
29 #include <iostream>
30 #include <memory>
31 
32 using std::cout;
33 using std::string;
34 using std::string_view;
35 
36 namespace openmsx {
37 
38 // class CommandLineParser
39 
41  : reactor(reactor_)
42  , msxRomCLI(*this)
43  , cliExtension(*this)
44  , replayCLI(*this)
45  , saveStateCLI(*this)
46  , cassettePlayerCLI(*this)
48  , laserdiscPlayerCLI(*this)
49 #endif
50  , diskImageCLI(*this)
51  , hdImageCLI(*this)
52  , cdImageCLI(*this)
53  , parseStatus(UNPARSED)
54 {
55  haveConfig = false;
56  haveSettings = false;
57 
58  registerOption("-h", helpOption, PHASE_BEFORE_INIT, 1);
59  registerOption("--help", helpOption, PHASE_BEFORE_INIT, 1);
60  registerOption("-v", versionOption, PHASE_BEFORE_INIT, 1);
61  registerOption("--version", versionOption, PHASE_BEFORE_INIT, 1);
62  registerOption("-bash", bashOption, PHASE_BEFORE_INIT, 1);
63 
64  registerOption("-setting", settingOption, PHASE_BEFORE_SETTINGS);
65  registerOption("-control", controlOption, PHASE_BEFORE_SETTINGS, 1);
66  registerOption("-script", scriptOption, PHASE_BEFORE_SETTINGS, 1); // correct phase?
67  registerOption("-command", commandOption, PHASE_BEFORE_SETTINGS, 1); // same phase as -script
68  registerOption("-testconfig", testConfigOption, PHASE_BEFORE_SETTINGS, 1);
69 
70  registerOption("-machine", machineOption, PHASE_LOAD_MACHINE);
71 
72  registerFileType({"tcl"}, scriptOption);
73 
74  // At this point all options and file-types must be registered
75  ranges::sort(options, {}, &OptionData::name);
76  ranges::sort(fileTypes, StringOp::caseless{}, &FileTypeData::extension);
77 }
78 
80  const char* str, CLIOption& cliOption, ParsePhase phase, unsigned length)
81 {
82  options.emplace_back(OptionData{str, &cliOption, phase, length});
83 }
84 
86  std::initializer_list<string_view> extensions, CLIFileType& cliFileType)
87 {
88  append(fileTypes, view::transform(extensions,
89  [&](auto& ext) { return FileTypeData{ext, &cliFileType}; }));
90 }
91 
92 bool CommandLineParser::parseOption(
93  const string& arg, span<string>& cmdLine, ParsePhase phase)
94 {
95  if (auto it = ranges::lower_bound(options, arg, {}, &OptionData::name);
96  (it != end(options)) && (it->name == arg)) {
97  // parse option
98  if (it->phase <= phase) {
99  try {
100  it->option->parseOption(arg, cmdLine);
101  return true;
102  } catch (MSXException& e) {
103  throw FatalError(std::move(e).getMessage());
104  }
105  }
106  }
107  return false; // unknown
108 }
109 
110 bool CommandLineParser::parseFileName(const string& arg, span<string>& cmdLine)
111 {
112  if (auto* handler = getFileTypeHandlerForFileName(arg)) {
113  try {
114  // parse filetype
115  handler->parseFileType(arg, cmdLine);
116  return true; // file processed
117  } catch (MSXException& e) {
118  throw FatalError(std::move(e).getMessage());
119  }
120  }
121  return false;
122 }
123 
124 CLIFileType* CommandLineParser::getFileTypeHandlerForFileName(string_view filename) const
125 {
126  auto inner = [&](string_view arg) -> CLIFileType* {
127  string_view extension = FileOperations::getExtension(arg); // includes leading '.' (if any)
128  if (extension.size() <= 1) {
129  return nullptr; // no extension -> no handler
130  }
131  extension.remove_prefix(1);
132 
133  auto it = ranges::lower_bound(fileTypes, extension, StringOp::caseless{},
134  &FileTypeData::extension);
135  StringOp::casecmp cmp;
136  if ((it == end(fileTypes)) || !cmp(it->extension, extension)) {
137  return nullptr; // unknown extension
138  }
139  return it->fileType;
140  };
141 
142  // First try the fileName as we get it from the commandline. This may
143  // be more interesting than the original fileName of a (g)zipped file:
144  // in case of an OMR file for instance, we want to select on the
145  // original extension, and not on the extension inside the (g)zipped
146  // file.
147  auto* result = inner(filename);
148  if (!result) {
149  try {
150  File file(userFileContext().resolve(filename));
151  result = inner(file.getOriginalName());
152  } catch (FileException&) {
153  // ignore
154  }
155  }
156  return result;
157 }
158 
159 void CommandLineParser::parse(int argc, char** argv)
160 {
161  parseStatus = RUN;
162 
163  auto cmdLineBuf = to_vector(view::transform(xrange(1, argc), [&](auto i) {
164  return FileOperations::getConventionalPath(argv[i]);
165  }));
166  span<string> cmdLine(cmdLineBuf);
167  std::vector<string> backupCmdLine;
168 
169  for (ParsePhase phase = PHASE_BEFORE_INIT;
170  (phase <= PHASE_LAST) && (parseStatus != EXIT);
171  phase = static_cast<ParsePhase>(phase + 1)) {
172  switch (phase) {
173  case PHASE_INIT:
174  reactor.init();
175  fileTypeCategoryInfo.emplace(
176  reactor.getOpenMSXInfoCommand(), *this);
177  getInterpreter().init(argv[0]);
178  break;
179  case PHASE_LOAD_SETTINGS:
180  // after -control and -setting has been parsed
181  if (parseStatus != CONTROL) {
182  // if there already is a XML-StdioConnection, we
183  // can't also show plain messages on stdout
184  auto& cliComm = reactor.getGlobalCliComm();
185  cliComm.addListener(std::make_unique<StdioMessages>());
186  }
187  if (!haveSettings) {
188  auto& settingsConfig =
190  // Load default settings file in case the user
191  // didn't specify one.
192  auto context = systemFileContext();
193  string filename = "settings.xml";
194  try {
195  settingsConfig.loadSetting(context, filename);
196  } catch (XMLException& e) {
197  reactor.getCliComm().printWarning(
198  "Loading of settings failed: ",
199  e.getMessage(), "\n"
200  "Reverting to default settings.");
201  } catch (FileException&) {
202  // settings.xml not found
203  } catch (ConfigException& e) {
204  throw FatalError("Error in default settings: ",
205  e.getMessage());
206  }
207  // Consider an attempt to load the settings good enough.
208  haveSettings = true;
209  // Even if parsing failed, use this file for saving,
210  // this forces overwriting a non-setting file.
211  settingsConfig.setSaveFilename(context, filename);
212  }
213  break;
214  case PHASE_DEFAULT_MACHINE: {
215  if (!haveConfig) {
216  // load default config file in case the user didn't specify one
217  const auto& machine =
218  reactor.getMachineSetting().getString();
219  try {
220  reactor.switchMachine(string(machine));
221  } catch (MSXException& e) {
222  reactor.getCliComm().printInfo(
223  "Failed to initialize default machine: ",
224  e.getMessage());
225  // Default machine is broken; fall back to C-BIOS config.
226  const auto& fallbackMachine =
227  reactor.getMachineSetting().getRestoreValue().getString();
228  reactor.getCliComm().printInfo(
229  "Using fallback machine: ", fallbackMachine);
230  try {
231  reactor.switchMachine(string(fallbackMachine));
232  } catch (MSXException& e2) {
233  // Fallback machine failed as well; we're out of options.
234  throw FatalError(std::move(e2).getMessage());
235  }
236  }
237  haveConfig = true;
238  }
239  break;
240  }
241  default:
242  // iterate over all arguments
243  while (!cmdLine.empty()) {
244  string arg = std::move(cmdLine.front());
245  cmdLine = cmdLine.subspan(1);
246  // first try options
247  if (!parseOption(arg, cmdLine, phase)) {
248  // next try the registered filetypes (xml)
249  if ((phase != PHASE_LAST) ||
250  !parseFileName(arg, cmdLine)) {
251  // no option or known file
252  backupCmdLine.push_back(arg);
253  if (auto it = ranges::lower_bound(options, arg, {}, &OptionData::name);
254  (it != end(options)) && (it->name == arg)) {
255  for (unsigned i = 0; i < it->length - 1; ++i) {
256  if (cmdLine.empty()) break;
257  backupCmdLine.push_back(std::move(cmdLine.front()));
258  cmdLine = cmdLine.subspan(1);
259  }
260  }
261  }
262  }
263  }
264  std::swap(backupCmdLine, cmdLineBuf);
265  backupCmdLine.clear();
266  cmdLine = cmdLineBuf;
267  break;
268  }
269  }
270  for (const auto& option : options) {
271  option.option->parseDone();
272  }
273  if (!cmdLine.empty() && (parseStatus != EXIT)) {
274  throw FatalError(
275  "Error parsing command line: ", cmdLine.front(), "\n"
276  "Use \"openmsx -h\" to see a list of available options");
277  }
278 }
279 
281 {
282  return parseStatus == one_of(CONTROL, TEST);
283 }
284 
286 {
287  assert(parseStatus != UNPARSED);
288  return parseStatus;
289 }
290 
292 {
293  return reactor.getMotherBoard();
294 }
295 
297 {
298  return reactor.getGlobalCommandController();
299 }
300 
302 {
303  return reactor.getInterpreter();
304 }
305 
306 
307 // Control option
308 
309 void CommandLineParser::ControlOption::parseOption(
310  const string& option, span<string>& cmdLine)
311 {
312  const auto& fullType = getArgument(option, cmdLine);
313  auto [type, arguments] = StringOp::splitOnFirst(fullType, ':');
314 
315  auto& parser = OUTER(CommandLineParser, controlOption);
316  auto& controller = parser.getGlobalCommandController();
317  auto& distributor = parser.reactor.getEventDistributor();
318  auto& cliComm = parser.reactor.getGlobalCliComm();
319  std::unique_ptr<CliListener> connection;
320  if (type == "stdio") {
321  connection = std::make_unique<StdioConnection>(
322  controller, distributor);
323 #ifdef _WIN32
324  } else if (type == "pipe") {
325  connection = std::make_unique<PipeConnection>(
326  controller, distributor, arguments);
327 #endif
328  } else {
329  throw FatalError("Unknown control type: '", type, '\'');
330  }
331  cliComm.addListener(std::move(connection));
332 
333  parser.parseStatus = CommandLineParser::CONTROL;
334 }
335 
336 string_view CommandLineParser::ControlOption::optionHelp() const
337 {
338  return "Enable external control of openMSX process";
339 }
340 
341 
342 // Script option
343 
344 void CommandLineParser::ScriptOption::parseOption(
345  const string& option, span<string>& cmdLine)
346 {
347  parseFileType(getArgument(option, cmdLine), cmdLine);
348 }
349 
350 string_view CommandLineParser::ScriptOption::optionHelp() const
351 {
352  return "Run extra startup script";
353 }
354 
355 void CommandLineParser::ScriptOption::parseFileType(
356  const string& filename, span<std::string>& /*cmdLine*/)
357 {
358  scripts.push_back(filename);
359 }
360 
361 string_view CommandLineParser::ScriptOption::fileTypeHelp() const
362 {
363  return "Extra Tcl script to run at startup";
364 }
365 
366 string_view CommandLineParser::ScriptOption::fileTypeCategoryName() const
367 {
368  return "script";
369 }
370 
371 
372 // Command option
373 void CommandLineParser::CommandOption::parseOption(
374  const std::string& option, span<std::string>& cmdLine)
375 {
376  commands.push_back(getArgument(option, cmdLine));
377 }
378 
379 std::string_view CommandLineParser::CommandOption::optionHelp() const
380 {
381  return "Run Tcl command at startup (see also -script)";
382 }
383 
384 
385 // Help option
386 
387 static string formatSet(span<const string_view> inputSet, string::size_type columns)
388 {
389  string outString;
390  string::size_type totalLength = 0; // ignore the starting spaces for now
391  for (const auto& temp : inputSet) {
392  if (totalLength == 0) {
393  // first element ?
394  strAppend(outString, " ", temp);
395  totalLength = temp.size();
396  } else {
397  outString += ", ";
398  if ((totalLength + temp.size()) > columns) {
399  strAppend(outString, "\n ", temp);
400  totalLength = temp.size();
401  } else {
402  strAppend(outString, temp);
403  totalLength += 2 + temp.size();
404  }
405  }
406  }
407  if (totalLength < columns) {
408  outString.append(columns - totalLength, ' ');
409  }
410  return outString;
411 }
412 
413 static string formatHelptext(string_view helpText,
414  unsigned maxLength, unsigned indent)
415 {
416  string outText;
417  string_view::size_type index = 0;
418  while (helpText.substr(index).size() > maxLength) {
419  auto pos = helpText.substr(index, maxLength).rfind(' ');
420  if (pos == string_view::npos) {
421  pos = helpText.substr(maxLength).find(' ');
422  if (pos == string_view::npos) {
423  pos = helpText.substr(index).size();
424  }
425  }
426  strAppend(outText, helpText.substr(index, index + pos), '\n',
427  spaces(indent));
428  index = pos + 1;
429  }
430  strAppend(outText, helpText.substr(index));
431  return outText;
432 }
433 
434 // items grouped per common help-text
436 static void printItemMap(const GroupedItems& itemMap)
437 {
438  auto printSet = to_vector(view::transform(itemMap, [](auto& p) {
439  return strCat(formatSet(p.second, 15), ' ',
440  formatHelptext(p.first, 50, 20));
441  }));
442  ranges::sort(printSet);
443  for (auto& s : printSet) {
444  cout << s << '\n';
445  }
446 }
447 
448 
449 // class HelpOption
450 
451 void CommandLineParser::HelpOption::parseOption(
452  const string& /*option*/, span<string>& /*cmdLine*/)
453 {
454  auto& parser = OUTER(CommandLineParser, helpOption);
455  const auto& fullVersion = Version::full();
456  cout << fullVersion << '\n'
457  << string(fullVersion.size(), '=') << "\n"
458  "\n"
459  "usage: openmsx [arguments]\n"
460  " an argument is either an option or a filename\n"
461  "\n"
462  " this is the list of supported options:\n";
463 
464  GroupedItems itemMap;
465  for (const auto& option : parser.options) {
466  const auto& helpText = option.option->optionHelp();
467  if (!helpText.empty()) {
468  itemMap[helpText].push_back(option.name);
469  }
470  }
471  printItemMap(itemMap);
472 
473  cout << "\n"
474  " this is the list of supported file types:\n";
475 
476  itemMap.clear();
477  for (const auto& [extension, fileType] : parser.fileTypes) {
478  itemMap[fileType->fileTypeHelp()].push_back(extension);
479  }
480  printItemMap(itemMap);
481 
482  parser.parseStatus = CommandLineParser::EXIT;
483 }
484 
485 string_view CommandLineParser::HelpOption::optionHelp() const
486 {
487  return "Shows this text";
488 }
489 
490 
491 // class VersionOption
492 
493 void CommandLineParser::VersionOption::parseOption(
494  const string& /*option*/, span<string>& /*cmdLine*/)
495 {
496  cout << Version::full() << "\n"
497  "flavour: " << BUILD_FLAVOUR << "\n"
498  "components: " << BUILD_COMPONENTS << '\n';
499  auto& parser = OUTER(CommandLineParser, versionOption);
500  parser.parseStatus = CommandLineParser::EXIT;
501 }
502 
503 string_view CommandLineParser::VersionOption::optionHelp() const
504 {
505  return "Prints openMSX version and exits";
506 }
507 
508 
509 // Machine option
510 
511 void CommandLineParser::MachineOption::parseOption(
512  const string& option, span<string>& cmdLine)
513 {
514  auto& parser = OUTER(CommandLineParser, machineOption);
515  if (parser.haveConfig) {
516  throw FatalError("Only one machine option allowed");
517  }
518  try {
519  parser.reactor.switchMachine(getArgument(option, cmdLine));
520  } catch (MSXException& e) {
521  throw FatalError(std::move(e).getMessage());
522  }
523  parser.haveConfig = true;
524 }
525 
526 string_view CommandLineParser::MachineOption::optionHelp() const
527 {
528  return "Use machine specified in argument";
529 }
530 
531 
532 // class SettingOption
533 
534 void CommandLineParser::SettingOption::parseOption(
535  const string& option, span<string>& cmdLine)
536 {
537  auto& parser = OUTER(CommandLineParser, settingOption);
538  if (parser.haveSettings) {
539  throw FatalError("Only one setting option allowed");
540  }
541  try {
542  auto& settingsConfig = parser.reactor.getGlobalCommandController().getSettingsConfig();
543  settingsConfig.loadSetting(
544  currentDirFileContext(), getArgument(option, cmdLine));
545  parser.haveSettings = true;
546  } catch (FileException& e) {
547  throw FatalError(std::move(e).getMessage());
548  } catch (ConfigException& e) {
549  throw FatalError(std::move(e).getMessage());
550  }
551 }
552 
553 string_view CommandLineParser::SettingOption::optionHelp() const
554 {
555  return "Load an alternative settings file";
556 }
557 
558 
559 // class TestConfigOption
560 
561 void CommandLineParser::TestConfigOption::parseOption(
562  const string& /*option*/, span<string>& /*cmdLine*/)
563 {
564  auto& parser = OUTER(CommandLineParser, testConfigOption);
565  parser.parseStatus = CommandLineParser::TEST;
566 }
567 
568 string_view CommandLineParser::TestConfigOption::optionHelp() const
569 {
570  return "Test if the specified config works and exit";
571 }
572 
573 // class BashOption
574 
575 void CommandLineParser::BashOption::parseOption(
576  const string& /*option*/, span<string>& cmdLine)
577 {
578  auto& parser = OUTER(CommandLineParser, bashOption);
579  string_view last = cmdLine.empty() ? string_view{} : cmdLine.front();
580  cmdLine = cmdLine.subspan(0, 0); // eat all remaining parameters
581 
582  if (last == "-machine") {
583  for (auto& s : Reactor::getHwConfigs("machines")) {
584  cout << s << '\n';
585  }
586  } else if (StringOp::startsWith(last, "-ext")) {
587  for (auto& s : Reactor::getHwConfigs("extensions")) {
588  cout << s << '\n';
589  }
590  } else if (last == "-romtype") {
591  for (const auto& s : RomInfo::getAllRomTypes()) {
592  cout << s << '\n';
593  }
594  } else {
595  for (const auto& option : parser.options) {
596  cout << option.name << '\n';
597  }
598  }
599  parser.parseStatus = CommandLineParser::EXIT;
600 }
601 
602 string_view CommandLineParser::BashOption::optionHelp() const
603 {
604  return {}; // don't include this option in --help
605 }
606 
607 // class FileTypeCategoryInfoTopic
608 
609 CommandLineParser::FileTypeCategoryInfoTopic::FileTypeCategoryInfoTopic(
610  InfoCommand& openMSXInfoCommand, const CommandLineParser& parser_)
611  : InfoTopic(openMSXInfoCommand, "file_type_category")
612  , parser(parser_)
613 {
614 }
615 
616 void CommandLineParser::FileTypeCategoryInfoTopic::execute(
617  span<const TclObject> tokens, TclObject& result) const
618 {
619  checkNumArgs(tokens, 3, "filename");
620  assert(tokens.size() == 3);
621 
622  // get the category and add it to result
623  std::string_view fileName = tokens[2].getString();
624  if (const auto* handler = parser.getFileTypeHandlerForFileName(fileName)) {
625  result.addListElement(handler->fileTypeCategoryName());
626  } else {
627  result.addListElement("unknown");
628  }
629 }
630 
631 string CommandLineParser::FileTypeCategoryInfoTopic::help(span<const TclObject> /*tokens*/) const
632 {
633  return "Returns the file type category for the given file.";
634 }
635 
636 } // namespace openmsx
Definition: one_of.hh:7
void printInfo(std::string_view message)
Definition: CliComm.cc:5
void printWarning(std::string_view message)
Definition: CliComm.cc:10
CommandLineParser(Reactor &reactor)
bool isHiddenStartup() const
Need to suppress renderer window on startup?
void registerOption(const char *str, CLIOption &cliOption, ParsePhase phase=PHASE_LAST, unsigned length=2)
void parse(int argc, char **argv)
void registerFileType(std::initializer_list< std::string_view > extensions, CLIFileType &cliFileType)
ParseStatus getParseStatus() const
GlobalCommandController & getGlobalCommandController() const
Interpreter & getInterpreter() const
MSXMotherBoard * getMotherBoard() const
void addListener(std::unique_ptr< CliListener > listener)
void init(const char *programName)
Definition: Interpreter.cc:71
const std::string & getMessage() const &
Definition: MSXException.hh:23
Contains the main loop of openMSX.
Definition: Reactor.hh:68
MSXMotherBoard * getMotherBoard() const
Definition: Reactor.cc:374
InfoCommand & getOpenMSXInfoCommand()
Definition: Reactor.cc:327
void switchMachine(const std::string &machine)
Definition: Reactor.cc:420
GlobalCliComm & getGlobalCliComm()
Definition: Reactor.hh:83
CliComm & getCliComm()
Definition: Reactor.cc:312
EnumSetting< int > & getMachineSetting()
Definition: Reactor.hh:90
Interpreter & getInterpreter()
Definition: Reactor.cc:317
GlobalCommandController & getGlobalCommandController()
Definition: Reactor.hh:84
static std::vector< std::string > getHwConfigs(std::string_view type)
Definition: Reactor.cc:332
static auto getAllRomTypes()
Definition: RomInfo.hh:69
static std::string full()
Definition: Version.cc:8
Definition: span.hh:126
constexpr subspan_return_t< Offset, Count > subspan() const
Definition: span.hh:266
constexpr reference front() const
Definition: span.hh:310
constexpr index_type size() const noexcept
Definition: span.hh:296
constexpr bool empty() const noexcept
Definition: span.hh:300
#define COMPONENT_LASERDISC
Definition: components.hh:8
std::pair< string_view, string_view > splitOnFirst(string_view str, string_view chars)
Definition: StringOp.cc:111
bool startsWith(string_view total, string_view part)
Definition: StringOp.cc:29
T length(const vecN< N, T > &x)
Definition: gl_vec.hh:343
std::optional< Context > context
Definition: GLContext.cc:9
const std::string & getConventionalPath(const std::string &path)
Returns the path in conventional path-delimiter.
string_view getExtension(string_view path)
Returns the extension portion of a path.
This file implemented 3 utility functions:
Definition: Autofire.cc:9
const FileContext & systemFileContext()
Definition: FileContext.cc:157
hash_map< string_view, std::vector< string_view >, XXHasher > GroupedItems
constexpr unsigned maxLength
const FileContext & currentDirFileContext()
Definition: FileContext.cc:191
constexpr const char *const filename
FileContext userFileContext(string_view savePath)
Definition: FileContext.cc:173
void sort(RandomAccessRange &&range)
Definition: ranges.hh:34
auto lower_bound(ForwardRange &&range, const T &value, Compare comp={}, Proj proj={})
Definition: ranges.hh:85
constexpr auto transform(Range &&range, UnaryOp op)
Definition: view.hh:392
#define OUTER(type, member)
Definition: outer.hh:41
auto to_vector(Range &&range) -> std::vector< detail::ToVectorType< T, decltype(std::begin(range))>>
Definition: stl.hh:275
strCatImpl::ConcatSpaces spaces(size_t n)
Definition: strCat.hh:700
std::string strCat(Ts &&...ts)
Definition: strCat.hh:591
void strAppend(std::string &result, Ts &&...ts)
Definition: strCat.hh:669
constexpr auto xrange(T e)
Definition: xrange.hh:155
constexpr auto end(const zstring_view &x)
Definition: zstring_view.hh:84