The Battle for Wesnoth  1.19.10+dev
map_command_handler.hpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2006 - 2025
3  by Joerg Hinrichs <joerg.hinrichs@alice-dsl.de>
4  Copyright (C) 2003 by David White <dave@whitevine.net>
5  Part of the Battle for Wesnoth Project https://www.wesnoth.org/
6 
7  This program is free software; you can redistribute it and/or modify
8  it under the terms of the GNU General Public License as published by
9  the Free Software Foundation; either version 2 of the License, or
10  (at your option) any later version.
11  This program is distributed in the hope that it will be useful,
12  but WITHOUT ANY WARRANTY.
13 
14  See the COPYING file for more details.
15 */
16 
17 #pragma once
18 #include "config.hpp"
20 #include "formula/string_utils.hpp"
21 #include "gettext.hpp"
22 
23 #include <boost/algorithm/string.hpp>
24 
25 namespace events {
26 
27 //simple command args parser, separated from command_handler for clarity.
28 //a word begins with a nonspace
29 //n-th arg is n-th word up to the next space
30 //n-th data is n-th word up to the end
31 //cmd is 0-th arg, begins at 0 always.
33 {
34 public:
36  str_(""),
37  args(1, 0),
38  args_end(false)
39  {
40  }
41 
42  explicit cmd_arg_parser(const std::string& str) :
43  str_(str),
44  args(1, 0),
45  args_end(false)
46  {
47  }
48 
49  void parse(const std::string& str)
50  {
51  str_ = str;
52  args.clear();
53  args.push_back(0);
54  args_end = false;
55  }
56 
57  const std::string& get_str() const
58  {
59  return str_;
60  }
61  std::string get_arg(unsigned n) const
62  {
64  if (n < args.size()) {
65  return std::string(str_, args[n], str_.find(' ', args[n]) - args[n]);
66  }
67  else {
68  return "";
69  }
70  }
71  std::string get_data(unsigned n) const
72  {
74  if (n < args.size()) {
75  std::string data(str_, args[n]);
77  return data;
78  }
79  else {
80  return "";
81  }
82  }
83  std::string get_cmd() const
84  {
85  return get_arg(0);
86  }
87 private:
90  void advance_to_arg(unsigned n) const
91  {
92  while (n < args.size() && !args_end) {
93  std::size_t first_space = str_.find_first_of(' ', args.back());
94  std::size_t next_arg_begin = str_.find_first_not_of(' ', first_space);
95  if (next_arg_begin != std::string::npos) {
96  args.push_back(next_arg_begin);
97  }
98  else {
99  args_end = true;
100  }
101  }
102  }
103  std::string str_;
104  mutable std::vector<std::size_t> args;
105  mutable bool args_end;
106 };
107 
108 
109 //A helper class template with a slim public interface
110 //This represents a map of strings to void()-member-function-of-Worker-pointers
111 //with all the common functionality like general help, command help and aliases
112 //Usage (of a derived class): Derived(specific-arguments) d; d.dispatch(command);
113 //Derived classes should override virtual functions where noted.
114 //The template parameter currently must be the dervived class itself,
115 //i.e. class X : public map_command_handler<X>
116 //To add a new command in a derived class:
117 // * add a new private void function() to the derived class
118 // * add it to the function map in init_map there, setting flags like
119 // "D" for debug only (checking the flag is also done in the derived class)
120 // * remember to add some help and/or usage information in init_map()
121 template <class Worker>
123 {
124 public:
125  typedef void (Worker::*command_handler)();
126  struct command
127  {
129  std::string help; //long help text
130  std::string usage; //only args info
131  std::string flags; //< see implementation of get_command_flags_description() for meaning of flags
132  explicit command(command_handler h, const std::string& help = "",
133  const std::string& usage = "", const std::string& flags = "")
135  {
136  }
137  bool has_flag(const char f) const
138  {
139  return flags.find(f) != flags.npos;
140  }
141  command& add_flag(const char f)
142  {
143  flags += f;
144  return *this;
145  }
146  };
147  typedef std::map<std::string, command> command_map;
148  typedef std::map<std::string, std::string> command_alias_map;
149 
151  {
152  }
153 
154  virtual ~map_command_handler() {}
155 
156  bool empty() const
157  {
158  return command_map_.empty();
159  }
160  //actual work function
161  bool dispatch(std::string cmd)
162  {
163  if (empty()) {
165  init_map();
166  }
167 
168  // We recursively resolve alias (100 max to avoid infinite recursion)
169  for (int i = 0; i < 100; ++i) {
170  parse_cmd(cmd);
171  std::string actual_cmd = get_actual_cmd(get_cmd());
172  if (actual_cmd == get_cmd())
173  break;
174  std::string data = get_data(1);
175  // translate the command and add space + data if any
176  cmd = actual_cmd + (data.empty() ? "" : " ") + data;
177  }
178 
179  if (get_cmd().empty()) {
180  return false;
181  }
182 
183  if (const command* c = get_command(get_cmd())) {
184  if (is_enabled(*c)) {
185  (static_cast<Worker*>(this)->*(c->handler))();
186  return true;
187  }
188  else {
189  print(get_cmd(), _("This command is currently unavailable."));
190  return false;
191  }
192  }
193 
194  utils::string_map symbols;
195  if(!cmd_flag_) {
196  symbols["help_command"] = cmd_prefix_ + "help";
197  symbols["command"] = cmd_prefix_ + get_cmd();
198  }
199  else {
200  symbols["help_command"] = "help";
201  symbols["command"] = get_cmd();
202  }
203  std::string string_user = get_cmd();
204  int distance = 0;
205  // Minimum length of the two compared strings.
206  int len_min = 0;
207  bool has_command_proposal = false;
208  // Compare the input with every command (excluding alias).
209  for(const auto& [key, index] : command_map_) {
210  // No need to test commands that are not enabled.
211  if(is_enabled(index)) {
212  distance = edit_distance_approx(string_user, key);
213  len_min = std::min(string_user.length(), key.length());
214  // Maximum of a third of the letters are wrong. The ratio
215  // between the edit distance and the minimum length, multiplied
216  // by a hundred gives us the percentage of errors.
217  if(distance * 100 / len_min < 34) {
218  symbols["command_proposal"] = key;
219  has_command_proposal = true;
220  // If a good enough candidate is found, exit the loop.
221  break;
222  }
223  }
224  }
225  // If a proposal for a command is found, print it
226  if(has_command_proposal) {
227  print("help", VGETTEXT("Unknown command ‘$command’, did you mean ‘$command_proposal’? try $help_command "
228  "for a list of available commands.", symbols));
229  }
230  else {
231  print("help", VGETTEXT("Unknown command ‘$command’, try $help_command "
232  "for a list of available commands.", symbols));
233  }
234  return false;
235  }
236 
237  std::vector<std::string> get_commands_list() const
238  {
239  std::vector<std::string> res;
240  for (typename command_map::value_type i : command_map_) {
241  res.push_back(i.first);
242  }
243  return res;
244  }
245  //command error reporting shorthands
246  void command_failed(const std::string& message, bool = false)
247  {
248  print(get_cmd(), _("Error:") + std::string(" ") + message);
249  }
250 protected:
252  {
254  _("Available commands list and command-specific help. "
255  "Use \"help all\" to include currently unavailable commands."),
256  // TRANSLATORS: These are the arguments accepted by the "help" command,
257  // which are either "all" or the name of another command.
258  // As with the command's name, "all" is hardcoded, and shouldn't change in the translation.
259  _("[all|<command>]\n“all” = overview of all commands, <command> = name of a specific command (provides more detail)"));
260  }
261  //derived classes initialize the map overriding this function
262  virtual void init_map() = 0;
263  //overridden in derived classes to actually print the messages somwehere
264  virtual void print(const std::string& title, const std::string& message) = 0;
265  //should be overridden in derived classes if the commands have flags
266  //this should return a string describing what all the flags mean
267  virtual std::string get_flags_description() const
268  {
269  return "";
270  }
271  //this should return a string describing the flags of the given command
272  virtual std::string get_command_flags_description(const command& /*c*/) const
273  {
274  return "";
275  }
276  //this should be overridden if e.g. flags are used to control command
277  //availability. Return false if the command should not be executed by dispatch()
278  virtual bool is_enabled(const command& /*c*/) const
279  {
280  return true;
281  }
282  virtual void parse_cmd(const std::string& cmd_string)
283  {
284  cap_.parse(cmd_string);
285  }
286  //safe n-th argunment getter
287  virtual std::string get_arg(unsigned argn) const
288  {
289  return cap_.get_arg(argn);
290  }
291  //"data" is n-th arg and everything after it
292  virtual std::string get_data(unsigned argn = 1) const
293  {
294  return cap_.get_data(argn);
295  }
296  virtual std::string get_cmd() const
297  {
298  return cap_.get_cmd();
299  }
300  void command_failed_need_arg(int argn)
301  {
302  utils::string_map symbols;
303  symbols["arg_id"] = std::to_string(argn);
304  command_failed(VGETTEXT("Missing argument $arg_id", symbols));
305  }
306  void print_usage()
307  {
309  }
310  //take aliases into account
311  std::string get_actual_cmd(const std::string& cmd) const
312  {
313  command_alias_map::const_iterator i = command_alias_map_.find(cmd);
314  return i != command_alias_map_.end() ? i->second : cmd;
315  }
316  const command* get_command(const std::string& cmd) const
317  {
318  typename command_map::const_iterator i = command_map_.find(cmd);
319  return i != command_map_.end() ? &i->second : 0;
320  }
321  command* get_command(const std::string& cmd)
322  {
323  typename command_map::iterator i = command_map_.find(cmd);
324  return i != command_map_.end() ? &i->second : 0;
325  }
326  void help()
327  {
328  //print command-specific help if available, otherwise list commands
329  if (help_command(get_arg(1))) {
330  return;
331  }
332  std::stringstream ss;
333  bool show_unavail = show_unavailable_ || get_arg(1) == "all";
334  for (typename command_map::value_type i : command_map_) {
335  if (show_unavail || is_enabled(i.second)) {
336  ss << i.first;
337  //if (!i.second.usage.empty()) {
338  // ss << " " << i.second.usage;
339  //}
340  //uncomment the above to display usage information in command list
341  //which might clutter it somewhat
342  if (!i.second.flags.empty()) {
343  ss << " (" << i.second.flags << ") ";
344  }
345  ss << "; ";
346  }
347  }
348  utils::string_map symbols;
349  symbols["flags_description"] = get_flags_description();
350  symbols["list_of_commands"] = ss.str();
351  if(!cmd_flag_) {
352  symbols["help_command"] = cmd_prefix_ + "help";
353  }
354  else {
355  symbols["help_command"] = "help";
356  }
357  print(_("help"), VGETTEXT("Available commands $flags_description:\n$list_of_commands", symbols));
358  print(_("help"), VGETTEXT("Type $help_command <command> for more info.", symbols));
359  }
360  //returns true if the command exists.
361  bool help_command(const std::string& acmd)
362  {
363  std::string cmd = get_actual_cmd(acmd);
364  const command* c = get_command(cmd);
365  if (c) {
366  std::stringstream ss;
367  if(!cmd_flag_) {
368  ss << cmd_prefix_ << cmd;
369  }
370  else {
371  ss << cmd;
372  }
373  if (c->help.empty() && c->usage.empty()) {
374  ss << _(" No help available.");
375  }
376  else {
377  ss << " - " << c->help << "\n";
378  }
379  if (!c->usage.empty()) {
380  if(!cmd_flag_) {
381  ss << _("Usage:") << " " << cmd_prefix_ << cmd << " " << c->usage << "\n";
382  }
383  else {
384  ss << _("Usage:") << " " << cmd << " " << c->usage << "\n";
385  }
386  }
387  const auto flags_description = get_command_flags_description(*c);
388  if (!flags_description.empty()) {
389  // This shares the objectives dialog's translation of "Notes:"
390  ss << _("Notes:") << " " << get_command_flags_description(*c) << "\n";
391  }
392  const std::vector<std::string> l = get_aliases(cmd);
393  if (!l.empty()) {
394  // TRANSLATORS: alternative names for command-line commands, only shown if
395  // there is at least one of them.
396  ss << _n("command^Alias:", "Aliases:", l.size()) << " " << utils::join(l, " ") << "\n";
397  }
398  print(_("help"), ss.str());
399  }
400  return c != 0;
401  }
403 protected:
404  //this is display-only
405  static void set_cmd_prefix(const std::string& value)
406  {
407  cmd_prefix_ = value;
408  }
409  //sets the "cmd_flag_" flag as "true" when "cmd_prefix_" is in command
410  //line mode and to "false" when "cmd_prefix_" is in message line mode.
411  //The "help" message's symbols depend on the flag's value.
412  static void set_cmd_flag(bool value)
413  {
414  cmd_flag_ = value;
415  }
416  virtual void register_command(const std::string& cmd,
417  command_handler h, const std::string& help = "",
418  const std::string& usage = "", const std::string& flags = "")
419  {
420  command c = command(h, help, usage, flags);
421  std::pair<typename command_map::iterator, bool> r;
422  r = command_map_.insert(typename command_map::value_type(cmd, c));
423  if (!r.second) { //overwrite if exists
424  r.first->second = c;
425  }
426  }
427 
428  virtual void register_alias(const std::string& to_cmd,
429  const std::string& cmd)
430  {
431  command_alias_map_[cmd] = to_cmd;
432  }
433 
434  //get all aliases of a command.
435  static const std::vector<std::string> get_aliases(const std::string& cmd)
436  {
437  std::vector<std::string> aliases;
438  typedef command_alias_map::value_type p;
439  for (p i : command_alias_map_) {
440  if (i.second == cmd) {
441  aliases.push_back(i.first);
442  }
443  }
444  return aliases;
445  }
446 private:
447  static inline command_map command_map_ {};
449  static inline bool show_unavailable_ = false;
450  static inline std::string cmd_prefix_ {};
451  static inline bool cmd_flag_ = false;
452 };
453 
454 }
void parse(const std::string &str)
std::string get_arg(unsigned n) const
cmd_arg_parser(const cmd_arg_parser &)
void advance_to_arg(unsigned n) const
const std::string & get_str() const
cmd_arg_parser & operator=(const cmd_arg_parser &)
std::vector< std::size_t > args
std::string get_data(unsigned n) const
std::string get_cmd() const
cmd_arg_parser(const std::string &str)
std::map< std::string, std::string > command_alias_map
static const std::vector< std::string > get_aliases(const std::string &cmd)
const command * get_command(const std::string &cmd) const
virtual std::string get_flags_description() const
virtual void register_command(const std::string &cmd, command_handler h, const std::string &help="", const std::string &usage="", const std::string &flags="")
std::map< std::string, command > command_map
virtual std::string get_command_flags_description(const command &) const
virtual std::string get_arg(unsigned argn) const
command * get_command(const std::string &cmd)
virtual std::string get_cmd() const
std::vector< std::string > get_commands_list() const
static void set_cmd_prefix(const std::string &value)
virtual bool is_enabled(const command &) const
virtual std::string get_data(unsigned argn=1) const
static void set_cmd_flag(bool value)
virtual void init_map()=0
virtual void parse_cmd(const std::string &cmd_string)
void command_failed(const std::string &message, bool=false)
bool help_command(const std::string &acmd)
static command_alias_map command_alias_map_
std::string get_actual_cmd(const std::string &cmd) const
virtual void register_alias(const std::string &to_cmd, const std::string &cmd)
virtual void print(const std::string &title, const std::string &message)=0
Definitions for the interface to Wesnoth Markup Language (WML).
std::size_t edit_distance_approx(std::string_view str_1, std::string_view str_2) noexcept
Calculate the approximate edit distance of two strings.
#define VGETTEXT(msgid,...)
Handy wrappers around interpolate_variables_into_string and gettext.
std::size_t i
Definition: function.cpp:1030
static std::string _n(const char *str1, const char *str2, int n)
Definition: gettext.hpp:101
static std::string _(const char *str)
Definition: gettext.hpp:97
Handling of system events.
std::size_t index(std::string_view str, const std::size_t index)
Codepoint index corresponding to the nth character in a UTF-8 string.
Definition: unicode.cpp:70
void trim(std::string_view &s)
std::string join(const T &v, const std::string &s=",")
Generates a new string joining container items in a list.
std::map< std::string, t_string > string_map
std::string::const_iterator iterator
Definition: tokenizer.hpp:25
std::string_view data
Definition: picture.cpp:178
command(command_handler h, const std::string &help="", const std::string &usage="", const std::string &flags="")
mock_char c
mock_party p
static map_location::direction n
#define h
#define f