openMSX
GlobalCommandController.cc
Go to the documentation of this file.
2 #include "Reactor.hh"
3 #include "Setting.hh"
4 #include "ProxyCommand.hh"
5 #include "ProxySetting.hh"
6 #include "LocalFileReference.hh"
7 #include "GlobalCliComm.hh"
8 #include "CliConnection.hh"
9 #include "CommandException.hh"
10 #include "SettingsManager.hh"
11 #include "TclObject.hh"
12 #include "Version.hh"
13 #include "ScopedAssign.hh"
14 #include "checked_cast.hh"
15 #include "outer.hh"
16 #include "ranges.hh"
17 #include "stl.hh"
18 #include "view.hh"
19 #include "xrange.hh"
20 #include <cassert>
21 #include <memory>
22 
23 using std::string;
24 using std::vector;
25 
26 namespace openmsx {
27 
29  EventDistributor& eventDistributor,
30  GlobalCliComm& cliComm_, Reactor& reactor_)
31  : cliComm(cliComm_)
32  , connection(nullptr)
33  , reactor(reactor_)
34  , openMSXInfoCommand(*this, "openmsx_info")
35  , hotKey(reactor.getRTScheduler(), *this, eventDistributor)
36  , settingsConfig(*this, hotKey)
37  , helpCmd(*this)
38  , tabCompletionCmd(*this)
39  , updateCmd(*this)
40  , platformInfo(getOpenMSXInfoCommand())
41  , versionInfo (getOpenMSXInfoCommand())
42  , romInfoTopic(getOpenMSXInfoCommand())
43 {
44 }
45 
47 
49 {
50  // GlobalCommandController destructor must have run before
51  // we can check this.
52  assert(commands.empty());
53  assert(commandCompleters.empty());
54 }
55 
57 {
58  auto it = proxyCommandMap.find(name);
59  if (it == end(proxyCommandMap)) {
60  it = proxyCommandMap.emplace_noDuplicateCheck(
61  0, std::make_unique<ProxyCmd>(reactor, name));
62  }
63  ++it->first;
64 }
65 
67 {
68  auto it = proxyCommandMap.find(name);
69  assert(it != end(proxyCommandMap));
70  assert(it->first > 0);
71  --it->first;
72  if (it->first == 0) {
73  proxyCommandMap.erase(it);
74  }
75 }
76 
77 GlobalCommandController::ProxySettings::iterator
78 GlobalCommandController::findProxySetting(string_view name)
79 {
80  return ranges::find_if(proxySettings, [&](auto& v) {
81  return v.first->getFullName() == name;
82  });
83 }
84 
86 {
87  const auto& name = setting.getBaseNameObj();
88  auto it = findProxySetting(name.getString());
89  if (it == end(proxySettings)) {
90  // first occurrence
91  auto proxy = std::make_unique<ProxySetting>(reactor, name);
94  proxySettings.emplace_back(std::move(proxy), 1);
95  } else {
96  // was already registered
97  ++(it->second);
98  }
99 }
100 
102 {
103  auto it = findProxySetting(setting.getBaseName());
104  assert(it != end(proxySettings));
105  assert(it->second);
106  --(it->second);
107  if (it->second == 0) {
108  auto& proxy = *it->first;
111  move_pop_back(proxySettings, it);
112  }
113 }
114 
116 {
117  return cliComm;
118 }
119 
121 {
122  return interpreter;
123 }
124 
126  Command& command, const string& str)
127 {
128  assert(!commands.contains(str));
129  commands.emplace_noDuplicateCheck(str, &command);
130  interpreter.registerCommand(str, command);
131 }
132 
134  Command& command, string_view str)
135 {
136  assert(commands.contains(str));
137  assert(commands[str.str()] == &command);
138  commands.erase(str);
139  interpreter.unregisterCommand(command);
140 }
141 
143  CommandCompleter& completer, string_view str)
144 {
145  if (str.starts_with("::")) str.remove_prefix(2); // drop leading ::
146  assert(!commandCompleters.contains(str));
147  commandCompleters.emplace_noDuplicateCheck(str.str(), &completer);
148 }
149 
151  CommandCompleter& completer, string_view str)
152 {
153  if (str.starts_with("::")) str.remove_prefix(2); // drop leading ::
154  assert(commandCompleters.contains(str));
155  assert(commandCompleters[str.str()] == &completer); (void)completer;
156  commandCompleters.erase(str);
157 }
158 
160 {
162  interpreter.registerSetting(setting);
163 }
164 
166 {
167  interpreter.unregisterSetting(setting);
169 }
170 
172 {
173  return commands.contains(command);
174 }
175 
176 void GlobalCommandController::split(string_view str, vector<string>& tokens,
177  const char delimiter)
178 {
179  enum ParseState {Alpha, BackSlash, Quote};
180  ParseState state = Alpha;
181 
182  for (auto chr : str) {
183  switch (state) {
184  case Alpha:
185  if (tokens.empty()) {
186  tokens.emplace_back();
187  }
188  if (chr == delimiter) {
189  // token done, start new token
190  tokens.emplace_back();
191  } else {
192  tokens.back() += chr;
193  if (chr == '\\') {
194  state = BackSlash;
195  } else if (chr == '"') {
196  state = Quote;
197  }
198  }
199  break;
200  case Quote:
201  tokens.back() += chr;
202  if (chr == '"') {
203  state = Alpha;
204  }
205  break;
206  case BackSlash:
207  tokens.back() += chr;
208  state = Alpha;
209  break;
210  }
211  }
212 }
213 
214 string GlobalCommandController::removeEscaping(const string& str)
215 {
216  enum ParseState {Alpha, BackSlash, Quote};
217  ParseState state = Alpha;
218 
219  string result;
220  for (auto chr : str) {
221  switch (state) {
222  case Alpha:
223  if (chr == '\\') {
224  state = BackSlash;
225  } else if (chr == '"') {
226  state = Quote;
227  } else {
228  result += chr;
229  }
230  break;
231  case Quote:
232  if (chr == '"') {
233  state = Alpha;
234  } else {
235  result += chr;
236  }
237  break;
238  case BackSlash:
239  result += chr;
240  state = Alpha;
241  break;
242  }
243  }
244  return result;
245 }
246 
247 vector<string> GlobalCommandController::removeEscaping(
248  const vector<string>& input, bool keepLastIfEmpty)
249 {
250  vector<string> result;
251  for (auto& s : input) {
252  if (!s.empty()) {
253  result.push_back(removeEscaping(s));
254  }
255  }
256  if (keepLastIfEmpty && (input.empty() || input.back().empty())) {
257  result.emplace_back();
258  }
259  return result;
260 }
261 
262 static string escapeChars(const string& str, const string& chars)
263 {
264  string result;
265  for (auto chr : str) {
266  if (chars.find(chr) != string::npos) {
267  result += '\\';
268  }
269  result += chr;
270 
271  }
272  return result;
273 }
274 
275 string GlobalCommandController::addEscaping(const string& str, bool quote,
276  bool finished)
277 {
278  if (str.empty() && finished) {
279  quote = true;
280  }
281  string result = escapeChars(str, "$[]");
282  if (quote) {
283  result.insert(result.begin(), '"');
284  if (finished) {
285  result += '"';
286  }
287  } else {
288  result = escapeChars(result, " ");
289  }
290  return result;
291 }
292 
293 string GlobalCommandController::join(
294  const vector<string>& tokens, char delimiter)
295 {
296  string result;
297  bool first = true;
298  for (auto& t : tokens) {
299  if (!first) {
300  result += delimiter;
301  }
302  first = false;
303  result += t;
304  }
305  return result;
306 }
307 
308 bool GlobalCommandController::isComplete(const string& command)
309 {
310  return interpreter.isComplete(command);
311 }
312 
314  const string& command, CliConnection* connection_)
315 {
316  ScopedAssign<CliConnection*> sa(connection, connection_);
317  return interpreter.execute(command);
318 }
319 
320 void GlobalCommandController::source(const string& script)
321 {
322  try {
323  LocalFileReference file(script);
324  interpreter.executeFile(file.getFilename());
325  } catch (CommandException& e) {
327  "While executing ", script, ": ", e.getMessage());
328  }
329 }
330 
332 {
333  // split on 'active' command (the command that should actually be
334  // completed). Some examples:
335  // if {[debug rea<tab> <-- should complete the 'debug' command
336  // instead of the 'if' command
337  // bind F6 { cycl<tab> <-- should complete 'cycle' instead of 'bind'
338  TclParser parser = interpreter.parse(command);
339  int last = parser.getLast();
340  string_view pre = command.substr(0, last);
341  string_view post = command.substr(last);
342 
343  // split command string in tokens
344  vector<string> originalTokens;
345  split(post, originalTokens, ' ');
346  if (originalTokens.empty()) {
347  originalTokens.emplace_back();
348  }
349 
350  // complete last token
351  auto tokens = removeEscaping(originalTokens, true);
352  auto oldNum = tokens.size();
353  tabCompletion(tokens);
354  auto newNum = tokens.size();
355  bool tokenFinished = oldNum != newNum;
356 
357  // replace last token
358  string& original = originalTokens.back();
359  string& completed = tokens[oldNum - 1];
360  if (!completed.empty()) {
361  bool quote = !original.empty() && (original[0] == '"');
362  original = addEscaping(completed, quote, tokenFinished);
363  }
364  if (tokenFinished) {
365  assert(newNum == (oldNum + 1));
366  assert(tokens.back().empty());
367  originalTokens.emplace_back();
368  }
369 
370  // rebuild command string
371  return strCat(pre, join(originalTokens, ' '));
372 }
373 
374 void GlobalCommandController::tabCompletion(vector<string>& tokens)
375 {
376  if (tokens.empty()) {
377  // nothing typed yet
378  return;
379  }
380  if (tokens.size() == 1) {
381  string_view cmd = tokens[0];
382  string_view leadingNs;
383  // remove leading ::
384  if (cmd.starts_with("::")) {
385  cmd.remove_prefix(2);
386  leadingNs = "::";
387  }
388  // get current (typed) namespace
389  auto p1 = cmd.rfind("::");
390  string_view ns = (p1 == string_view::npos) ? cmd : cmd.substr(0, p1 + 2);
391 
392  // build a list of all command strings
393  TclObject names = interpreter.getCommandNames();
394  vector<string> names2;
395  names2.reserve(names.size());
396  for (string_view n1 : names) {
397  // remove leading ::
398  if (n1.starts_with("::")) n1.remove_prefix(2);
399  // initial namespace part must match
400  if (!n1.starts_with(ns)) continue;
401  // the part following the initial namespace
402  string_view n2 = n1.substr(ns.size());
403  // only keep upto the next namespace portion,
404  auto p2 = n2.find("::");
405  auto n3 = (p2 == string_view::npos) ? n1 : n1.substr(0, ns.size() + p2 + 2);
406  // don't care about adding the same string multiple times
407  names2.push_back(strCat(leadingNs, n3));
408  }
409  Completer::completeString(tokens, names2);
410  } else {
411  string_view cmd = tokens.front();
412  if (cmd.starts_with("::")) cmd.remove_prefix(2); // drop leading ::
413 
414  if (auto v = lookup(commandCompleters, cmd)) {
415  (*v)->tabCompletion(tokens);
416  } else {
417  TclObject command;
418  command.addListElement("openmsx::tabcompletion");
419  command.addListElements(tokens);
420  try {
421  TclObject list = command.executeCommand(interpreter);
422  bool sensitive = true;
423  auto begin = list.begin();
424  auto end = list.end();
425  if (begin != end) {
426  auto it2 = end; --it2;
427  auto back = *it2;
428  if (back == "false") {
429  end = it2;
430  sensitive = false;
431  } else if (back == "true") {
432  end = it2;
433  sensitive = true;
434  }
435  }
436  Completer::completeString(tokens, begin, end, sensitive);
437  } catch (CommandException& e) {
438  cliComm.printWarning(
439  "Error while executing tab-completion "
440  "proc: ", e.getMessage());
441  }
442  }
443  }
444 }
445 
446 
447 // Help Command
448 
449 GlobalCommandController::HelpCmd::HelpCmd(GlobalCommandController& controller_)
450  : Command(controller_, "help")
451 {
452 }
453 
454 void GlobalCommandController::HelpCmd::execute(
455  span<const TclObject> tokens, TclObject& result)
456 {
457  auto& controller = OUTER(GlobalCommandController, helpCmd);
458  switch (tokens.size()) {
459  case 1: {
460  string text =
461  "Use 'help [command]' to get help for a specific command\n"
462  "The following commands exist:\n";
463  auto cmds = to_vector<string_view>(
464  view::keys(controller.commandCompleters));
465  ranges::sort(cmds);
466  for (auto& line : formatListInColumns(cmds)) {
467  strAppend(text, line, '\n');
468  }
469  result = text;
470  break;
471  }
472  default: {
473  if (auto v = lookup(controller.commandCompleters, tokens[1].getString())) {
474  auto tokens2 = to_vector(view::transform(
475  view::drop(tokens, 1),
476  [](auto& t) { return t.getString().str(); }));
477  result = (*v)->help(tokens2);
478  } else {
479  TclObject command;
480  command.addListElement("openmsx::help");
481  command.addListElements(view::drop(tokens, 1));
482  result = command.executeCommand(getInterpreter());
483  }
484  break;
485  }
486  }
487 }
488 
489 string GlobalCommandController::HelpCmd::help(const vector<string>& /*tokens*/) const
490 {
491  return "prints help information for commands\n";
492 }
493 
494 void GlobalCommandController::HelpCmd::tabCompletion(vector<string>& tokens) const
495 {
496  string front = std::move(tokens.front());
497  tokens.erase(begin(tokens));
498  auto& controller = OUTER(GlobalCommandController, helpCmd);
499  controller.tabCompletion(tokens);
500  tokens.insert(begin(tokens), std::move(front));
501 }
502 
503 
504 // TabCompletionCmd Command
505 
506 GlobalCommandController::TabCompletionCmd::TabCompletionCmd(
507  GlobalCommandController& controller_)
508  : Command(controller_, "tabcompletion")
509 {
510 }
511 
512 void GlobalCommandController::TabCompletionCmd::execute(
513  span<const TclObject> tokens, TclObject& result)
514 {
515  switch (tokens.size()) {
516  case 2: {
517  // TODO this prints list of possible completions in the console
518  auto& controller = OUTER(GlobalCommandController, tabCompletionCmd);
519  result = controller.tabCompletion(tokens[1].getString());
520  break;
521  }
522  default:
523  throw SyntaxError();
524  }
525 }
526 
527 string GlobalCommandController::TabCompletionCmd::help(const vector<string>& /*tokens*/) const
528 {
529  return "!!! This command will change in the future !!!\n"
530  "Tries to completes the given argument as if it were typed in "
531  "the console. This command is only useful to provide "
532  "tabcompletion to external console interfaces.";
533 }
534 
535 
536 // class UpdateCmd
537 
538 GlobalCommandController::UpdateCmd::UpdateCmd(CommandController& commandController_)
539  : Command(commandController_, "openmsx_update")
540 {
541 }
542 
543 static GlobalCliComm::UpdateType getType(const TclObject& name)
544 {
545  auto updateStr = CliComm::getUpdateStrings();
546  for (auto i : xrange(updateStr.size())) {
547  if (updateStr[i] == name) {
548  return static_cast<CliComm::UpdateType>(i);
549  }
550  }
551  throw CommandException("No such update type: ", name.getString());
552 }
553 
554 CliConnection& GlobalCommandController::UpdateCmd::getConnection()
555 {
556  auto& controller = OUTER(GlobalCommandController, updateCmd);
557  if (auto* c = controller.getConnection()) {
558  return *c;
559  }
560  throw CommandException("This command only makes sense when "
561  "it's used from an external application.");
562 }
563 
564 void GlobalCommandController::UpdateCmd::execute(
565  span<const TclObject> tokens, TclObject& /*result*/)
566 {
567  if (tokens.size() != 3) {
568  throw SyntaxError();
569  }
570  if (tokens[1] == "enable") {
571  getConnection().setUpdateEnable(getType(tokens[2]), true);
572  } else if (tokens[1] == "disable") {
573  getConnection().setUpdateEnable(getType(tokens[2]), false);
574  } else {
575  throw SyntaxError();
576  }
577 }
578 
579 string GlobalCommandController::UpdateCmd::help(const vector<string>& /*tokens*/) const
580 {
581  static const string helpText = "Enable or disable update events for external applications. See doc/openmsx-control-xml.txt.";
582  return helpText;
583 }
584 
585 void GlobalCommandController::UpdateCmd::tabCompletion(vector<string>& tokens) const
586 {
587  switch (tokens.size()) {
588  case 2: {
589  static const char* const ops[] = { "enable", "disable" };
590  completeString(tokens, ops);
591  break;
592  }
593  case 3:
594  completeString(tokens, CliComm::getUpdateStrings());
595  break;
596  }
597 }
598 
599 
600 // Platform info
601 
602 GlobalCommandController::PlatformInfo::PlatformInfo(InfoCommand& openMSXInfoCommand_)
603  : InfoTopic(openMSXInfoCommand_, "platform")
604 {
605 }
606 
607 void GlobalCommandController::PlatformInfo::execute(
608  span<const TclObject> /*tokens*/, TclObject& result) const
609 {
610  result = TARGET_PLATFORM;
611 }
612 
613 string GlobalCommandController::PlatformInfo::help(const vector<string>& /*tokens*/) const
614 {
615  return "Prints openMSX platform.";
616 }
617 
618 // Version info
619 
620 GlobalCommandController::VersionInfo::VersionInfo(InfoCommand& openMSXInfoCommand_)
621  : InfoTopic(openMSXInfoCommand_, "version")
622 {
623 }
624 
625 void GlobalCommandController::VersionInfo::execute(
626  span<const TclObject> /*tokens*/, TclObject& result) const
627 {
628  result = Version::full();
629 }
630 
631 string GlobalCommandController::VersionInfo::help(const vector<string>& /*tokens*/) const
632 {
633  return "Prints openMSX version.";
634 }
635 
636 } // namespace openmsx
auto transform(Range &&range, UnaryOp op)
Definition: view.hh:312
const std::string getFilename() const
Returns path to a local uncompressed version of this file.
Contains the main loop of openMSX.
Definition: Reactor.hh:66
hash_map< std::string, Command *, XXHasher > commands
bool starts_with(string_view x) const
Definition: string_view.cc:116
const std::string & getMessage() const &
Definition: MSXException.hh:23
void registerCommand(const std::string &name, Command &command)
Definition: Interpreter.cc:136
auto xrange(T e)
Definition: xrange.hh:170
GlobalCommandController(const GlobalCommandController &)=delete
TclObject executeFile(const std::string &filename)
Definition: Interpreter.cc:206
void registerProxyCommand(const std::string &name)
auto begin() const
Definition: TclObject.hh:130
void unregisterSetting(BaseSetting &setting)
Definition: span.hh:34
const Value * lookup(const hash_map< Key, Value, Hasher, Equal > &map, const Key2 &key)
Definition: hash_map.hh:91
mat3 n3(vec3(1, 0, 3), vec3(4, 5, 6), vec3(7, 8, 9))
string_view getString() const
Definition: TclObject.cc:77
void unregisterCommand(Command &command, string_view str) override
void registerSetting(Setting &setting) override
TODO.
void unregisterCompleter(CommandCompleter &completer, string_view str) override
const TclObject & getBaseNameObj() const
Definition: Setting.hh:33
void move_pop_back(VECTOR &v, typename VECTOR::iterator it)
Erase the pointed to element from the given vector.
Definition: stl.hh:191
int getLast() const
Get Start of the last subcommand.
Definition: TclParser.hh:31
void strAppend(std::string &result, Ts &&...ts)
Definition: strCat.hh:648
TclParser parse(string_view command)
Definition: Interpreter.cc:438
auto find_if(InputRange &&range, UnaryPredicate pred)
Definition: ranges.hh:113
static const size_type npos
Definition: string_view.hh:23
auto begin(const string_view &x)
Definition: string_view.hh:152
bool hasCommand(string_view command) const override
Does a command with this name already exist?
void unregisterProxyCommand(string_view name)
unsigned size() const
Definition: TclObject.hh:128
void registerSetting(BaseSetting &setting)
void remove_prefix(size_type n)
Definition: string_view.hh:73
bool isComplete(const std::string &command)
Returns true iff the command is complete (all braces, quotes etc.
auto keys(Map &&map)
Definition: view.hh:317
size_type find(string_view s) const
Definition: string_view.cc:38
void registerCompleter(CommandCompleter &completer, string_view str) override
(Un)register a command completer, used to complete build-in Tcl cmds
void sort(RandomAccessRange &&range)
Definition: ranges.hh:35
static span< const char *const > getUpdateStrings()
Definition: CliComm.hh:81
Thanks to enen for testing this on a real cartridge:
Definition: Autofire.cc:5
void unregisterSetting(Setting &setting) override
size_type rfind(string_view s) const
Definition: string_view.cc:65
static void completeString(std::vector< std::string > &tokens, ITER begin, ITER end, bool caseSensitive=true)
Definition: Completer.hh:108
This class implements a (close approximation) of the std::string_view class.
Definition: string_view.hh:15
std::string tabCompletion(string_view command)
Complete the given command.
void addListElement(T t)
Definition: TclObject.hh:107
std::string str() const
Definition: string_view.cc:12
void unregisterCommand(Command &command)
Definition: Interpreter.cc:144
void registerCommand(Command &command, const std::string &str) override
(Un)register a command
TclObject getCommandNames()
Definition: Interpreter.cc:187
string_view substr(size_type pos, size_type n=npos) const
Definition: string_view.cc:32
TclObject execute(const std::string &command)
Definition: Interpreter.cc:197
hash_map< std::string, CommandCompleter *, XXHasher > commandCompleters
static std::string full()
Definition: Version.cc:8
const string_view getBaseName() const
Definition: Setting.hh:35
auto drop(Range &&range, size_t n)
Definition: view.hh:294
std::string strCat(Ts &&...ts)
Definition: strCat.hh:577
size_type size() const
Definition: string_view.hh:52
auto end() const
Definition: TclObject.hh:131
#define OUTER(type, member)
Definition: outer.hh:38
void source(const std::string &script)
Executes all defined auto commands.
void registerSetting(BaseSetting &variable)
Definition: Interpreter.cc:252
Helper class to use files in APIs other than openmsx::File.
void addListElements(ITER first, ITER last)
Definition: TclObject.hh:108
TclObject executeCommand(Interpreter &interp, bool compile=false)
Interpret this TclObject as a command and execute it.
Definition: TclObject.cc:147
void printWarning(string_view message)
Definition: CliComm.cc:20
auto to_vector(Range &&range) -> std::vector< detail::ToVectorType< T, decltype(std::begin(range))>>
Definition: stl.hh:325
TclObject executeCommand(const std::string &command, CliConnection *connection=nullptr) override
Execute the given command.
void unregisterSetting(BaseSetting &variable)
Definition: Interpreter.cc:304
auto end(const string_view &x)
Definition: string_view.hh:153
void setUpdateEnable(CliComm::UpdateType type, bool value)
Assign new value to some variable and restore the original value when this object goes out of scope...
Definition: ScopedAssign.hh:7
bool isComplete(const std::string &command) const
Definition: Interpreter.cc:192