153 lines
4.4 KiB
Python
153 lines
4.4 KiB
Python
|
|
from __future__ import with_statement
|
||
|
|
|
||
|
|
import click
|
||
|
|
import sys
|
||
|
|
from prompt_toolkit import PromptSession
|
||
|
|
from prompt_toolkit.history import InMemoryHistory
|
||
|
|
|
||
|
|
from ._completer import ClickCompleter
|
||
|
|
from .exceptions import ClickExit # type: ignore[attr-defined]
|
||
|
|
from .exceptions import CommandLineParserError, ExitReplException, InvalidGroupFormat
|
||
|
|
from .utils import _execute_internal_and_sys_cmds
|
||
|
|
|
||
|
|
|
||
|
|
__all__ = ["bootstrap_prompt", "register_repl", "repl"]
|
||
|
|
|
||
|
|
|
||
|
|
def bootstrap_prompt(
|
||
|
|
group,
|
||
|
|
prompt_kwargs,
|
||
|
|
ctx=None,
|
||
|
|
):
|
||
|
|
"""
|
||
|
|
Bootstrap prompt_toolkit kwargs or use user defined values.
|
||
|
|
|
||
|
|
:param group: click Group
|
||
|
|
:param prompt_kwargs: The user specified prompt kwargs.
|
||
|
|
"""
|
||
|
|
|
||
|
|
defaults = {
|
||
|
|
"history": InMemoryHistory(),
|
||
|
|
"completer": ClickCompleter(group, ctx=ctx),
|
||
|
|
"message": "> ",
|
||
|
|
}
|
||
|
|
|
||
|
|
defaults.update(prompt_kwargs)
|
||
|
|
return defaults
|
||
|
|
|
||
|
|
|
||
|
|
def repl(
|
||
|
|
old_ctx, prompt_kwargs={}, allow_system_commands=True, allow_internal_commands=True
|
||
|
|
):
|
||
|
|
"""
|
||
|
|
Start an interactive shell. All subcommands are available in it.
|
||
|
|
|
||
|
|
:param old_ctx: The current Click context.
|
||
|
|
:param prompt_kwargs: Parameters passed to
|
||
|
|
:py:func:`prompt_toolkit.PromptSession`.
|
||
|
|
|
||
|
|
If stdin is not a TTY, no prompt will be printed, but only commands read
|
||
|
|
from stdin.
|
||
|
|
"""
|
||
|
|
|
||
|
|
group_ctx = old_ctx
|
||
|
|
# Switching to the parent context that has a Group as its command
|
||
|
|
# as a Group acts as a CLI for all of its subcommands
|
||
|
|
if old_ctx.parent is not None and not isinstance(old_ctx.command, click.Group):
|
||
|
|
group_ctx = old_ctx.parent
|
||
|
|
|
||
|
|
group = group_ctx.command
|
||
|
|
|
||
|
|
# An Optional click.Argument in the CLI Group, that has no value
|
||
|
|
# will consume the first word from the REPL input, causing issues in
|
||
|
|
# executing the command
|
||
|
|
# So, if there's an empty Optional Argument
|
||
|
|
for param in group.params:
|
||
|
|
if (
|
||
|
|
isinstance(param, click.Argument)
|
||
|
|
and group_ctx.params[param.name] is None
|
||
|
|
and not param.required
|
||
|
|
):
|
||
|
|
raise InvalidGroupFormat(
|
||
|
|
f"{type(group).__name__} '{group.name}' requires value for "
|
||
|
|
f"an optional argument '{param.name}' in REPL mode"
|
||
|
|
)
|
||
|
|
|
||
|
|
isatty = sys.stdin.isatty()
|
||
|
|
|
||
|
|
# Delete the REPL command from those available, as we don't want to allow
|
||
|
|
# nesting REPLs (note: pass `None` to `pop` as we don't want to error if
|
||
|
|
# REPL command already not present for some reason).
|
||
|
|
repl_command_name = old_ctx.command.name
|
||
|
|
if isinstance(group_ctx.command, click.CommandCollection):
|
||
|
|
available_commands = {
|
||
|
|
cmd_name: cmd_obj
|
||
|
|
for source in group_ctx.command.sources
|
||
|
|
for cmd_name, cmd_obj in source.commands.items()
|
||
|
|
}
|
||
|
|
else:
|
||
|
|
available_commands = group_ctx.command.commands
|
||
|
|
|
||
|
|
original_command = available_commands.pop(repl_command_name, None)
|
||
|
|
|
||
|
|
if isatty:
|
||
|
|
prompt_kwargs = bootstrap_prompt(group, prompt_kwargs, group_ctx)
|
||
|
|
session = PromptSession(**prompt_kwargs)
|
||
|
|
|
||
|
|
def get_command():
|
||
|
|
return session.prompt()
|
||
|
|
|
||
|
|
else:
|
||
|
|
get_command = sys.stdin.readline
|
||
|
|
|
||
|
|
while True:
|
||
|
|
try:
|
||
|
|
command = get_command()
|
||
|
|
except KeyboardInterrupt:
|
||
|
|
continue
|
||
|
|
except EOFError:
|
||
|
|
break
|
||
|
|
|
||
|
|
if not command:
|
||
|
|
if isatty:
|
||
|
|
continue
|
||
|
|
else:
|
||
|
|
break
|
||
|
|
|
||
|
|
try:
|
||
|
|
args = _execute_internal_and_sys_cmds(
|
||
|
|
command, allow_internal_commands, allow_system_commands
|
||
|
|
)
|
||
|
|
if args is None:
|
||
|
|
continue
|
||
|
|
|
||
|
|
except CommandLineParserError:
|
||
|
|
continue
|
||
|
|
|
||
|
|
except ExitReplException:
|
||
|
|
break
|
||
|
|
|
||
|
|
try:
|
||
|
|
# The group command will dispatch based on args.
|
||
|
|
old_protected_args = group_ctx.protected_args
|
||
|
|
try:
|
||
|
|
group_ctx.protected_args = args
|
||
|
|
group.invoke(group_ctx)
|
||
|
|
finally:
|
||
|
|
group_ctx.protected_args = old_protected_args
|
||
|
|
except click.ClickException as e:
|
||
|
|
e.show()
|
||
|
|
except (ClickExit, SystemExit):
|
||
|
|
pass
|
||
|
|
|
||
|
|
except ExitReplException:
|
||
|
|
break
|
||
|
|
|
||
|
|
if original_command is not None:
|
||
|
|
available_commands[repl_command_name] = original_command
|
||
|
|
|
||
|
|
|
||
|
|
def register_repl(group, name="repl"):
|
||
|
|
"""Register :func:`repl()` as sub-command *name* of *group*."""
|
||
|
|
group.command(name=name)(click.pass_context(repl))
|