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