summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cmd2/cmd2.py159
1 files changed, 92 insertions, 67 deletions
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index e50d9512..f8eb6bee 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -416,6 +416,10 @@ class Cmd(cmd.Cmd):
self.perror('Invalid value: {} (valid values: {}, {}, {})'.format(new_val, ansi.ANSI_TERMINAL,
ansi.ANSI_ALWAYS, ansi.ANSI_NEVER))
+ def _completion_supported(self) -> bool:
+ """Return whether tab completion is supported"""
+ return self.use_rawinput and self.completekey and rl_type != RlType.NONE
+
@property
def visible_prompt(self) -> str:
"""Read-only property to get the visible prompt with any ANSI escape codes stripped.
@@ -1322,7 +1326,7 @@ class Cmd(cmd.Cmd):
"""
# noinspection PyBroadException
try:
- if state == 0 and rl_type != RlType.NONE:
+ if state == 0:
self._reset_completion_defaults()
# Check if we are completing a multiline command
@@ -1649,7 +1653,7 @@ class Cmd(cmd.Cmd):
"""Keep accepting lines of input until the command is complete.
There is some pretty hacky code here to handle some quirks of
- self._pseudo_raw_input(). It returns a literal 'eof' if the input
+ self._read_command_line(). It returns a literal 'eof' if the input
pipe runs out. We can't refactor it because we need to retain
backwards compatibility with the standard library version of cmd.
@@ -1683,7 +1687,7 @@ class Cmd(cmd.Cmd):
# Save the command line up to this point for tab completion
self._multiline_in_progress = line + '\n'
- nextline = self._pseudo_raw_input(self.continuation_prompt)
+ nextline = self._read_command_line(self.continuation_prompt)
if nextline == 'eof':
# they entered either a blank line, or we hit an EOF
# for some other reason. Turn the literal 'eof'
@@ -1989,36 +1993,60 @@ class Cmd(cmd.Cmd):
# Set apply_style to False so default_error's style is not overridden
self.perror(err_msg, apply_style=False)
- def _pseudo_raw_input(self, prompt: str) -> str:
- """Began life as a copy of cmd's cmdloop; like raw_input but
+ def read_input(self, prompt: str, allow_completion: bool = False) -> str:
+ """
+ Read input from appropriate stdin value. Also allows you to disable tab completion while input is being read.
- - accounts for changed stdin, stdout
- - if input is a pipe (instead of a tty), look at self.echo
- to decide whether to print the prompt and the input
+ :param prompt: prompt to display to user
+ :param allow_completion: if True, then tab completion of commands is enabled. This generally should be
+ set to False unless reading the command line. Defaults to False.
+ :return: the line read from stdin with all trailing new lines removed
+ :raises whatever exceptions are raised by input()
"""
- if self.use_rawinput:
- try:
- if sys.stdin.isatty():
- # Wrap in try since terminal_lock may not be locked when this function is called from unit tests
- try:
- # A prompt is about to be drawn. Allow asynchronous changes to the terminal.
- self.terminal_lock.release()
- except RuntimeError:
- pass
+ completion_disabled = False
+ orig_completer = None
+
+ def disable_completion():
+ """Turn off completion during the select input line"""
+ nonlocal orig_completer
+ nonlocal completion_disabled
+
+ if self._completion_supported() and not completion_disabled:
+ orig_completer = readline.get_completer()
+ readline.set_completer(lambda *args, **kwargs: None)
+ completion_disabled = True
+
+ def enable_completion():
+ """Restore tab completion when select is done reading input"""
+ nonlocal completion_disabled
+
+ if self._completion_supported() and completion_disabled:
+ readline.set_completer(orig_completer)
+ completion_disabled = False
+ # Check we are reading from sys.stdin
+ if self.use_rawinput:
+ if sys.stdin.isatty():
+ try:
# Deal with the vagaries of readline and ANSI escape codes
safe_prompt = rl_make_safe_prompt(prompt)
+
+ # Check if tab completion should be disabled
+ with self.sigint_protection:
+ if not allow_completion:
+ disable_completion()
line = input(safe_prompt)
- else:
- line = input()
- if self.echo:
- sys.stdout.write('{}{}\n'.format(prompt, line))
- except EOFError:
- line = 'eof'
- finally:
- if sys.stdin.isatty():
- # The prompt is gone. Do not allow asynchronous changes to the terminal.
- self.terminal_lock.acquire()
+ finally:
+ # Check if we need to reenable tab completion
+ with self.sigint_protection:
+ if not allow_completion:
+ enable_completion()
+ else:
+ line = input()
+ if self.echo:
+ sys.stdout.write('{}{}\n'.format(prompt, line))
+
+ # Otherwise read from self.stdin
else:
if self.stdin.isatty():
# on a tty, print the prompt first, then read the line
@@ -2041,6 +2069,28 @@ class Cmd(cmd.Cmd):
return line.rstrip('\r\n')
+ def _read_command_line(self, prompt: str) -> str:
+ """
+ Read command line from appropriate stdin
+
+ :param prompt: prompt to display to user
+ :return: command line text of 'eof' if an EOFError was caught
+ :raises whatever exceptions are raised by input() except for EOFError
+ """
+ try:
+ # Wrap in try since terminal_lock may not be locked
+ try:
+ # Command line is about to be drawn. Allow asynchronous changes to the terminal.
+ self.terminal_lock.release()
+ except RuntimeError:
+ pass
+ return self.read_input(prompt, allow_completion=True)
+ except EOFError:
+ return 'eof'
+ finally:
+ # Command line is gone. Do not allow asynchronous changes to the terminal.
+ self.terminal_lock.acquire()
+
def _set_up_cmd2_readline(self) -> _SavedReadlineSettings:
"""
Set up readline with cmd2-specific settings
@@ -2048,7 +2098,7 @@ class Cmd(cmd.Cmd):
"""
readline_settings = _SavedReadlineSettings()
- if self.use_rawinput and self.completekey and rl_type != RlType.NONE:
+ if self._completion_supported():
# Set up readline for our tab completion needs
if rl_type == RlType.GNU:
@@ -2080,7 +2130,7 @@ class Cmd(cmd.Cmd):
Restore saved readline settings
:param readline_settings: the readline settings to restore
"""
- if self.use_rawinput and self.completekey and rl_type != RlType.NONE:
+ if self._completion_supported():
# Restore what we changed in readline
readline.set_completer(readline_settings.completer)
@@ -2114,7 +2164,7 @@ class Cmd(cmd.Cmd):
while not stop:
# Get commands from user
try:
- line = self._pseudo_raw_input(self.prompt)
+ line = self._read_command_line(self.prompt)
except KeyboardInterrupt as ex:
if self.quit_on_sigint:
raise ex
@@ -2693,27 +2743,6 @@ class Cmd(cmd.Cmd):
that the return value can differ from
the text advertised to the user """
- completion_disabled = False
- orig_completer = None
-
- def disable_completion():
- """Turn off completion during the select input line"""
- nonlocal orig_completer
- nonlocal completion_disabled
-
- if rl_type != RlType.NONE and not completion_disabled:
- orig_completer = readline.get_completer()
- readline.set_completer(lambda *args, **kwargs: None)
- completion_disabled = True
-
- def enable_completion():
- """Restore tab completion when select is done reading input"""
- nonlocal completion_disabled
-
- if rl_type != RlType.NONE and completion_disabled:
- readline.set_completer(orig_completer)
- completion_disabled = False
-
local_opts = opts
if isinstance(opts, str):
local_opts = list(zip(opts.split(), opts.split()))
@@ -2730,18 +2759,14 @@ class Cmd(cmd.Cmd):
self.poutput(' %2d. %s' % (idx + 1, text))
while True:
- safe_prompt = rl_make_safe_prompt(prompt)
-
try:
- with self.sigint_protection:
- disable_completion()
- response = input(safe_prompt)
+ response = self.read_input(prompt)
except EOFError:
response = ''
self.poutput('\n', end='')
- finally:
- with self.sigint_protection:
- enable_completion()
+ except KeyboardInterrupt as ex:
+ self.poutput('^C')
+ raise ex
if not response:
continue
@@ -2921,7 +2946,7 @@ class Cmd(cmd.Cmd):
for item in self._py_history:
readline.add_history(item)
- if self.use_rawinput and self.completekey:
+ if self._completion_supported():
# Set up tab completion for the Python console
# rlcompleter relies on the default settings of the Python readline module
if rl_type == RlType.GNU:
@@ -2988,7 +3013,7 @@ class Cmd(cmd.Cmd):
for item in cmd2_env.history:
readline.add_history(item)
- if self.use_rawinput and self.completekey:
+ if self._completion_supported():
# Restore cmd2's tab completion settings
readline.set_completer(cmd2_env.readline_settings.completer)
readline.set_completer_delims(cmd2_env.readline_settings.delims)
@@ -3715,7 +3740,7 @@ class Cmd(cmd.Cmd):
def async_alert(self, alert_msg: str, new_prompt: Optional[str] = None) -> None: # pragma: no cover
"""
- Display an important message to the user while they are at the prompt in between commands.
+ Display an important message to the user while they are at a command line prompt.
To the user it appears as if an alert message is printed above the prompt and their current input
text and cursor location is left alone.
@@ -3775,10 +3800,10 @@ class Cmd(cmd.Cmd):
def async_update_prompt(self, new_prompt: str) -> None: # pragma: no cover
"""
- Update the prompt while the user is still typing at it. This is good for alerting the user to system
- changes dynamically in between commands. For instance you could alter the color of the prompt to indicate
- a system status or increase a counter to report an event. If you do alter the actual text of the prompt,
- it is best to keep the prompt the same width as what's on screen. Otherwise the user's input text will
+ Update the command line prompt while the user is still typing at it. This is good for alerting the user to
+ system changes dynamically in between commands. For instance you could alter the color of the prompt to
+ indicate a system status or increase a counter to report an event. If you do alter the actual text of the
+ prompt, it is best to keep the prompt the same width as what's on screen. Otherwise the user's input text will
be shifted and the update will not be seamless.
Raises a `RuntimeError` if called while another thread holds `terminal_lock`.
@@ -3948,7 +3973,7 @@ class Cmd(cmd.Cmd):
original_sigint_handler = signal.getsignal(signal.SIGINT)
signal.signal(signal.SIGINT, self.sigint_handler)
- # Grab terminal lock before the prompt has been drawn by readline
+ # Grab terminal lock before the command line prompt has been drawn by readline
self.terminal_lock.acquire()
# Always run the preloop first