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 "GLUtil.hh"
19 #include "Reactor.hh"
20 #include "RomInfo.hh"
21 #include "hash_map.hh"
22 #include "memory.hh"
23 #include "outer.hh"
24 #include "stl.hh"
25 #include "xxhash.hh"
26 #include "build-info.hh"
27 #include <cassert>
28 #include <iostream>
29 
30 using std::cout;
31 using std::endl;
32 using std::string;
33 using std::vector;
34 
35 namespace openmsx {
36 
37 // class CommandLineParser
38 
41 
43  : reactor(reactor_)
44  , msxRomCLI(*this)
45  , cliExtension(*this)
46  , replayCLI(*this)
47  , saveStateCLI(*this)
48  , cassettePlayerCLI(*this)
50  , laserdiscPlayerCLI(*this)
51 #endif
52  , diskImageCLI(*this)
53  , hdImageCLI(*this)
54  , cdImageCLI(*this)
55  , parseStatus(UNPARSED)
56 {
57  haveConfig = false;
58  haveSettings = false;
59 
60  registerOption("-h", helpOption, PHASE_BEFORE_INIT, 1);
61  registerOption("--help", helpOption, PHASE_BEFORE_INIT, 1);
62  registerOption("-v", versionOption, PHASE_BEFORE_INIT, 1);
63  registerOption("--version", versionOption, PHASE_BEFORE_INIT, 1);
64  registerOption("-bash", bashOption, PHASE_BEFORE_INIT, 1);
65 
66  registerOption("-setting", settingOption, PHASE_BEFORE_SETTINGS);
67  registerOption("-control", controlOption, PHASE_BEFORE_SETTINGS, 1);
68  registerOption("-script", scriptOption, PHASE_BEFORE_SETTINGS, 1); // correct phase?
69  #if COMPONENT_GL
70  registerOption("-nopbo", noPBOOption, PHASE_BEFORE_SETTINGS, 1);
71  #endif
72  registerOption("-testconfig", testConfigOption, PHASE_BEFORE_SETTINGS, 1);
73 
74  registerOption("-machine", machineOption, PHASE_BEFORE_MACHINE);
75 
76  registerFileType("tcl", scriptOption);
77 
78  // At this point all options and file-types must be registered
79  sort(begin(options), end(options), CmpOptions());
80  sort(begin(fileTypes), end(fileTypes), CmpFileTypes());
81 }
82 
84 {
85 }
86 
88  const char* str, CLIOption& cliOption, ParsePhase phase, unsigned length)
89 {
90  options.emplace_back(str, OptionData{&cliOption, phase, length});
91 }
92 
94  string_ref extensions, CLIFileType& cliFileType)
95 {
96  for (auto& ext: StringOp::split(extensions, ',')) {
97  fileTypes.emplace_back(ext, &cliFileType);
98  }
99 }
100 
101 bool CommandLineParser::parseOption(
102  const string& arg, array_ref<string>& cmdLine, ParsePhase phase)
103 {
104  auto it = lower_bound(begin(options), end(options), arg, CmpOptions());
105  if ((it != end(options)) && (it->first == arg)) {
106  // parse option
107  if (it->second.phase <= phase) {
108  try {
109  it->second.option->parseOption(arg, cmdLine);
110  return true;
111  } catch (MSXException& e) {
112  throw FatalError(e.getMessage());
113  }
114  }
115  }
116  return false; // unknown
117 }
118 
119 bool CommandLineParser::parseFileName(const string& arg, array_ref<string>& cmdLine)
120 {
121  // First try the fileName as we get it from the commandline. This may
122  // be more interesting than the original fileName of a (g)zipped file:
123  // in case of an OMR file for instance, we want to select on the
124  // original extension, and not on the extension inside the gzipped
125  // file.
126  bool processed = parseFileNameInner(arg, arg, cmdLine);
127  if (!processed) {
128  try {
129  File file(userFileContext().resolve(arg));
130  string originalName = file.getOriginalName();
131  processed = parseFileNameInner(originalName, arg, cmdLine);
132  } catch (FileException&) {
133  // ignore
134  }
135  }
136  return processed;
137 }
138 
139 bool CommandLineParser::parseFileNameInner(const string& name, const string& originalPath, array_ref<string>& cmdLine)
140 {
141  string_ref extension = FileOperations::getExtension(name);
142  if (extension.empty()) {
143  return false; // no extension
144  }
145 
146  auto it = lower_bound(begin(fileTypes), end(fileTypes), extension,
147  CmpFileTypes());
148  StringOp::casecmp cmp;
149  if ((it == end(fileTypes)) || !cmp(it->first, extension)) {
150  return false; // unknown extension
151  }
152 
153  try {
154  // parse filetype
155  it->second->parseFileType(originalPath, cmdLine);
156  return true; // file processed
157  } catch (MSXException& e) {
158  throw FatalError(e.getMessage());
159  }
160 }
161 
162 void CommandLineParser::parse(int argc, char** argv)
163 {
164  parseStatus = RUN;
165 
166  vector<string> cmdLineBuf;
167  for (auto i : xrange(1, argc)) {
168  cmdLineBuf.push_back(FileOperations::getConventionalPath(argv[i]));
169  }
170  array_ref<string> cmdLine(cmdLineBuf);
171  vector<string> backupCmdLine;
172 
173  for (ParsePhase phase = PHASE_BEFORE_INIT;
174  (phase <= PHASE_LAST) && (parseStatus != EXIT);
175  phase = static_cast<ParsePhase>(phase + 1)) {
176  switch (phase) {
177  case PHASE_INIT:
178  reactor.init();
179  getInterpreter().init(argv[0]);
180  break;
181  case PHASE_LOAD_SETTINGS:
182  // after -control and -setting has been parsed
183  if (parseStatus != CONTROL) {
184  // if there already is a XML-StdioConnection, we
185  // can't also show plain messages on stdout
186  auto& cliComm = reactor.getGlobalCliComm();
187  cliComm.addListener(make_unique<StdioMessages>());
188  }
189  if (!haveSettings) {
190  auto& settingsConfig =
192  // Load default settings file in case the user
193  // didn't specify one.
194  auto context = systemFileContext();
195  string filename = "settings.xml";
196  try {
197  settingsConfig.loadSetting(context, filename);
198  } catch (XMLException& e) {
199  reactor.getCliComm().printWarning(
200  "Loading of settings failed: " +
201  e.getMessage() + "\n"
202  "Reverting to default settings.");
203  } catch (FileException&) {
204  // settings.xml not found
205  } catch (ConfigException& e) {
206  throw FatalError("Error in default settings: "
207  + e.getMessage());
208  }
209  // Consider an attempt to load the settings good enough.
210  haveSettings = true;
211  // Even if parsing failed, use this file for saving,
212  // this forces overwriting a non-setting file.
213  settingsConfig.setSaveFilename(context, filename);
214  }
215  break;
216  case PHASE_LOAD_MACHINE: {
217  if (!haveConfig) {
218  // load default config file in case the user didn't specify one
219  const auto& machine =
220  reactor.getMachineSetting().getString();
221  try {
222  reactor.switchMachine(machine.str());
223  } catch (MSXException& e) {
224  reactor.getCliComm().printInfo(
225  "Failed to initialize default machine: " + e.getMessage());
226  // Default machine is broken; fall back to C-BIOS config.
227  const auto& fallbackMachine =
229  reactor.getCliComm().printInfo("Using fallback machine: " + fallbackMachine);
230  try {
231  reactor.switchMachine(fallbackMachine.str());
232  } catch (MSXException& e2) {
233  // Fallback machine failed as well; we're out of options.
234  throw FatalError(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.pop_front();
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  auto it = lower_bound(begin(options), end(options), arg, CmpOptions());
254  if ((it != end(options)) && (it->first == arg)) {
255  for (unsigned i = 0; i < it->second.length - 1; ++i) {
256  if (!cmdLine.empty()) {
257  backupCmdLine.push_back(std::move(cmdLine.front()));
258  cmdLine.pop_front();
259  }
260  }
261  }
262  }
263  }
264  }
265  std::swap(backupCmdLine, cmdLineBuf);
266  backupCmdLine.clear();
267  cmdLine = cmdLineBuf;
268  break;
269  }
270  }
271  for (auto& p : options) {
272  p.second.option->parseDone();
273  }
274  if (!cmdLine.empty() && (parseStatus != EXIT)) {
275  throw FatalError(
276  "Error parsing command line: " + cmdLine.front() + "\n" +
277  "Use \"openmsx -h\" to see a list of available options" );
278  }
279 }
280 
282 {
283  return (parseStatus == CONTROL) || (parseStatus == TEST);
284 }
285 
287 {
288  assert(parseStatus != UNPARSED);
289  return parseStatus;
290 }
291 
293 {
294  return scriptOption.scripts;
295 }
296 
298 {
299  return reactor.getMotherBoard();
300 }
301 
303 {
304  return reactor.getGlobalCommandController();
305 }
306 
308 {
309  return reactor.getInterpreter();
310 }
311 
312 
313 // Control option
314 
315 void CommandLineParser::ControlOption::parseOption(
316  const string& option, array_ref<string>& cmdLine)
317 {
318  const auto& fullType = getArgument(option, cmdLine);
319  string_ref type, arguments;
320  StringOp::splitOnFirst(fullType, ':', type, arguments);
321 
322  auto& parser = OUTER(CommandLineParser, controlOption);
323  auto& controller = parser.getGlobalCommandController();
324  auto& distributor = parser.reactor.getEventDistributor();
325  auto& cliComm = parser.reactor.getGlobalCliComm();
326  std::unique_ptr<CliListener> connection;
327  if (type == "stdio") {
328  connection = make_unique<StdioConnection>(
329  controller, distributor);
330 #ifdef _WIN32
331  } else if (type == "pipe") {
332  OSVERSIONINFO info;
333  info.dwOSVersionInfoSize = sizeof(info);
334  GetVersionEx(&info);
335  if (info.dwPlatformId == VER_PLATFORM_WIN32_NT) {
336  connection = make_unique<PipeConnection>(
337  controller, distributor, arguments);
338  } else {
339  throw FatalError("Pipes are not supported on this "
340  "version of Windows");
341  }
342 #endif
343  } else {
344  throw FatalError("Unknown control type: '" + type + '\'');
345  }
346  cliComm.addListener(std::move(connection));
347 
348  parser.parseStatus = CommandLineParser::CONTROL;
349 }
350 
351 string_ref CommandLineParser::ControlOption::optionHelp() const
352 {
353  return "Enable external control of openMSX process";
354 }
355 
356 
357 // Script option
358 
359 void CommandLineParser::ScriptOption::parseOption(
360  const string& option, array_ref<string>& cmdLine)
361 {
362  parseFileType(getArgument(option, cmdLine), cmdLine);
363 }
364 
365 string_ref CommandLineParser::ScriptOption::optionHelp() const
366 {
367  return "Run extra startup script";
368 }
369 
370 void CommandLineParser::ScriptOption::parseFileType(
371  const string& filename, array_ref<std::string>& /*cmdLine*/)
372 {
373  scripts.push_back(filename);
374 }
375 
376 string_ref CommandLineParser::ScriptOption::fileTypeHelp() const
377 {
378  return "Extra Tcl script to run at startup";
379 }
380 
381 
382 // Help option
383 
384 static string formatSet(const vector<string_ref>& inputSet, string::size_type columns)
385 {
386  StringOp::Builder outString;
387  string::size_type totalLength = 0; // ignore the starting spaces for now
388  for (auto& temp : inputSet) {
389  if (totalLength == 0) {
390  // first element ?
391  outString << " " << temp;
392  totalLength = temp.size();
393  } else {
394  outString << ", ";
395  if ((totalLength + temp.size()) > columns) {
396  outString << "\n " << temp;
397  totalLength = temp.size();
398  } else {
399  outString << temp;
400  totalLength += 2 + temp.size();
401  }
402  }
403  }
404  if (totalLength < columns) {
405  outString << string(columns - totalLength, ' ');
406  }
407  return outString;
408 }
409 
410 static string formatHelptext(string_ref helpText,
411  unsigned maxLength, unsigned indent)
412 {
413  string outText;
414  string_ref::size_type index = 0;
415  while (helpText.substr(index).size() > maxLength) {
416  auto pos = helpText.substr(index, maxLength).rfind(' ');
417  if (pos == string_ref::npos) {
418  pos = helpText.substr(maxLength).find(' ');
419  if (pos == string_ref::npos) {
420  pos = helpText.substr(index).size();
421  }
422  }
423  outText += helpText.substr(index, index + pos) + '\n' +
424  string(indent, ' ');
425  index = pos + 1;
426  }
427  string_ref t = helpText.substr(index);
428  outText.append(t.data(), t.size());
429  return outText;
430 }
431 
432 // items grouped per common help-text
434 static void printItemMap(const GroupedItems& itemMap)
435 {
436  vector<string> printSet;
437  for (auto& p : itemMap) {
438  printSet.push_back(formatSet(p.second, 15) + ' ' +
439  formatHelptext(p.first, 50, 20));
440  }
441  sort(begin(printSet), end(printSet));
442  for (auto& s : printSet) {
443  cout << s << endl;
444  }
445 }
446 
447 
448 // class HelpOption
449 
450 void CommandLineParser::HelpOption::parseOption(
451  const string& /*option*/, array_ref<string>& /*cmdLine*/)
452 {
453  auto& parser = OUTER(CommandLineParser, helpOption);
454  const auto& fullVersion = Version::full();
455  cout << fullVersion << endl;
456  cout << string(fullVersion.size(), '=') << endl;
457  cout << endl;
458  cout << "usage: openmsx [arguments]" << endl;
459  cout << " an argument is either an option or a filename" << endl;
460  cout << endl;
461  cout << " this is the list of supported options:" << endl;
462 
463  GroupedItems itemMap;
464  for (auto& p : parser.options) {
465  const auto& helpText = p.second.option->optionHelp();
466  if (!helpText.empty()) {
467  itemMap[helpText].push_back(p.first);
468  }
469  }
470  printItemMap(itemMap);
471 
472  cout << endl;
473  cout << " this is the list of supported file types:" << endl;
474 
475  itemMap.clear();
476  for (auto& p : parser.fileTypes) {
477  itemMap[p.second->fileTypeHelp()].push_back(p.first);
478  }
479  printItemMap(itemMap);
480 
481  parser.parseStatus = CommandLineParser::EXIT;
482 }
483 
484 string_ref 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*/, array_ref<string>& /*cmdLine*/)
494 {
495  cout << Version::full() << endl;
496  cout << "flavour: " << BUILD_FLAVOUR << endl;
497  cout << "components: " << BUILD_COMPONENTS << endl;
498  auto& parser = OUTER(CommandLineParser, versionOption);
499  parser.parseStatus = CommandLineParser::EXIT;
500 }
501 
502 string_ref 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, array_ref<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(e.getMessage());
521  }
522  parser.haveConfig = true;
523 }
524 
525 string_ref 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, array_ref<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(e.getMessage());
547  } catch (ConfigException& e) {
548  throw FatalError(e.getMessage());
549  }
550 }
551 
552 string_ref CommandLineParser::SettingOption::optionHelp() const
553 {
554  return "Load an alternative settings file";
555 }
556 
557 
558 // class NoPBOOption
559 
560 void CommandLineParser::NoPBOOption::parseOption(
561  const string& /*option*/, array_ref<string>& /*cmdLine*/)
562 {
563  #if COMPONENT_GL
564  cout << "Disabling PBO" << endl;
566  #endif
567 }
568 
569 string_ref CommandLineParser::NoPBOOption::optionHelp() const
570 {
571  return "Disables usage of openGL PBO (for debugging)";
572 }
573 
574 
575 // class TestConfigOption
576 
577 void CommandLineParser::TestConfigOption::parseOption(
578  const string& /*option*/, array_ref<string>& /*cmdLine*/)
579 {
580  auto& parser = OUTER(CommandLineParser, testConfigOption);
581  parser.parseStatus = CommandLineParser::TEST;
582 }
583 
584 string_ref CommandLineParser::TestConfigOption::optionHelp() const
585 {
586  return "Test if the specified config works and exit";
587 }
588 
589 // class BashOption
590 
591 void CommandLineParser::BashOption::parseOption(
592  const string& /*option*/, array_ref<string>& cmdLine)
593 {
594  auto& parser = OUTER(CommandLineParser, bashOption);
595  string last = cmdLine.empty() ? "" : cmdLine.front();
596  cmdLine.clear(); // eat all remaining parameters
597 
598  if (last == "-machine") {
599  for (auto& s : Reactor::getHwConfigs("machines")) {
600  cout << s << '\n';
601  }
602  } else if (StringOp::startsWith(last, "-ext")) {
603  for (auto& s : Reactor::getHwConfigs("extensions")) {
604  cout << s << '\n';
605  }
606  } else if (last == "-romtype") {
607  for (auto& s : RomInfo::getAllRomTypes()) {
608  cout << s << '\n';
609  }
610  } else {
611  for (auto& p : parser.options) {
612  cout << p.first << '\n';
613  }
614  }
615  parser.parseStatus = CommandLineParser::EXIT;
616 }
617 
618 string_ref CommandLineParser::BashOption::optionHelp() const
619 {
620  return ""; // don't include this option in --help
621 }
622 
623 } // namespace openmsx
T length(const vecN< N, T > &x)
Definition: gl_vec.hh:322
Contains the main loop of openMSX.
Definition: Reactor.hh:61
void pop_front()
Definition: array_ref.hh:91
string_ref::const_iterator end(const string_ref &x)
Definition: string_ref.hh:167
GlobalCommandController & getGlobalCommandController()
Definition: Reactor.hh:78
MSXMotherBoard * getMotherBoard() const
const Scripts & getStartupScripts() const
bool isHiddenStartup() const
Need to suppress renderer window on startup?
void addListener(std::unique_ptr< CliListener > listener)
static std::vector< std::string > getHwConfigs(string_ref type)
Definition: Reactor.cc:285
string_ref getString() const
Definition: TclObject.cc:139
string getConventionalPath(string_ref path)
Returns the path in conventional path-delimiter.
void splitOnFirst(string_ref str, string_ref chars, string_ref &first, string_ref &last)
Definition: StringOp.cc:324
FileContext systemFileContext()
Definition: FileContext.cc:149
void init(const char *programName)
Definition: Interpreter.cc:70
void printWarning(string_ref message)
Definition: CliComm.cc:28
size_type find(string_ref s) const
Definition: string_ref.cc:38
This class implements a subset of the proposal for std::string_ref (proposed for the next c++ standar...
Definition: string_ref.hh:18
static std::vector< string_ref > getAllRomTypes()
Definition: RomInfo.cc:199
CommandLineParser(Reactor &reactor)
const T & front() const
Definition: array_ref.hh:69
size_type size() const
Definition: string_ref.hh:55
EnumSetting< int > & getMachineSetting()
Definition: Reactor.hh:84
const char * data() const
Definition: string_ref.hh:68
size_type rfind(string_ref s) const
Definition: string_ref.cc:65
#define COMPONENT_LASERDISC
Definition: components.hh:8
void clear()
Definition: array_ref.hh:74
bool startsWith(string_ref total, string_ref part)
Definition: StringOp.cc:242
void clear()
Definition: hash_set.hh:542
This class implements a subset of the proposal for std::array_ref (proposed for the next c++ standard...
Definition: array_ref.hh:19
std::unique_ptr< Context > context
Definition: GLContext.cc:9
bool empty() const
Definition: array_ref.hh:62
MSXMotherBoard * getMotherBoard() const
Definition: Reactor.cc:328
std::vector< std::string > Scripts
void registerOption(const char *str, CLIOption &cliOption, ParsePhase phase=PHASE_LAST, unsigned length=2)
const std::string & getMessage() const
Definition: MSXException.hh:14
size_t size_type
Definition: string_ref.hh:21
TclObject getRestoreValue() const finaloverride
Get the value that will be set after a Tcl &#39;unset&#39; command.
Definition: Setting.hh:158
CmpTupleElement< 0, StringOp::caseless > CmpFileTypes
Thanks to enen for testing this on a real cartridge:
Definition: Autofire.cc:5
static const size_type npos
Definition: string_ref.hh:26
GlobalCliComm & getGlobalCliComm()
Definition: Reactor.hh:77
void registerFileType(string_ref extensions, CLIFileType &cliFileType)
const std::string getOriginalName()
Get Original filename for this object.
Definition: File.cc:138
FileContext currentDirFileContext()
Definition: FileContext.cc:177
static bool enabled
Global switch to disable pixel buffers using the "-nopbo" option.
Definition: GLUtil.hh:141
vector< string_ref > split(string_ref str, char chars)
Definition: StringOp.cc:370
string_ref getString() const
Definition: EnumSetting.hh:109
FileContext userFileContext(string_ref savePath)
Definition: FileContext.cc:161
GlobalCommandController & getGlobalCommandController() const
string_ref getExtension(string_ref path)
Returns the extension portion of a path.
void parse(int argc, char **argv)
static std::string full()
Definition: Version.cc:7
void printInfo(string_ref message)
Definition: CliComm.cc:23
ParseStatus getParseStatus() const
CliComm & getCliComm()
Definition: Reactor.cc:265
Interpreter & getInterpreter() const
string_ref substr(size_type pos, size_type n=npos) const
Definition: string_ref.cc:32
void switchMachine(const std::string &machine)
Definition: Reactor.cc:387
#define OUTER(type, member)
Definition: outer.hh:38
string_ref::const_iterator begin(const string_ref &x)
Definition: string_ref.hh:166
LessTupleElement< 0 > CmpOptions
bool empty() const
Definition: string_ref.hh:56
XRange< T > xrange(T e)
Definition: xrange.hh:98
Interpreter & getInterpreter()
Definition: Reactor.cc:270