diff --git a/news/andash.rst b/news/andash.rst new file mode 100644 index 000000000..f1ea2680a --- /dev/null +++ b/news/andash.rst @@ -0,0 +1,28 @@ +**Added:** + +* + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* Fixed issue with ``and`` & ``or`` being incoreectly tokenized in implicit + subprocesses. Auto-wrapping of certain subprocesses will now correctly work. + For example:: + + $ echo x-and-y + x-and-y + +**Security:** + +* diff --git a/tests/sample.xsh b/tests/sample.xsh index 9f6b92249..0795e25ac 100644 --- a/tests/sample.xsh +++ b/tests/sample.xsh @@ -2,4 +2,4 @@ aliases['echo'] = lambda args, stdin=None: print(' '.join(args)) $WAKKA = "jawaka" -x = $(echo "hello mom" $WAKKA) \ No newline at end of file +x = $(echo "hello mom" $WAKKA) diff --git a/tests/test_lexer.py b/tests/test_lexer.py index ca9a13598..9b84e0986 100644 --- a/tests/test_lexer.py +++ b/tests/test_lexer.py @@ -165,7 +165,10 @@ def test_atdollar_expression(): def test_and(): - assert check_token("and", ["AND", "and", 0]) + # no preceding whitespace or other tokens, so this + # resolves to NAME, since it doesn't make sense for + # Python code to start with "and" + assert check_token("and", ["NAME", "and", 0]) def test_ampersand(): diff --git a/tests/test_tools.py b/tests/test_tools.py index 09dd1b231..74ff7e834 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -374,6 +374,29 @@ def test_subproc_toks_pyeval_nested(): assert exp == obs +@pytest.mark.parametrize('phrase', [ + 'xandy', + 'xory', + 'xand', + 'andy', + 'xor', + 'ory', + 'x-and', + 'x-or', + 'and-y', + 'or-y', + 'x-and-y', + 'x-or-y', + 'in/and/path', + 'in/or/path', +]) +def test_subproc_toks_and_or(phrase): + s = "echo " + phrase + exp = "![{0}]".format(s) + obs = subproc_toks(s, lexer=LEXER, returnline=True) + assert exp == obs + + def test_subproc_toks_pyeval_nested_parens(): s = "echo @(min(1, 42))" inp = "({0})".format(s) diff --git a/xonsh/lexer.py b/xonsh/lexer.py index 9eff6d6f0..e51cdd82a 100644 --- a/xonsh/lexer.py +++ b/xonsh/lexer.py @@ -4,6 +4,7 @@ Written using a hybrid of ``tokenize`` and PLY. """ import io +import re # 'keyword' interferes with ast.keyword import keyword as kwmod @@ -116,21 +117,33 @@ def token_map(): return tm +NEED_WHITESPACE = frozenset(["and", "or"]) + + +@lazyobject +def RE_NEED_WHITESPACE(): + pattern = r"\s?(" + "|".join(NEED_WHITESPACE) + r")(\s|[\\]$)" + return re.compile(pattern) + + def handle_name(state, token): """Function for handling name tokens""" typ = "NAME" + state["last"] = token + needs_whitespace = token.string in NEED_WHITESPACE + has_whitespace = needs_whitespace and RE_NEED_WHITESPACE.match( + token.line[max(0, token.start[1] - 1) :] + ) if state["pymode"][-1][0]: - if token.string in kwmod.kwlist: + if needs_whitespace and not has_whitespace: + pass + elif token.string in kwmod.kwlist: typ = token.string.upper() - state["last"] = token yield _new_token(typ, token.string, token.start) else: - prev = state["last"] - state["last"] = token - has_whitespace = prev.end != token.start - if token.string == "and" and has_whitespace: + if has_whitespace and token.string == "and": yield _new_token("AND", token.string, token.start) - elif token.string == "or" and has_whitespace: + elif has_whitespace and token.string == "or": yield _new_token("OR", token.string, token.start) else: yield _new_token("NAME", token.string, token.start)