Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding Linux-style command chaining #46

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion click_shell/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@
'__version__',
]

__version__ = "3.0.dev0"
__version__ = "3.0.dev1"
88 changes: 85 additions & 3 deletions click_shell/cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,66 @@
from ._compat import readline


class CommandChainException(Exception):
pass


class Command:
INITIAL = 'INITIAL'
AND = 'AND'
OR = 'OR'

def __init__(self, command: str, cmd_type: Union[AND, OR, INITIAL]):
self.text = command
self.cmd_type = cmd_type


class CommandQueue(List):
"""
ClickCmd expects a List, so we implement our own with parsing
"""

def __init__(self):
super().__init__()
self.queue = list()

def parse_line(self, *, line: str):
initial_command = True
for command in line.split("&&"):
command = command.strip()
if initial_command:
self.queue.append(Command(command=command, cmd_type=Command.INITIAL))
initial_command = False
continue
if '||' not in command:
self.queue.append(Command(command=command, cmd_type=Command.AND))
else:
first = True
for _command in command.split("||"):
_command = _command.strip()
if first:
self.queue.append(Command(command=_command, cmd_type=Command.AND))
first = False
continue
self.queue.append(Command(command=_command, cmd_type=Command.OR))

def pop(self, __index: int = ...):
return self.queue.pop(__index)

def flush(self) -> None:
"""
Flushes out the queue by setting the property to an empty list
:return:
"""
self.queue = list()

def __len__(self) -> int:
"""
:return: int length of self.queue
"""
return len(self.queue)


class ClickCmd(Cmd):
"""
A simple wrapper around the builtin python cmd module that:
Expand Down Expand Up @@ -57,6 +117,8 @@ def __init__(
if not os.path.isdir(os.path.dirname(self.hist_file)):
os.makedirs(os.path.dirname(self.hist_file))

self.cmdqueue = CommandQueue()

def preloop(self):
# read our history
if readline:
Expand Down Expand Up @@ -108,9 +170,12 @@ def cmdloop(self, intro: str = None): # pylint: disable=too-many-branches
click.echo(file=self._stdout)
click.echo('KeyboardInterrupt', file=self._stdout)
continue
line = self.precmd(line)
stop = self.onecmd(line)
stop = self.postcmd(stop, line)
try:
line = self.precmd(line)
stop = self.onecmd(line)
stop = self.postcmd(stop, line)
except CommandChainException:
self.cmdqueue.flush()

finally:
self.postloop()
Expand All @@ -120,6 +185,23 @@ def cmdloop(self, intro: str = None): # pylint: disable=too-many-branches
if self.old_delims:
readline.set_completer_delims(self.old_delims)

def precmd(self, line: Union[str, Command]) -> str:
if isinstance(line, str):
if any([chain in line for chain in ["&&", "||"]]):
self.cmdqueue.parse_line(line=line)
line = self.cmdqueue.pop(0).text
if isinstance(line, Command):
# a non-zero return code followed by and AND-ed command
if self.ctx.return_code and line.cmd_type == Command.AND: # noqa: return_code set in initial context
raise CommandChainException
# a zero return code followed by an OR-ed command
elif not self.ctx.return_code and line.cmd_type == Command.OR: # noqa: return_code set in initial context
raise CommandChainException
else:
line = line.text

return line

def get_prompt(self) -> Optional[str]:
if callable(self.prompt):
kwargs = {}
Expand Down
9 changes: 5 additions & 4 deletions click_shell/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,11 @@ def get_invoke(command: click.Command) -> Callable[[ClickCmd, str], bool]:

def invoke_(self: ClickCmd, arg: str): # pylint: disable=unused-argument
try:
command.main(args=shlex.split(arg),
prog_name=command.name,
standalone_mode=False,
parent=self.ctx)
self.ctx.return_code = 0 # Set initial code
self.ctx.return_code = command.main(args=shlex.split(arg),
prog_name=command.name,
standalone_mode=False,
parent=self.ctx)
except click.ClickException as e:
# Show the error message
e.show()
Expand Down
7 changes: 7 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import pytest
from click.testing import CliRunner


@pytest.fixture
def cli_runner():
return CliRunner()