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