CommandLineParser.cc 17 KB

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