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