diff options
-rw-r--r-- | cmd2/cmd2.py | 52 | ||||
-rw-r--r-- | tests/test_completion.py | 75 |
2 files changed, 115 insertions, 12 deletions
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 217a92c8..7efa6849 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -1195,7 +1195,7 @@ class Cmd(cmd.Cmd): def _redirect_complete(self, text: str, line: str, begidx: int, endidx: int, compfunc: Callable) -> List[str]: """Called by complete() as the first tab completion function for all commands - It determines if it should tab complete for redirection (|, <, >, >>) or use the + It determines if it should tab complete for redirection (|, >, >>) or use the completer function for the current command :param text: the string prefix we are attempting to match (all returned matches must begin with it) @@ -1214,28 +1214,58 @@ class Cmd(cmd.Cmd): if not raw_tokens: return [] + # Must at least have the command if len(raw_tokens) > 1: - # Check if there are redirection strings prior to the token being completed - seen_pipe = False + # True when command line contains any redirection tokens has_redirection = False - for cur_token in raw_tokens[:-1]: + # Keep track of state while examining tokens + in_pipe = False + in_file_redir = False + do_shell_completion = False + do_path_completion = False + prior_token = None + + for cur_token in raw_tokens: + # Process redirection tokens if cur_token in constants.REDIRECTION_TOKENS: has_redirection = True + # Check if we are at a pipe if cur_token == constants.REDIRECTION_PIPE: - seen_pipe = True + # Do not complete bad syntax (e.g cmd | |) + if prior_token == constants.REDIRECTION_PIPE: + return [] + + in_pipe = True + in_file_redir = False + + # Otherwise this is a file redirection token + else: + if prior_token in constants.REDIRECTION_TOKENS or in_file_redir: + # Do not complete bad syntax (e.g cmd | >) (e.g cmd > blah >) + return [] + + in_pipe = False + in_file_redir = True + + # Not a redirection token + else: + do_shell_completion = False + do_path_completion = False + + if prior_token == constants.REDIRECTION_PIPE: + do_shell_completion = True + elif in_pipe or prior_token in (constants.REDIRECTION_OUTPUT, constants.REDIRECTION_APPEND): + do_path_completion = True - # Get token prior to the one being completed - prior_token = raw_tokens[-2] + prior_token = cur_token - # If a pipe is right before the token being completed, complete a shell command as the piped process - if prior_token == constants.REDIRECTION_PIPE: + if do_shell_completion: return self.shell_cmd_complete(text, line, begidx, endidx) - # Otherwise do path completion either as files to redirectors or arguments to the piped process - elif prior_token in constants.REDIRECTION_TOKENS or seen_pipe: + elif do_path_completion: return self.path_complete(text, line, begidx, endidx) # If there were redirection strings anywhere on the command line, then we diff --git a/tests/test_completion.py b/tests/test_completion.py index 9157ce84..eea34ba6 100644 --- a/tests/test_completion.py +++ b/tests/test_completion.py @@ -6,15 +6,23 @@ Unit/functional testing for readline tab-completion functions in the cmd2.py mod These are primarily tests related to readline completer functions which handle tab-completion of cmd2/cmd commands, file system paths, and shell commands. """ +# Python 3.5 had some regressions in the unitest.mock module, so use 3rd party mock if available +try: + import mock +except ImportError: + from unittest import mock + import argparse +import enum import os import sys import pytest + import cmd2 from cmd2 import utils -from .conftest import base_app, complete_tester, normalize, run_cmd from examples.subcommands import SubcommandsExample +from .conftest import complete_tester, normalize, run_cmd # List of strings used with completion functions food_item_strs = ['Pizza', 'Ham', 'Ham Sandwich', 'Potato', 'Cheese "Pizza"'] @@ -854,6 +862,71 @@ def test_quote_as_command(cmd2_app): first_match = complete_tester(text, line, begidx, endidx, cmd2_app) assert first_match is None and not cmd2_app.completion_matches + +# Used by redirect_complete tests +class RedirCompType(enum.Enum): + SHELL_CMD = 1, + PATH = 2, + DEFAULT = 3, + NONE = 4 + + """ + fake > > + fake | grep > file + fake | grep > file > + """ + +@pytest.mark.parametrize('line, comp_type', [ + ('fake', RedirCompType.DEFAULT), + ('fake arg', RedirCompType.DEFAULT), + ('fake |', RedirCompType.SHELL_CMD), + ('fake | grep', RedirCompType.PATH), + ('fake | grep arg', RedirCompType.PATH), + ('fake | grep >', RedirCompType.PATH), + ('fake | grep > >', RedirCompType.NONE), + ('fake | grep > file', RedirCompType.NONE), + ('fake | grep > file >', RedirCompType.NONE), + ('fake | grep > file |', RedirCompType.SHELL_CMD), + ('fake | grep > file | grep', RedirCompType.PATH), + ('fake | |', RedirCompType.NONE), + ('fake | >', RedirCompType.NONE), + ('fake >', RedirCompType.PATH), + ('fake >>', RedirCompType.PATH), + ('fake > >', RedirCompType.NONE), + ('fake > |', RedirCompType.SHELL_CMD), + ('fake >> file |', RedirCompType.SHELL_CMD), + ('fake >> file | grep', RedirCompType.PATH), + ('fake > file', RedirCompType.NONE), + ('fake > file >', RedirCompType.NONE), + ('fake > file >>', RedirCompType.NONE), +]) +def test_redirect_complete(cmd2_app, monkeypatch, line, comp_type): + shell_cmd_complete_mock = mock.MagicMock(name='shell_cmd_complete') + monkeypatch.setattr("cmd2.Cmd.shell_cmd_complete", shell_cmd_complete_mock) + + path_complete_mock = mock.MagicMock(name='path_complete') + monkeypatch.setattr("cmd2.Cmd.path_complete", path_complete_mock) + + default_complete_mock = mock.MagicMock(name='fake_completer') + + text = '' + line = '{} {}'.format(line, text) + endidx = len(line) + begidx = endidx - len(text) + + cmd2_app._redirect_complete(text, line, begidx, endidx, default_complete_mock) + + if comp_type == RedirCompType.SHELL_CMD: + shell_cmd_complete_mock.assert_called_once() + elif comp_type == RedirCompType.PATH: + path_complete_mock.assert_called_once() + elif comp_type == RedirCompType.DEFAULT: + default_complete_mock.assert_called_once() + else: + shell_cmd_complete_mock.assert_not_called() + path_complete_mock.assert_not_called() + default_complete_mock.assert_not_called() + @pytest.fixture def sc_app(): c = SubcommandsExample() |