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