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