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 {
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_DEFAULT_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  connection = make_unique<PipeConnection>(
333  controller, distributor, arguments);
334 #endif
335  } else {
336  throw FatalError("Unknown control type: '" + type + '\'');
337  }
338  cliComm.addListener(std::move(connection));
339 
340  parser.parseStatus = CommandLineParser::CONTROL;
341 }
342 
343 string_ref CommandLineParser::ControlOption::optionHelp() const
344 {
345  return "Enable external control of openMSX process";
346 }
347 
348 
349 // Script option
350 
351 void CommandLineParser::ScriptOption::parseOption(
352  const string& option, array_ref<string>& cmdLine)
353 {
354  parseFileType(getArgument(option, cmdLine), cmdLine);
355 }
356 
357 string_ref CommandLineParser::ScriptOption::optionHelp() const
358 {
359  return "Run extra startup script";
360 }
361 
362 void CommandLineParser::ScriptOption::parseFileType(
363  const string& filename, array_ref<std::string>& /*cmdLine*/)
364 {
365  scripts.push_back(filename);
366 }
367 
368 string_ref CommandLineParser::ScriptOption::fileTypeHelp() const
369 {
370  return "Extra Tcl script to run at startup";
371 }
372 
373 
374 // Help option
375 
376 static string formatSet(const vector<string_ref>& inputSet, string::size_type columns)
377 {
378  StringOp::Builder outString;
379  string::size_type totalLength = 0; // ignore the starting spaces for now
380  for (auto& temp : inputSet) {
381  if (totalLength == 0) {
382  // first element ?
383  outString << " " << temp;
384  totalLength = temp.size();
385  } else {
386  outString << ", ";
387  if ((totalLength + temp.size()) > columns) {
388  outString << "\n " << temp;
389  totalLength = temp.size();
390  } else {
391  outString << temp;
392  totalLength += 2 + temp.size();
393  }
394  }
395  }
396  if (totalLength < columns) {
397  outString << string(columns - totalLength, ' ');
398  }
399  return outString;
400 }
401 
402 static string formatHelptext(string_ref helpText,
403  unsigned maxLength, unsigned indent)
404 {
405  string outText;
406  string_ref::size_type index = 0;
407  while (helpText.substr(index).size() > maxLength) {
408  auto pos = helpText.substr(index, maxLength).rfind(' ');
409  if (pos == string_ref::npos) {
410  pos = helpText.substr(maxLength).find(' ');
411  if (pos == string_ref::npos) {
412  pos = helpText.substr(index).size();
413  }
414  }
415  outText += helpText.substr(index, index + pos) + '\n' +
416  string(indent, ' ');
417  index = pos + 1;
418  }
419  string_ref t = helpText.substr(index);
420  outText.append(t.data(), t.size());
421  return outText;
422 }
423 
424 // items grouped per common help-text
426 static void printItemMap(const GroupedItems& itemMap)
427 {
428  vector<string> printSet;
429  for (auto& p : itemMap) {
430  printSet.push_back(formatSet(p.second, 15) + ' ' +
431  formatHelptext(p.first, 50, 20));
432  }
433  sort(begin(printSet), end(printSet));
434  for (auto& s : printSet) {
435  cout << s << endl;
436  }
437 }
438 
439 
440 // class HelpOption
441 
442 void CommandLineParser::HelpOption::parseOption(
443  const string& /*option*/, array_ref<string>& /*cmdLine*/)
444 {
445  auto& parser = OUTER(CommandLineParser, helpOption);
446  const auto& fullVersion = Version::full();
447  cout << fullVersion << endl;
448  cout << string(fullVersion.size(), '=') << endl;
449  cout << endl;
450  cout << "usage: openmsx [arguments]" << endl;
451  cout << " an argument is either an option or a filename" << endl;
452  cout << endl;
453  cout << " this is the list of supported options:" << endl;
454 
455  GroupedItems itemMap;
456  for (auto& p : parser.options) {
457  const auto& helpText = p.second.option->optionHelp();
458  if (!helpText.empty()) {
459  itemMap[helpText].push_back(p.first);
460  }
461  }
462  printItemMap(itemMap);
463 
464  cout << endl;
465  cout << " this is the list of supported file types:" << endl;
466 
467  itemMap.clear();
468  for (auto& p : parser.fileTypes) {
469  itemMap[p.second->fileTypeHelp()].push_back(p.first);
470  }
471  printItemMap(itemMap);
472 
473  parser.parseStatus = CommandLineParser::EXIT;
474 }
475 
476 string_ref CommandLineParser::HelpOption::optionHelp() const
477 {
478  return "Shows this text";
479 }
480 
481 
482 // class VersionOption
483 
484 void CommandLineParser::VersionOption::parseOption(
485  const string& /*option*/, array_ref<string>& /*cmdLine*/)
486 {
487  cout << Version::full() << endl;
488  cout << "flavour: " << BUILD_FLAVOUR << endl;
489  cout << "components: " << BUILD_COMPONENTS << endl;
490  auto& parser = OUTER(CommandLineParser, versionOption);
491  parser.parseStatus = CommandLineParser::EXIT;
492 }
493 
494 string_ref CommandLineParser::VersionOption::optionHelp() const
495 {
496  return "Prints openMSX version and exits";
497 }
498 
499 
500 // Machine option
501 
502 void CommandLineParser::MachineOption::parseOption(
503  const string& option, array_ref<string>& cmdLine)
504 {
505  auto& parser = OUTER(CommandLineParser, machineOption);
506  if (parser.haveConfig) {
507  throw FatalError("Only one machine option allowed");
508  }
509  try {
510  parser.reactor.switchMachine(getArgument(option, cmdLine));
511  } catch (MSXException& e) {
512  throw FatalError(e.getMessage());
513  }
514  parser.haveConfig = true;
515 }
516 
517 string_ref CommandLineParser::MachineOption::optionHelp() const
518 {
519  return "Use machine specified in argument";
520 }
521 
522 
523 // class SettingOption
524 
525 void CommandLineParser::SettingOption::parseOption(
526  const string& option, array_ref<string>& cmdLine)
527 {
528  auto& parser = OUTER(CommandLineParser, settingOption);
529  if (parser.haveSettings) {
530  throw FatalError("Only one setting option allowed");
531  }
532  try {
533  auto& settingsConfig = parser.reactor.getGlobalCommandController().getSettingsConfig();
534  settingsConfig.loadSetting(
535  currentDirFileContext(), getArgument(option, cmdLine));
536  parser.haveSettings = true;
537  } catch (FileException& e) {
538  throw FatalError(e.getMessage());
539  } catch (ConfigException& e) {
540  throw FatalError(e.getMessage());
541  }
542 }
543 
544 string_ref CommandLineParser::SettingOption::optionHelp() const
545 {
546  return "Load an alternative settings file";
547 }
548 
549 
550 // class NoPBOOption
551 
552 void CommandLineParser::NoPBOOption::parseOption(
553  const string& /*option*/, array_ref<string>& /*cmdLine*/)
554 {
555  #if COMPONENT_GL
556  cout << "Disabling PBO" << endl;
558  #endif
559 }
560 
561 string_ref CommandLineParser::NoPBOOption::optionHelp() const
562 {
563  return "Disables usage of openGL PBO (for debugging)";
564 }
565 
566 
567 // class TestConfigOption
568 
569 void CommandLineParser::TestConfigOption::parseOption(
570  const string& /*option*/, array_ref<string>& /*cmdLine*/)
571 {
572  auto& parser = OUTER(CommandLineParser, testConfigOption);
573  parser.parseStatus = CommandLineParser::TEST;
574 }
575 
576 string_ref CommandLineParser::TestConfigOption::optionHelp() const
577 {
578  return "Test if the specified config works and exit";
579 }
580 
581 // class BashOption
582 
583 void CommandLineParser::BashOption::parseOption(
584  const string& /*option*/, array_ref<string>& cmdLine)
585 {
586  auto& parser = OUTER(CommandLineParser, bashOption);
587  string last = cmdLine.empty() ? "" : cmdLine.front();
588  cmdLine.clear(); // eat all remaining parameters
589 
590  if (last == "-machine") {
591  for (auto& s : Reactor::getHwConfigs("machines")) {
592  cout << s << '\n';
593  }
594  } else if (StringOp::startsWith(last, "-ext")) {
595  for (auto& s : Reactor::getHwConfigs("extensions")) {
596  cout << s << '\n';
597  }
598  } else if (last == "-romtype") {
599  for (auto& s : RomInfo::getAllRomTypes()) {
600  cout << s << '\n';
601  }
602  } else {
603  for (auto& p : parser.options) {
604  cout << p.first << '\n';
605  }
606  }
607  parser.parseStatus = CommandLineParser::EXIT;
608 }
609 
610 string_ref CommandLineParser::BashOption::optionHelp() const
611 {
612  return ""; // don't include this option in --help
613 }
614 
615 } // 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: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
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:14
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: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 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:138
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:370
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:23
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