Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 41 additions & 11 deletions cmd2/cmd2.py
Original file line number Diff line number Diff line change
Expand Up @@ -1200,7 +1200,7 @@ def shell_cmd_complete(self, text: str, line: str, begidx: int, endidx: int,

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)
Expand All @@ -1219,28 +1219,58 @@ def _redirect_complete(self, text: str, line: str, begidx: int, endidx: int, com
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
Expand Down
75 changes: 74 additions & 1 deletion tests/test_completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,23 @@
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"']
Expand Down Expand Up @@ -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', [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding all of these unit tests

('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()
Expand Down