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