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