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