The Battle for Wesnoth  1.19.0-dev
map_command_handler.hpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2006 - 2024
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  void 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;
181  }
182 
183  if (const command* c = get_command(get_cmd())) {
184  if (is_enabled(*c)) {
185  (static_cast<Worker*>(this)->*(c->handler))();
186  }
187  else {
188  print(get_cmd(), _("This command is currently unavailable."));
189  }
190  }
191  else if (help_on_unknown_) {
192  utils::string_map symbols;
193  if(!cmd_flag_) {
194  symbols["help_command"] = cmd_prefix_ + "help";
195  symbols["command"] = cmd_prefix_ + get_cmd();
196  }
197  else {
198  symbols["help_command"] = "help";
199  symbols["command"] = get_cmd();
200  }
201  std::string string_user = get_cmd();
202  int distance = 0;
203  // Minimum length of the two compared strings.
204  int len_min = 0;
205  bool has_command_proposal = false;
206  // Compare the input with every command (excluding alias).
207  for(const auto& [key, index] : command_map_) {
208  // No need to test commands that are not enabled.
209  if(is_enabled(index)) {
210  distance = edit_distance_approx(string_user, key);
211  len_min = std::min(string_user.length(), key.length());
212  // Maximum of a third of the letters are wrong. The ratio
213  // between the edit distance and the minimum length, multiplied
214  // by a hundred gives us the percentage of errors.
215  if(distance * 100 / len_min < 34) {
216  symbols["command_proposal"] = key;
217  has_command_proposal = true;
218  // If a good enough candidate is found, exit the loop.
219  break;
220  }
221  }
222  }
223  // If a proposal for a command is found, print it
224  if(has_command_proposal) {
225  print("help", VGETTEXT("Unknown command '$command', did you mean '$command_proposal'? try $help_command "
226  "for a list of available commands.", symbols));
227  }
228  else {
229  print("help", VGETTEXT("Unknown command '$command', try $help_command "
230  "for a list of available commands.", symbols));
231  }
232  }
233  }
234 
235  std::vector<std::string> get_commands_list() const
236  {
237  std::vector<std::string> res;
238  for (typename command_map::value_type i : command_map_) {
239  res.push_back(i.first);
240  }
241  return res;
242  }
243  //command error reporting shorthands
244  void command_failed(const std::string& message, bool = false)
245  {
246  print(get_cmd(), _("Error:") + std::string(" ") + message);
247  }
248 protected:
250  {
252  _("Available commands list and command-specific help. "
253  "Use \"help all\" to include currently unavailable commands."),
254  // TRANSLATORS: These are the arguments accepted by the "help" command,
255  // which are either "all" or the name of another command.
256  // As with the command's name, "all" is hardcoded, and shouldn't change in the translation.
257  _("[all|<command>]\n“all” = overview of all commands, <command> = name of a specific command (provides more detail)"));
258  }
259  //derived classes initialize the map overriding this function
260  virtual void init_map() = 0;
261  //overridden in derived classes to actually print the messages somwehere
262  virtual void print(const std::string& title, const std::string& message) = 0;
263  //should be overridden in derived classes if the commands have flags
264  //this should return a string describing what all the flags mean
265  virtual std::string get_flags_description() const
266  {
267  return "";
268  }
269  //this should return a string describing the flags of the given command
270  virtual std::string get_command_flags_description(const command& /*c*/) const
271  {
272  return "";
273  }
274  //this should be overridden if e.g. flags are used to control command
275  //availability. Return false if the command should not be executed by dispatch()
276  virtual bool is_enabled(const command& /*c*/) const
277  {
278  return true;
279  }
280  virtual void parse_cmd(const std::string& cmd_string)
281  {
282  cap_.parse(cmd_string);
283  }
284  //safe n-th argunment getter
285  virtual std::string get_arg(unsigned argn) const
286  {
287  return cap_.get_arg(argn);
288  }
289  //"data" is n-th arg and everything after it
290  virtual std::string get_data(unsigned argn = 1) const
291  {
292  return cap_.get_data(argn);
293  }
294  virtual std::string get_cmd() const
295  {
296  return cap_.get_cmd();
297  }
298  void command_failed_need_arg(int argn)
299  {
300  utils::string_map symbols;
301  symbols["arg_id"] = std::to_string(argn);
302  command_failed(VGETTEXT("Missing argument $arg_id", symbols));
303  }
304  void print_usage()
305  {
307  }
308  //take aliases into account
309  std::string get_actual_cmd(const std::string& cmd) const
310  {
311  command_alias_map::const_iterator i = command_alias_map_.find(cmd);
312  return i != command_alias_map_.end() ? i->second : cmd;
313  }
314  const command* get_command(const std::string& cmd) const
315  {
316  typename command_map::const_iterator i = command_map_.find(cmd);
317  return i != command_map_.end() ? &i->second : 0;
318  }
319  command* get_command(const std::string& cmd)
320  {
321  typename command_map::iterator i = command_map_.find(cmd);
322  return i != command_map_.end() ? &i->second : 0;
323  }
324  void help()
325  {
326  //print command-specific help if available, otherwise list commands
327  if (help_command(get_arg(1))) {
328  return;
329  }
330  std::stringstream ss;
331  bool show_unavail = show_unavailable_ || get_arg(1) == "all";
332  for (typename command_map::value_type i : command_map_) {
333  if (show_unavail || is_enabled(i.second)) {
334  ss << i.first;
335  //if (!i.second.usage.empty()) {
336  // ss << " " << i.second.usage;
337  //}
338  //uncomment the above to display usage information in command list
339  //which might clutter it somewhat
340  if (!i.second.flags.empty()) {
341  ss << " (" << i.second.flags << ") ";
342  }
343  ss << "; ";
344  }
345  }
346  utils::string_map symbols;
347  symbols["flags_description"] = get_flags_description();
348  symbols["list_of_commands"] = ss.str();
349  if(!cmd_flag_) {
350  symbols["help_command"] = cmd_prefix_ + "help";
351  }
352  else {
353  symbols["help_command"] = "help";
354  }
355  print(_("help"), VGETTEXT("Available commands $flags_description:\n$list_of_commands", symbols));
356  print(_("help"), VGETTEXT("Type $help_command <command> for more info.", symbols));
357  }
358  //returns true if the command exists.
359  bool help_command(const std::string& acmd)
360  {
361  std::string cmd = get_actual_cmd(acmd);
362  const command* c = get_command(cmd);
363  if (c) {
364  std::stringstream ss;
365  if(!cmd_flag_) {
366  ss << cmd_prefix_ << cmd;
367  }
368  else {
369  ss << cmd;
370  }
371  if (c->help.empty() && c->usage.empty()) {
372  ss << _(" No help available.");
373  }
374  else {
375  ss << " - " << c->help << "\n";
376  }
377  if (!c->usage.empty()) {
378  if(!cmd_flag_) {
379  ss << _("Usage:") << " " << cmd_prefix_ << cmd << " " << c->usage << "\n";
380  }
381  else {
382  ss << _("Usage:") << " " << cmd << " " << c->usage << "\n";
383  }
384  }
385  const auto flags_description = get_command_flags_description(*c);
386  if (!flags_description.empty()) {
387  // This shares the objectives dialog's translation of "Notes:"
388  ss << _("Notes:") << " " << get_command_flags_description(*c) << "\n";
389  }
390  const std::vector<std::string> l = get_aliases(cmd);
391  if (!l.empty()) {
392  // TRANSLATORS: alternative names for command-line commands, only shown if
393  // there is at least one of them.
394  ss << _n("command^Alias:", "Aliases:", l.size()) << " " << utils::join(l, " ") << "\n";
395  }
396  print(_("help"), ss.str());
397  }
398  return c != 0;
399  }
401 protected:
402  //show a "try help" message on unknown command?
403  static void set_help_on_unknown(bool value)
404  {
405  help_on_unknown_ = value;
406  }
407  //this is display-only
408  static void set_cmd_prefix(const std::string& value)
409  {
410  cmd_prefix_ = value;
411  }
412  //sets the "cmd_flag_" flag as "true" when "cmd_prefix_" is in command
413  //line mode and to "false" when "cmd_prefix_" is in message line mode.
414  //The "help" message's symbols depend on the flag's value.
415  static void set_cmd_flag(bool value)
416  {
417  cmd_flag_ = value;
418  }
419  virtual void register_command(const std::string& cmd,
420  command_handler h, const std::string& help = "",
421  const std::string& usage = "", const std::string& flags = "")
422  {
423  command c = command(h, help, usage, flags);
424  std::pair<typename command_map::iterator, bool> r;
425  r = command_map_.insert(typename command_map::value_type(cmd, c));
426  if (!r.second) { //overwrite if exists
427  r.first->second = c;
428  }
429  }
430 
431  virtual void register_alias(const std::string& to_cmd,
432  const std::string& cmd)
433  {
434  command_alias_map_[cmd] = to_cmd;
435  }
436 
437  //get all aliases of a command.
438  static const std::vector<std::string> get_aliases(const std::string& cmd)
439  {
440  std::vector<std::string> aliases;
441  typedef command_alias_map::value_type p;
442  for (p i : command_alias_map_) {
443  if (i.second == cmd) {
444  aliases.push_back(i.first);
445  }
446  }
447  return aliases;
448  }
449 private:
450  static inline command_map command_map_ {};
452  static inline bool help_on_unknown_ = true;
453  static inline bool show_unavailable_ = false;
454  static inline std::string cmd_prefix_ {};
455  static inline bool cmd_flag_ = false;
456 };
457 
458 }
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)
static void set_help_on_unknown(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
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:968
static std::string _n(const char *str1, const char *str2, int n)
Definition: gettext.hpp:97
static std::string _(const char *str)
Definition: gettext.hpp:93
Handling of system events.
Definition: help.cpp:53
std::size_t index(const std::string &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:194
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