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