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