mirror of
https://github.com/wesnoth/wesnoth
synced 2025-04-20 19:40:13 +00:00
455 lines
13 KiB
C++
455 lines
13 KiB
C++
/*
|
||
Copyright (C) 2006 - 2025
|
||
by Joerg Hinrichs <joerg.hinrichs@alice-dsl.de>
|
||
Copyright (C) 2003 by David White <dave@whitevine.net>
|
||
Part of the Battle for Wesnoth Project https://www.wesnoth.org/
|
||
|
||
This program is free software; you can redistribute it and/or modify
|
||
it under the terms of the GNU General Public License as published by
|
||
the Free Software Foundation; either version 2 of the License, or
|
||
(at your option) any later version.
|
||
This program is distributed in the hope that it will be useful,
|
||
but WITHOUT ANY WARRANTY.
|
||
|
||
See the COPYING file for more details.
|
||
*/
|
||
|
||
#pragma once
|
||
#include "config.hpp"
|
||
#include "serialization/string_utils.hpp"
|
||
#include "formula/string_utils.hpp"
|
||
#include "gettext.hpp"
|
||
|
||
#include <boost/algorithm/string.hpp>
|
||
|
||
namespace events {
|
||
|
||
//simple command args parser, separated from command_handler for clarity.
|
||
//a word begins with a nonspace
|
||
//n-th arg is n-th word up to the next space
|
||
//n-th data is n-th word up to the end
|
||
//cmd is 0-th arg, begins at 0 always.
|
||
class cmd_arg_parser
|
||
{
|
||
public:
|
||
cmd_arg_parser() :
|
||
str_(""),
|
||
args(1, 0),
|
||
args_end(false)
|
||
{
|
||
}
|
||
|
||
explicit cmd_arg_parser(const std::string& str) :
|
||
str_(str),
|
||
args(1, 0),
|
||
args_end(false)
|
||
{
|
||
}
|
||
|
||
void parse(const std::string& str)
|
||
{
|
||
str_ = str;
|
||
args.clear();
|
||
args.push_back(0);
|
||
args_end = false;
|
||
}
|
||
|
||
const std::string& get_str() const
|
||
{
|
||
return str_;
|
||
}
|
||
std::string get_arg(unsigned n) const
|
||
{
|
||
advance_to_arg(n);
|
||
if (n < args.size()) {
|
||
return std::string(str_, args[n], str_.find(' ', args[n]) - args[n]);
|
||
}
|
||
else {
|
||
return "";
|
||
}
|
||
}
|
||
std::string get_data(unsigned n) const
|
||
{
|
||
advance_to_arg(n);
|
||
if (n < args.size()) {
|
||
std::string data(str_, args[n]);
|
||
boost::trim(data);
|
||
return data;
|
||
}
|
||
else {
|
||
return "";
|
||
}
|
||
}
|
||
std::string get_cmd() const
|
||
{
|
||
return get_arg(0);
|
||
}
|
||
private:
|
||
cmd_arg_parser& operator=(const cmd_arg_parser&);
|
||
cmd_arg_parser(const cmd_arg_parser&);
|
||
void advance_to_arg(unsigned n) const
|
||
{
|
||
while (n < args.size() && !args_end) {
|
||
std::size_t first_space = str_.find_first_of(' ', args.back());
|
||
std::size_t next_arg_begin = str_.find_first_not_of(' ', first_space);
|
||
if (next_arg_begin != std::string::npos) {
|
||
args.push_back(next_arg_begin);
|
||
}
|
||
else {
|
||
args_end = true;
|
||
}
|
||
}
|
||
}
|
||
std::string str_;
|
||
mutable std::vector<std::size_t> args;
|
||
mutable bool args_end;
|
||
};
|
||
|
||
|
||
//A helper class template with a slim public interface
|
||
//This represents a map of strings to void()-member-function-of-Worker-pointers
|
||
//with all the common functionality like general help, command help and aliases
|
||
//Usage (of a derived class): Derived(specific-arguments) d; d.dispatch(command);
|
||
//Derived classes should override virtual functions where noted.
|
||
//The template parameter currently must be the dervived class itself,
|
||
//i.e. class X : public map_command_handler<X>
|
||
//To add a new command in a derived class:
|
||
// * add a new private void function() to the derived class
|
||
// * add it to the function map in init_map there, setting flags like
|
||
// "D" for debug only (checking the flag is also done in the derived class)
|
||
// * remember to add some help and/or usage information in init_map()
|
||
template <class Worker>
|
||
class map_command_handler
|
||
{
|
||
public:
|
||
typedef void (Worker::*command_handler)();
|
||
struct command
|
||
{
|
||
command_handler handler;
|
||
std::string help; //long help text
|
||
std::string usage; //only args info
|
||
std::string flags; //< see implementation of get_command_flags_description() for meaning of flags
|
||
explicit command(command_handler h, const std::string& help = "",
|
||
const std::string& usage = "", const std::string& flags = "")
|
||
: handler(h), help(help), usage(usage), flags(flags)
|
||
{
|
||
}
|
||
bool has_flag(const char f) const
|
||
{
|
||
return flags.find(f) != flags.npos;
|
||
}
|
||
command& add_flag(const char f)
|
||
{
|
||
flags += f;
|
||
return *this;
|
||
}
|
||
};
|
||
typedef std::map<std::string, command> command_map;
|
||
typedef std::map<std::string, std::string> command_alias_map;
|
||
|
||
map_command_handler() : cap_("")
|
||
{
|
||
}
|
||
|
||
virtual ~map_command_handler() {}
|
||
|
||
bool empty() const
|
||
{
|
||
return command_map_.empty();
|
||
}
|
||
//actual work function
|
||
bool dispatch(std::string cmd)
|
||
{
|
||
if (empty()) {
|
||
init_map_default();
|
||
init_map();
|
||
}
|
||
|
||
// We recursively resolve alias (100 max to avoid infinite recursion)
|
||
for (int i = 0; i < 100; ++i) {
|
||
parse_cmd(cmd);
|
||
std::string actual_cmd = get_actual_cmd(get_cmd());
|
||
if (actual_cmd == get_cmd())
|
||
break;
|
||
std::string data = get_data(1);
|
||
// translate the command and add space + data if any
|
||
cmd = actual_cmd + (data.empty() ? "" : " ") + data;
|
||
}
|
||
|
||
if (get_cmd().empty()) {
|
||
return false;
|
||
}
|
||
|
||
if (const command* c = get_command(get_cmd())) {
|
||
if (is_enabled(*c)) {
|
||
(static_cast<Worker*>(this)->*(c->handler))();
|
||
return true;
|
||
}
|
||
else {
|
||
print(get_cmd(), _("This command is currently unavailable."));
|
||
return false;
|
||
}
|
||
}
|
||
|
||
utils::string_map symbols;
|
||
if(!cmd_flag_) {
|
||
symbols["help_command"] = cmd_prefix_ + "help";
|
||
symbols["command"] = cmd_prefix_ + get_cmd();
|
||
}
|
||
else {
|
||
symbols["help_command"] = "help";
|
||
symbols["command"] = get_cmd();
|
||
}
|
||
std::string string_user = get_cmd();
|
||
int distance = 0;
|
||
// Minimum length of the two compared strings.
|
||
int len_min = 0;
|
||
bool has_command_proposal = false;
|
||
// Compare the input with every command (excluding alias).
|
||
for(const auto& [key, index] : command_map_) {
|
||
// No need to test commands that are not enabled.
|
||
if(is_enabled(index)) {
|
||
distance = edit_distance_approx(string_user, key);
|
||
len_min = std::min(string_user.length(), key.length());
|
||
// Maximum of a third of the letters are wrong. The ratio
|
||
// between the edit distance and the minimum length, multiplied
|
||
// by a hundred gives us the percentage of errors.
|
||
if(distance * 100 / len_min < 34) {
|
||
symbols["command_proposal"] = key;
|
||
has_command_proposal = true;
|
||
// If a good enough candidate is found, exit the loop.
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
// If a proposal for a command is found, print it
|
||
if(has_command_proposal) {
|
||
print("help", VGETTEXT("Unknown command ‘$command’, did you mean ‘$command_proposal’? try $help_command "
|
||
"for a list of available commands.", symbols));
|
||
}
|
||
else {
|
||
print("help", VGETTEXT("Unknown command ‘$command’, try $help_command "
|
||
"for a list of available commands.", symbols));
|
||
}
|
||
return false;
|
||
}
|
||
|
||
std::vector<std::string> get_commands_list() const
|
||
{
|
||
std::vector<std::string> res;
|
||
for (typename command_map::value_type i : command_map_) {
|
||
res.push_back(i.first);
|
||
}
|
||
return res;
|
||
}
|
||
//command error reporting shorthands
|
||
void command_failed(const std::string& message, bool = false)
|
||
{
|
||
print(get_cmd(), _("Error:") + std::string(" ") + message);
|
||
}
|
||
protected:
|
||
void init_map_default()
|
||
{
|
||
register_command("help", &map_command_handler<Worker>::help,
|
||
_("Available commands list and command-specific help. "
|
||
"Use \"help all\" to include currently unavailable commands."),
|
||
// TRANSLATORS: These are the arguments accepted by the "help" command,
|
||
// which are either "all" or the name of another command.
|
||
// As with the command's name, "all" is hardcoded, and shouldn't change in the translation.
|
||
_("[all|<command>]\n“all” = overview of all commands, <command> = name of a specific command (provides more detail)"));
|
||
}
|
||
//derived classes initialize the map overriding this function
|
||
virtual void init_map() = 0;
|
||
//overridden in derived classes to actually print the messages somwehere
|
||
virtual void print(const std::string& title, const std::string& message) = 0;
|
||
//should be overridden in derived classes if the commands have flags
|
||
//this should return a string describing what all the flags mean
|
||
virtual std::string get_flags_description() const
|
||
{
|
||
return "";
|
||
}
|
||
//this should return a string describing the flags of the given command
|
||
virtual std::string get_command_flags_description(const command& /*c*/) const
|
||
{
|
||
return "";
|
||
}
|
||
//this should be overridden if e.g. flags are used to control command
|
||
//availability. Return false if the command should not be executed by dispatch()
|
||
virtual bool is_enabled(const command& /*c*/) const
|
||
{
|
||
return true;
|
||
}
|
||
virtual void parse_cmd(const std::string& cmd_string)
|
||
{
|
||
cap_.parse(cmd_string);
|
||
}
|
||
//safe n-th argunment getter
|
||
virtual std::string get_arg(unsigned argn) const
|
||
{
|
||
return cap_.get_arg(argn);
|
||
}
|
||
//"data" is n-th arg and everything after it
|
||
virtual std::string get_data(unsigned argn = 1) const
|
||
{
|
||
return cap_.get_data(argn);
|
||
}
|
||
virtual std::string get_cmd() const
|
||
{
|
||
return cap_.get_cmd();
|
||
}
|
||
void command_failed_need_arg(int argn)
|
||
{
|
||
utils::string_map symbols;
|
||
symbols["arg_id"] = std::to_string(argn);
|
||
command_failed(VGETTEXT("Missing argument $arg_id", symbols));
|
||
}
|
||
void print_usage()
|
||
{
|
||
help_command(get_cmd());
|
||
}
|
||
//take aliases into account
|
||
std::string get_actual_cmd(const std::string& cmd) const
|
||
{
|
||
command_alias_map::const_iterator i = command_alias_map_.find(cmd);
|
||
return i != command_alias_map_.end() ? i->second : cmd;
|
||
}
|
||
const command* get_command(const std::string& cmd) const
|
||
{
|
||
typename command_map::const_iterator i = command_map_.find(cmd);
|
||
return i != command_map_.end() ? &i->second : 0;
|
||
}
|
||
command* get_command(const std::string& cmd)
|
||
{
|
||
typename command_map::iterator i = command_map_.find(cmd);
|
||
return i != command_map_.end() ? &i->second : 0;
|
||
}
|
||
void help()
|
||
{
|
||
//print command-specific help if available, otherwise list commands
|
||
if (help_command(get_arg(1))) {
|
||
return;
|
||
}
|
||
std::stringstream ss;
|
||
bool show_unavail = show_unavailable_ || get_arg(1) == "all";
|
||
for (typename command_map::value_type i : command_map_) {
|
||
if (show_unavail || is_enabled(i.second)) {
|
||
ss << i.first;
|
||
//if (!i.second.usage.empty()) {
|
||
// ss << " " << i.second.usage;
|
||
//}
|
||
//uncomment the above to display usage information in command list
|
||
//which might clutter it somewhat
|
||
if (!i.second.flags.empty()) {
|
||
ss << " (" << i.second.flags << ") ";
|
||
}
|
||
ss << "; ";
|
||
}
|
||
}
|
||
utils::string_map symbols;
|
||
symbols["flags_description"] = get_flags_description();
|
||
symbols["list_of_commands"] = ss.str();
|
||
if(!cmd_flag_) {
|
||
symbols["help_command"] = cmd_prefix_ + "help";
|
||
}
|
||
else {
|
||
symbols["help_command"] = "help";
|
||
}
|
||
print(_("help"), VGETTEXT("Available commands $flags_description:\n$list_of_commands", symbols));
|
||
print(_("help"), VGETTEXT("Type $help_command <command> for more info.", symbols));
|
||
}
|
||
//returns true if the command exists.
|
||
bool help_command(const std::string& acmd)
|
||
{
|
||
std::string cmd = get_actual_cmd(acmd);
|
||
const command* c = get_command(cmd);
|
||
if (c) {
|
||
std::stringstream ss;
|
||
if(!cmd_flag_) {
|
||
ss << cmd_prefix_ << cmd;
|
||
}
|
||
else {
|
||
ss << cmd;
|
||
}
|
||
if (c->help.empty() && c->usage.empty()) {
|
||
ss << _(" No help available.");
|
||
}
|
||
else {
|
||
ss << " - " << c->help << "\n";
|
||
}
|
||
if (!c->usage.empty()) {
|
||
if(!cmd_flag_) {
|
||
ss << _("Usage:") << " " << cmd_prefix_ << cmd << " " << c->usage << "\n";
|
||
}
|
||
else {
|
||
ss << _("Usage:") << " " << cmd << " " << c->usage << "\n";
|
||
}
|
||
}
|
||
const auto flags_description = get_command_flags_description(*c);
|
||
if (!flags_description.empty()) {
|
||
// This shares the objectives dialog's translation of "Notes:"
|
||
ss << _("Notes:") << " " << get_command_flags_description(*c) << "\n";
|
||
}
|
||
const std::vector<std::string> l = get_aliases(cmd);
|
||
if (!l.empty()) {
|
||
// TRANSLATORS: alternative names for command-line commands, only shown if
|
||
// there is at least one of them.
|
||
ss << _n("command^Alias:", "Aliases:", l.size()) << " " << utils::join(l, " ") << "\n";
|
||
}
|
||
print(_("help"), ss.str());
|
||
}
|
||
return c != 0;
|
||
}
|
||
cmd_arg_parser cap_;
|
||
protected:
|
||
//this is display-only
|
||
static void set_cmd_prefix(const std::string& value)
|
||
{
|
||
cmd_prefix_ = value;
|
||
}
|
||
//sets the "cmd_flag_" flag as "true" when "cmd_prefix_" is in command
|
||
//line mode and to "false" when "cmd_prefix_" is in message line mode.
|
||
//The "help" message's symbols depend on the flag's value.
|
||
static void set_cmd_flag(bool value)
|
||
{
|
||
cmd_flag_ = value;
|
||
}
|
||
virtual void register_command(const std::string& cmd,
|
||
command_handler h, const std::string& help = "",
|
||
const std::string& usage = "", const std::string& flags = "")
|
||
{
|
||
command c = command(h, help, usage, flags);
|
||
std::pair<typename command_map::iterator, bool> r;
|
||
r = command_map_.insert(typename command_map::value_type(cmd, c));
|
||
if (!r.second) { //overwrite if exists
|
||
r.first->second = c;
|
||
}
|
||
}
|
||
|
||
virtual void register_alias(const std::string& to_cmd,
|
||
const std::string& cmd)
|
||
{
|
||
command_alias_map_[cmd] = to_cmd;
|
||
}
|
||
|
||
//get all aliases of a command.
|
||
static const std::vector<std::string> get_aliases(const std::string& cmd)
|
||
{
|
||
std::vector<std::string> aliases;
|
||
typedef command_alias_map::value_type p;
|
||
for (p i : command_alias_map_) {
|
||
if (i.second == cmd) {
|
||
aliases.push_back(i.first);
|
||
}
|
||
}
|
||
return aliases;
|
||
}
|
||
private:
|
||
static inline command_map command_map_ {};
|
||
static inline command_alias_map command_alias_map_ {};
|
||
static inline bool show_unavailable_ = false;
|
||
static inline std::string cmd_prefix_ {};
|
||
static inline bool cmd_flag_ = false;
|
||
};
|
||
|
||
}
|