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(std::array<std::string_view, 1>{"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(str, &cliOption, phase, length);
79}
80
82 std::span<const 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 auto fallbackMachine = std::string(
219 reactor.getCliComm().printInfo(
220 "Using fallback machine: ", fallbackMachine);
221 try {
222 reactor.switchMachine(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 assert(parseStatus != UNPARSED);
273 return parseStatus;
274}
275
277{
278 return reactor.getMotherBoard();
279}
280
285
287{
288 return reactor.getInterpreter();
289}
290
291
292// Control option
293
294void CommandLineParser::ControlOption::parseOption(
295 const string& option, std::span<string>& cmdLine)
296{
297 const auto& fullType = getArgument(option, cmdLine);
298 auto [type, arguments] = StringOp::splitOnFirst(fullType, ':');
299
300 auto& parser = OUTER(CommandLineParser, controlOption);
301 auto& controller = parser.getGlobalCommandController();
302 auto& distributor = parser.reactor.getEventDistributor();
303 auto& cliComm = parser.reactor.getGlobalCliComm();
304 std::unique_ptr<CliListener> connection;
305 if (type == "stdio") {
306 connection = std::make_unique<StdioConnection>(
307 controller, distributor);
308#ifdef _WIN32
309 } else if (type == "pipe") {
310 connection = std::make_unique<PipeConnection>(
311 controller, distributor, arguments);
312#endif
313 } else {
314 throw FatalError("Unknown control type: '", type, '\'');
315 }
316 cliComm.addListener(std::move(connection));
317
318 parser.parseStatus = CommandLineParser::CONTROL;
319}
320
321string_view CommandLineParser::ControlOption::optionHelp() const
322{
323 return "Enable external control of openMSX process";
324}
325
326
327// Script option
328
329void CommandLineParser::ScriptOption::parseOption(
330 const string& option, std::span<string>& cmdLine)
331{
332 parseFileType(getArgument(option, cmdLine), cmdLine);
333}
334
335string_view CommandLineParser::ScriptOption::optionHelp() const
336{
337 return "Run extra startup script";
338}
339
340void CommandLineParser::ScriptOption::parseFileType(
341 const string& filename, std::span<std::string>& /*cmdLine*/)
342{
343 scripts.push_back(filename);
344}
345
346string_view CommandLineParser::ScriptOption::fileTypeHelp() const
347{
348 return "Extra Tcl script to run at startup";
349}
350
351string_view CommandLineParser::ScriptOption::fileTypeCategoryName() const
352{
353 return "script";
354}
355
356
357// Command option
358void CommandLineParser::CommandOption::parseOption(
359 const std::string& option, std::span<std::string>& cmdLine)
360{
361 commands.push_back(getArgument(option, cmdLine));
362}
363
364std::string_view CommandLineParser::CommandOption::optionHelp() const
365{
366 return "Run Tcl command at startup (see also -script)";
367}
368
369
370// Help option
371
372static string formatSet(std::span<const string_view> inputSet, string::size_type columns)
373{
374 string outString;
375 string::size_type totalLength = 0; // ignore the starting spaces for now
376 for (const auto& temp : inputSet) {
377 if (totalLength == 0) {
378 // first element ?
379 strAppend(outString, " ", temp);
380 totalLength = temp.size();
381 } else {
382 outString += ", ";
383 if ((totalLength + temp.size()) > columns) {
384 strAppend(outString, "\n ", temp);
385 totalLength = temp.size();
386 } else {
387 strAppend(outString, temp);
388 totalLength += 2 + temp.size();
389 }
390 }
391 }
392 if (totalLength < columns) {
393 outString.append(columns - totalLength, ' ');
394 }
395 return outString;
396}
397
398static string formatHelpText(string_view helpText,
399 unsigned maxLength, unsigned indent)
400{
401 string outText;
402 string_view::size_type index = 0;
403 while (helpText.substr(index).size() > maxLength) {
404 auto pos = helpText.substr(index, maxLength).rfind(' ');
405 if (pos == string_view::npos) {
406 pos = helpText.substr(maxLength).find(' ');
407 if (pos == string_view::npos) {
408 pos = helpText.substr(index).size();
409 }
410 }
411 strAppend(outText, helpText.substr(index, index + pos), '\n',
412 spaces(indent));
413 index = pos + 1;
414 }
415 strAppend(outText, helpText.substr(index));
416 return outText;
417}
418
419// items grouped per common help-text
421static void printItemMap(const GroupedItems& itemMap)
422{
423 auto printSet = to_vector(view::transform(itemMap, [](auto& p) {
424 return strCat(formatSet(p.second, 15), ' ',
425 formatHelpText(p.first, 50, 20));
426 }));
427 ranges::sort(printSet);
428 for (const auto& s : printSet) {
429 cout << s << '\n';
430 }
431}
432
433
434// class HelpOption
435
436void CommandLineParser::HelpOption::parseOption(
437 const string& /*option*/, std::span<string>& /*cmdLine*/)
438{
439 auto& parser = OUTER(CommandLineParser, helpOption);
440 const auto& fullVersion = Version::full();
441 cout << fullVersion << '\n'
442 << string(fullVersion.size(), '=') << "\n"
443 "\n"
444 "usage: openmsx [arguments]\n"
445 " an argument is either an option or a filename\n"
446 "\n"
447 " this is the list of supported options:\n";
448
449 GroupedItems itemMap;
450 for (const auto& option : parser.options) {
451 const auto& helpText = option.option->optionHelp();
452 if (!helpText.empty()) {
453 itemMap[helpText].push_back(option.name);
454 }
455 }
456 printItemMap(itemMap);
457
458 cout << "\n"
459 " this is the list of supported file types:\n";
460
461 itemMap.clear();
462 for (const auto& [extension, fileType] : parser.fileTypes) {
463 itemMap[fileType->fileTypeHelp()].push_back(extension);
464 }
465 printItemMap(itemMap);
466
467 parser.parseStatus = CommandLineParser::EXIT;
468}
469
470string_view CommandLineParser::HelpOption::optionHelp() const
471{
472 return "Shows this text";
473}
474
475
476// class VersionOption
477
478void CommandLineParser::VersionOption::parseOption(
479 const string& /*option*/, std::span<string>& /*cmdLine*/)
480{
481 cout << Version::full() << "\n"
482 "flavour: " << BUILD_FLAVOUR << "\n"
483 "components: " << BUILD_COMPONENTS << '\n';
484 auto& parser = OUTER(CommandLineParser, versionOption);
485 parser.parseStatus = CommandLineParser::EXIT;
486}
487
488string_view CommandLineParser::VersionOption::optionHelp() const
489{
490 return "Prints openMSX version and exits";
491}
492
493
494// Machine option
495
496void CommandLineParser::MachineOption::parseOption(
497 const string& option, std::span<string>& cmdLine)
498{
499 auto& parser = OUTER(CommandLineParser, machineOption);
500 if (parser.haveConfig) {
501 throw FatalError("Only one machine option allowed");
502 }
503 try {
504 parser.reactor.switchMachine(getArgument(option, cmdLine));
505 } catch (MSXException& e) {
506 throw FatalError(std::move(e).getMessage());
507 }
508 parser.haveConfig = true;
509}
510
511string_view CommandLineParser::MachineOption::optionHelp() const
512{
513 return "Use machine specified in argument";
514}
515
516
517// class SettingOption
518
519void CommandLineParser::SettingOption::parseOption(
520 const string& option, std::span<string>& cmdLine)
521{
522 auto& parser = OUTER(CommandLineParser, settingOption);
523 if (parser.haveSettings) {
524 throw FatalError("Only one setting option allowed");
525 }
526 try {
527 auto& settingsConfig = parser.reactor.getGlobalCommandController().getSettingsConfig();
528 settingsConfig.loadSetting(
529 currentDirFileContext(), getArgument(option, cmdLine));
530 parser.haveSettings = true;
531 } catch (FileException& e) {
532 throw FatalError(std::move(e).getMessage());
533 } catch (ConfigException& e) {
534 throw FatalError(std::move(e).getMessage());
535 }
536}
537
538string_view CommandLineParser::SettingOption::optionHelp() const
539{
540 return "Load an alternative settings file";
541}
542
543
544// class TestConfigOption
545
546void CommandLineParser::TestConfigOption::parseOption(
547 const string& /*option*/, std::span<string>& /*cmdLine*/)
548{
549 auto& parser = OUTER(CommandLineParser, testConfigOption);
550 parser.parseStatus = CommandLineParser::TEST;
551}
552
553string_view CommandLineParser::TestConfigOption::optionHelp() const
554{
555 return "Test if the specified config works and exit";
556}
557
558// class BashOption
559
560void CommandLineParser::BashOption::parseOption(
561 const string& /*option*/, std::span<string>& cmdLine)
562{
563 auto& parser = OUTER(CommandLineParser, bashOption);
564 string_view last = cmdLine.empty() ? string_view{} : cmdLine.front();
565 cmdLine = cmdLine.subspan(0, 0); // eat all remaining parameters
566
567 if (last == "-machine") {
568 for (const auto& s : Reactor::getHwConfigs("machines")) {
569 cout << s << '\n';
570 }
571 } else if (last.starts_with("-ext")) {
572 for (const auto& s : Reactor::getHwConfigs("extensions")) {
573 cout << s << '\n';
574 }
575 } else if (last == "-romtype") {
576 for (const auto& s : RomInfo::getAllRomTypes()) {
577 cout << s << '\n';
578 }
579 } else {
580 for (const auto& option : parser.options) {
581 cout << option.name << '\n';
582 }
583 }
584 parser.parseStatus = CommandLineParser::EXIT;
585}
586
587string_view CommandLineParser::BashOption::optionHelp() const
588{
589 return {}; // don't include this option in --help
590}
591
592// class FileTypeCategoryInfoTopic
593
594CommandLineParser::FileTypeCategoryInfoTopic::FileTypeCategoryInfoTopic(
595 InfoCommand& openMSXInfoCommand, const CommandLineParser& parser_)
596 : InfoTopic(openMSXInfoCommand, "file_type_category")
597 , parser(parser_)
598{
599}
600
601void CommandLineParser::FileTypeCategoryInfoTopic::execute(
602 std::span<const TclObject> tokens, TclObject& result) const
603{
604 checkNumArgs(tokens, 3, "filename");
605 assert(tokens.size() == 3);
606
607 // get the category and add it to result
608 std::string_view fileName = tokens[2].getString();
609 if (const auto* handler = parser.getFileTypeHandlerForFileName(fileName)) {
610 result.addListElement(handler->fileTypeCategoryName());
611 } else {
612 result.addListElement("unknown");
613 }
614}
615
616string CommandLineParser::FileTypeCategoryInfoTopic::help(std::span<const TclObject> /*tokens*/) const
617{
618 return "Returns the file type category for the given file.";
619}
620
621} // namespace openmsx
void printInfo(std::string_view message)
Definition CliComm.cc:7
void printWarning(std::string_view message)
Definition CliComm.cc:12
CommandLineParser(Reactor &reactor)
void registerOption(const char *str, CLIOption &cliOption, ParsePhase phase=PHASE_LAST, unsigned length=2)
ParseStatus getParseStatus() const
void registerFileType(std::span< const std::string_view > extensions, CLIFileType &cliFileType)
void parse(std::span< char * > argv)
GlobalCommandController & getGlobalCommandController() const
Interpreter & getInterpreter() const
MSXMotherBoard * getMotherBoard() const
std::string_view getString() const
CliListener * addListener(std::unique_ptr< CliListener > listener)
void init(const char *programName) const
Contains the main loop of openMSX.
Definition Reactor.hh:75
MSXMotherBoard * getMotherBoard() const
Definition Reactor.cc:409
GlobalCommandController & getGlobalCommandController()
Definition Reactor.hh:91
InfoCommand & getOpenMSXInfoCommand()
Definition Reactor.cc:338
void switchMachine(const std::string &machine)
Definition Reactor.cc:454
CliComm & getCliComm()
Definition Reactor.cc:323
EnumSetting< int > & getMachineSetting()
Definition Reactor.hh:97
GlobalCliComm & getGlobalCliComm()
Definition Reactor.hh:90
Interpreter & getInterpreter()
Definition Reactor.cc:328
TclObject getDefaultValue() const final
Get the default value of this setting.
Definition Setting.hh:153
zstring_view getString() const
Definition TclObject.cc:141
static std::string full()
Definition Version.cc:8
#define COMPONENT_LASERDISC
Definition components.hh:8
std::pair< string_view, string_view > splitOnFirst(string_view str, string_view chars)
Definition StringOp.cc:95
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:11
const FileContext & systemFileContext()
hash_map< string_view, std::vector< string_view >, XXHasher > GroupedItems
const FileContext & currentDirFileContext()
const FileContext & userFileContext()
constexpr void sort(RandomAccessRange &&range)
Definition ranges.hh:51
constexpr auto transform(Range &&range, UnaryOp op)
Definition view.hh:520
constexpr auto drop(Range &&range, size_t n)
Definition view.hh:502
#define OUTER(type, member)
Definition outer.hh:42
auto * binary_find(ForwardRange &&range, const T &value, Compare comp={}, Proj proj={})
Definition ranges.hh:448
auto to_vector(Range &&range) -> std::vector< detail::ToVectorType< T, decltype(std::begin(range))> >
Definition stl.hh:275
auto spaces(size_t n)
Definition strCat.hh:801
std::string strCat()
Definition strCat.hh:703
void strAppend(std::string &result, Ts &&...ts)
Definition strCat.hh:752