diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b0803f38c..847d34437a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Main branch (git) +- Bug fix: Make sure command parser gives precedence to longer cmd-aliases. So + if sending `smile at` and the cmd `smile` has alias `smile at`, the match is + ordered so the result is never interpreted as `smile` with an argument `at`. - Bug fix: || (escaped color tags) were parsed too early in help entries, leading to colors when wanting a | separator - Bug fix: Make sure spawned objects get `typeclass_path` pointing to the true diff --git a/evennia/commands/command.py b/evennia/commands/command.py index 9dac840128..cca0e35754 100644 --- a/evennia/commands/command.py +++ b/evennia/commands/command.py @@ -11,7 +11,6 @@ import re from django.conf import settings from django.urls import reverse from django.utils.text import slugify - from evennia.locks.lockhandler import LockHandler from evennia.utils.ansi import ANSIString from evennia.utils.evtable import EvTable @@ -54,10 +53,14 @@ def _init_command(cls, **kwargs): cls.aliases = [] cls.aliases = list(set(alias for alias in cls.aliases if alias and alias != cls.key)) - # optimization - a set is much faster to match against than a list + # optimization - a set is much faster to match against than a list. This is useful + # for 'does any match' kind of queries cls._matchset = set([cls.key] + cls.aliases) - # optimization for looping over keys+aliases - cls._keyaliases = tuple(cls._matchset) + # optimization for looping over keys+aliases. We sort by longest entry first, since we + # want to be able to catch commands with spaces in the alias/key (so if you have key 'smile' + # and alias 'smile at', writing 'smile at' should not risk being interpreted as 'smile' + # with an argument 'at') + cls._keyaliases = tuple(sorted([cls.key] + cls.aliases, key=len, reverse=True)) # by default we don't save the command between runs if not hasattr(cls, "save_for_next"): @@ -303,10 +306,13 @@ class Command(metaclass=CommandMeta): matches.extend(x.lower() for x in self.aliases) self._matchset = set(matches) - # optimization for looping over keys+aliases - self._keyaliases = tuple(self._matchset) + # optimization for looping over keys+aliases. We sort by longest entry first, since we + # want to be able to catch commands with spaces in the alias/key (so if you have key 'smile' + # and alias 'smile at', writing 'smile at' should not risk being interpreted as 'smile' + # with an argument 'at') + self._keyaliases = tuple(sorted(matches, key=len, reverse=True)) - self._noprefix_aliases = {x.lstrip(CMD_IGNORE_PREFIXES): x for x in matches} + self._noprefix_aliases = {x.lstrip(CMD_IGNORE_PREFIXES): x for x in self._keyaliases} def set_key(self, new_key): """ diff --git a/evennia/commands/tests.py b/evennia/commands/tests.py index c2357c00c6..a70572e6fc 100644 --- a/evennia/commands/tests.py +++ b/evennia/commands/tests.py @@ -4,7 +4,6 @@ Unit testing for the Command system itself. """ from django.test import override_settings - from evennia.commands import cmdparser from evennia.commands.cmdset import CmdSet from evennia.commands.command import Command @@ -991,9 +990,8 @@ class TestOptionTransferReplace(TestCase): import sys -from twisted.trial.unittest import TestCase as TwistedTestCase - from evennia.commands import cmdhandler +from twisted.trial.unittest import TestCase as TwistedTestCase def _mockdelay(time, func, *args, **kwargs): @@ -1232,3 +1230,35 @@ class TestCmdSet(BaseEvenniaTest): result = test_cmd_set.get("another command") self.assertIsInstance(result, _CmdTest2) + + +class _CmdG(Command): + key = "smile" + aliases = ["smile at", "grin", "grin at"] + + +class _CmdSetG(CmdSet): + def at_cmdset_creation(self): + self.add(_CmdG()) + + +class TestIssue3090(BaseEvenniaTest): + """ + Command aliases should be prioritized longest-match to shortest-match. + https://github.com/evennia/evennia/issues/3090 + + """ + + def test_long_aliases(self): + + cmdset_g = _CmdSetG() + + # print(cmdset_g.commands[0]._keyaliases) + + result = cmdparser.cmdparser("smile at", cmdset_g, None)[0] + self.assertEqual(result[0], "smile at") + self.assertEqual(result[1], "") + self.assertEqual(result[2].__class__, _CmdG) + self.assertEqual(result[3], 8) + self.assertEqual(result[4], 1.0) + self.assertEqual(result[5], "smile at")