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