diff --git a/evennia/utils/funcparser.py b/evennia/utils/funcparser.py index c3455f6774..54fc64b0c8 100644 --- a/evennia/utils/funcparser.py +++ b/evennia/utils/funcparser.py @@ -84,8 +84,8 @@ class _ParsedFunc: # state storage fullstr: str = "" infuncstr: str = "" - single_quoted: bool = False - double_quoted: bool = False + single_quoted: int = -1 + double_quoted: int = -1 current_kwarg: str = "" open_lparens: int = 0 open_lsquate: int = 0 @@ -318,8 +318,8 @@ class FuncParser: # parsing state callstack = [] - single_quoted = False - double_quoted = False + single_quoted = -1 + double_quoted = -1 open_lparens = 0 # open ( open_lsquare = 0 # open [ open_lcurly = 0 # open { @@ -330,6 +330,7 @@ class FuncParser: curr_func = None fullstr = "" # final string infuncstr = "" # string parts inside the current level of $funcdef (including $) + literal_infuncstr = False for char in string: @@ -373,12 +374,13 @@ class FuncParser: curr_func.open_lcurly = open_lcurly current_kwarg = "" infuncstr = "" - single_quoted = False - double_quoted = False + single_quoted = -1 + double_quoted = -1 open_lparens = 0 open_lsquare = 0 open_lcurly = 0 exec_return = "" + literal_infuncstr = False callstack.append(curr_func) # start a new func @@ -401,19 +403,41 @@ class FuncParser: infuncstr += str(exec_return) exec_return = "" - if char == "'": # note that this is the same as "\'" + if char == "'" and double_quoted < 0: # note that this is the same as "\'" # a single quote - flip status - single_quoted = not single_quoted - infuncstr += char + if single_quoted == 0: + infuncstr = infuncstr[1:] + single_quoted = -1 + elif single_quoted > 0: + prefix = infuncstr[0:single_quoted] + infuncstr = prefix + infuncstr[single_quoted+1:] + single_quoted = -1 + else: + infuncstr += char + infuncstr = infuncstr.strip() + single_quoted = len(infuncstr) - 1 + literal_infuncstr = True + continue - if char == '"': # note that this is the same as '\"' + if char == '"' and single_quoted < 0: # note that this is the same as '\"' # a double quote = flip status - double_quoted = not double_quoted - infuncstr += char + if double_quoted == 0: + infuncstr = infuncstr[1:] + double_quoted = -1 + elif double_quoted > 0: + prefix = infuncstr[0:double_quoted] + infuncstr = prefix + infuncstr[double_quoted + 1:] + double_quoted = -1 + else: + infuncstr += char + infuncstr = infuncstr.strip() + double_quoted = len(infuncstr) - 1 + literal_infuncstr = True + continue - if double_quoted or single_quoted: + if double_quoted >= 0 or single_quoted >= 0: # inside a string definition - this escapes everything else infuncstr += char continue @@ -477,12 +501,15 @@ class FuncParser: else: curr_func.args.append(exec_return) else: + if not literal_infuncstr: + infuncstr = infuncstr.strip() + # store a string instead if current_kwarg: - curr_func.kwargs[current_kwarg] = infuncstr.strip() - elif infuncstr.strip(): + curr_func.kwargs[current_kwarg] = infuncstr + elif literal_infuncstr or infuncstr.strip(): # don't store the empty string - curr_func.args.append(infuncstr.strip()) + curr_func.args.append(infuncstr) # note that at this point either exec_return or infuncstr will # be empty. We need to store the full string so we can print @@ -493,6 +520,7 @@ class FuncParser: current_kwarg = "" exec_return = "" infuncstr = "" + literal_infuncstr = False if char == ")": # closing the function list - this means we have a @@ -536,6 +564,7 @@ class FuncParser: if return_str: exec_return = "" infuncstr = "" + literal_infuncstr = False continue infuncstr += char diff --git a/evennia/utils/tests/test_funcparser.py b/evennia/utils/tests/test_funcparser.py index c62928ff53..3f6e6eb2f9 100644 --- a/evennia/utils/tests/test_funcparser.py +++ b/evennia/utils/tests/test_funcparser.py @@ -44,6 +44,7 @@ def _double_callable(*args, **kwargs): def _eval_callable(*args, **kwargs): if args: return simple_eval(args[0]) + return "" @@ -113,25 +114,25 @@ class TestFuncParser(TestCase): ("$foo() Test noargs5", "_test() Test noargs5"), ("Test args1 $foo(a,b,c)", "Test args1 _test(a, b, c)"), ("Test args2 $bar(foo, bar, too)", "Test args2 _test(foo, bar, too)"), - ("Test args3 $bar(foo, bar, ' too')", "Test args3 _test(foo, bar, ' too')"), - ("Test args4 $foo('')", "Test args4 _test('')"), - ('Test args4 $foo("")', 'Test args4 _test("")'), + (r"Test args3 $bar(foo, bar, ' too')", "Test args3 _test(foo, bar, too)"), + ("Test args4 $foo('')", "Test args4 _test()"), + ('Test args4 $foo("")', 'Test args4 _test()'), ("Test args5 $foo(\(\))", "Test args5 _test(())"), ("Test args6 $foo(\()", "Test args6 _test(()"), ("Test args7 $foo(())", "Test args7 _test(())"), ("Test args8 $foo())", "Test args8 _test())"), ("Test args9 $foo(=)", "Test args9 _test(=)"), ("Test args10 $foo(\,)", "Test args10 _test(,)"), - ("Test args10 $foo(',')", "Test args10 _test(',')"), + ("Test args10 $foo(',')", "Test args10 _test(,)"), ("Test args11 $foo(()", "Test args11 $foo(()"), # invalid syntax ( "Test kwarg1 $bar(foo=1, bar='foo', too=ere)", - "Test kwarg1 _test(foo=1, bar='foo', too=ere)", + "Test kwarg1 _test(foo=1, bar=foo, too=ere)", ), ("Test kwarg2 $bar(foo,bar,too=ere)", "Test kwarg2 _test(foo, bar, too=ere)"), ("test kwarg3 $foo(foo = bar, bar = ere )", "test kwarg3 _test(foo=bar, bar=ere)"), ( - "test kwarg4 $foo(foo =' bar ',\" bar \"= ere )", + r"test kwarg4 $foo(foo =\' bar \',\" bar \"= ere )", "test kwarg4 _test(foo=' bar ', \" bar \"=ere)", ), ( @@ -180,22 +181,24 @@ class TestFuncParser(TestCase): ("Test clr $clr(r, This is a red string!)", "Test clr |rThis is a red string!|n"), ("Test eval1 $eval(21 + 21 - 10)", "Test eval1 32"), ("Test eval2 $eval((21 + 21) / 2)", "Test eval2 21.0"), - ("Test eval3 $eval('21' + 'foo' + 'bar')", "Test eval3 21foobar"), - ("Test eval4 $eval('21' + '$repl()' + '' + str(10 // 2))", "Test eval4 21rr5"), - ("Test eval5 $eval('21' + '\$repl()' + '' + str(10 // 2))", "Test eval5 21$repl()5"), - ("Test eval6 $eval('$repl(a)' + '$repl(b)')", "Test eval6 rarrbr"), + ("Test eval3 $eval(\"'21' + 'foo' + 'bar'\")", "Test eval3 21foobar"), + (r"Test eval4 $eval(\'21\' + \'$repl()\' + \"''\" + str(10 // 2))", "Test eval4 21rr5"), + (r"Test eval5 $eval(\'21\' + \'\$repl()\' + \'\' + str(10 // 2))", "Test eval5 21$repl()5"), + ("Test eval6 $eval(\"'$repl(a)' + '$repl(b)'\")", "Test eval6 rarrbr"), ("Test type1 $typ([1,2,3,4])", "Test type1 "), ("Test type2 $typ((1,2,3,4))", "Test type2 "), ("Test type3 $typ({1,2,3,4})", "Test type3 "), ("Test type4 $typ({1:2,3:4})", "Test type4 "), ("Test type5 $typ(1), $typ(1.0)", "Test type5 , "), - ("Test type6 $typ('1'), $typ(\"1.0\")", "Test type6 , "), + ("Test type6 $typ(\"'1'\"), $typ('\"1.0\"')", "Test type6 , "), ("Test add1 $add(1, 2)", "Test add1 3"), ("Test add2 $add([1,2,3,4], [5,6])", "Test add2 [1, 2, 3, 4, 5, 6]"), ("Test literal1 $sum($lit([1,2,3,4,5,6]))", "Test literal1 21"), ("Test literal2 $typ($lit(1))", "Test literal2 "), ("Test literal3 $typ($lit(1)aaa)", "Test literal3 "), ("Test literal4 $typ(aaa$lit(1))", "Test literal4 "), + ("Test spider's thread", "Test spider's thread"), + ] ) def test_parse(self, string, expected): @@ -258,7 +261,11 @@ class TestFuncParser(TestCase): self.assertEqual([1, 2, 3, 4], ret) self.assertTrue(isinstance(ret, list)) - ret = self.parser.parse_to_any("$lit('')") + ret = self.parser.parse_to_any("$lit(\"''\")") + self.assertEqual("", ret) + self.assertTrue(isinstance(ret, str)) + + ret = self.parser.parse_to_any(r"$lit(\'\')") self.assertEqual("", ret) self.assertTrue(isinstance(ret, str)) @@ -390,7 +397,8 @@ class TestDefaultCallables(TestCase): ("Some $rjust(Hello, 30)", "Some Hello"), ("Some $rjust(Hello, width=30)", "Some Hello"), ("Some $cjust(Hello, 30)", "Some Hello "), - ("Some $eval('-'*20)Hello", "Some --------------------Hello"), + ("Some $eval(\"'-'*20\")Hello", "Some --------------------Hello"), + ("$crop(\"spider's silk\", 5)", "spide"), ] ) def test_other_callables(self, string, expected): @@ -455,18 +463,18 @@ class TestDefaultCallables(TestCase): self.parser.parse( "this should be $pad('''escaped,''' and '''instead,''' cropped $crop(with a long,5) text., 80)" ), - "this should be '''escaped,''' and '''instead,''' cropped with text. ", + "this should be escaped, and instead, cropped with text. ", ) def test_escaped2(self): + raw_str = 'this should be $pad("""escaped,""" and """instead,""" cropped $crop(with a long,5) text., 80)' + expected = 'this should be escaped, and instead, cropped with text. ' + result = self.parser.parse(raw_str) self.assertEqual( - self.parser.parse( - 'this should be $pad("""escaped,""" and """instead,""" cropped $crop(with a long,5) text., 80)' - ), - 'this should be """escaped,""" and """instead,""" cropped with text. ', + result, + expected, ) - class TestCallableSearch(test_resources.BaseEvenniaTest): """ Test the $search(query) callable