Skip to content

Commit 4202135

Browse files
committed
Added cmd2-specific input() function that reads from the correct stdin and can disable tab completion
1 parent 2cf4d87 commit 4202135

File tree

1 file changed

+92
-67
lines changed

1 file changed

+92
-67
lines changed

cmd2/cmd2.py

Lines changed: 92 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,10 @@ def allow_ansi(self, new_val: str) -> None:
416416
self.perror('Invalid value: {} (valid values: {}, {}, {})'.format(new_val, ansi.ANSI_TERMINAL,
417417
ansi.ANSI_ALWAYS, ansi.ANSI_NEVER))
418418

419+
def _completion_supported(self) -> bool:
420+
"""Return whether tab completion is supported"""
421+
return self.use_rawinput and self.completekey and rl_type != RlType.NONE
422+
419423
@property
420424
def visible_prompt(self) -> str:
421425
"""Read-only property to get the visible prompt with any ANSI escape codes stripped.
@@ -1322,7 +1326,7 @@ def complete(self, text: str, state: int) -> Optional[str]:
13221326
"""
13231327
# noinspection PyBroadException
13241328
try:
1325-
if state == 0 and rl_type != RlType.NONE:
1329+
if state == 0:
13261330
self._reset_completion_defaults()
13271331

13281332
# Check if we are completing a multiline command
@@ -1649,7 +1653,7 @@ def _complete_statement(self, line: str) -> Statement:
16491653
"""Keep accepting lines of input until the command is complete.
16501654
16511655
There is some pretty hacky code here to handle some quirks of
1652-
self._pseudo_raw_input(). It returns a literal 'eof' if the input
1656+
self._read_command_line(). It returns a literal 'eof' if the input
16531657
pipe runs out. We can't refactor it because we need to retain
16541658
backwards compatibility with the standard library version of cmd.
16551659
@@ -1683,7 +1687,7 @@ def _complete_statement(self, line: str) -> Statement:
16831687
# Save the command line up to this point for tab completion
16841688
self._multiline_in_progress = line + '\n'
16851689

1686-
nextline = self._pseudo_raw_input(self.continuation_prompt)
1690+
nextline = self._read_command_line(self.continuation_prompt)
16871691
if nextline == 'eof':
16881692
# they entered either a blank line, or we hit an EOF
16891693
# for some other reason. Turn the literal 'eof'
@@ -1989,36 +1993,60 @@ def default(self, statement: Statement) -> Optional[bool]:
19891993
# Set apply_style to False so default_error's style is not overridden
19901994
self.perror(err_msg, apply_style=False)
19911995

1992-
def _pseudo_raw_input(self, prompt: str) -> str:
1993-
"""Began life as a copy of cmd's cmdloop; like raw_input but
1996+
def read_input(self, prompt: str, allow_completion: bool = False) -> str:
1997+
"""
1998+
Read input from appropriate stdin value. Also allows you to disable tab completion while input is being read.
19941999
1995-
- accounts for changed stdin, stdout
1996-
- if input is a pipe (instead of a tty), look at self.echo
1997-
to decide whether to print the prompt and the input
2000+
:param prompt: prompt to display to user
2001+
:param allow_completion: if True, then tab completion of commands is enabled. This generally should be
2002+
set to False unless reading the command line. Defaults to False.
2003+
:return: the line read from stdin with all trailing new lines removed
2004+
:raises whatever exceptions are raised by input()
19982005
"""
1999-
if self.use_rawinput:
2000-
try:
2001-
if sys.stdin.isatty():
2002-
# Wrap in try since terminal_lock may not be locked when this function is called from unit tests
2003-
try:
2004-
# A prompt is about to be drawn. Allow asynchronous changes to the terminal.
2005-
self.terminal_lock.release()
2006-
except RuntimeError:
2007-
pass
2006+
completion_disabled = False
2007+
orig_completer = None
2008+
2009+
def disable_completion():
2010+
"""Turn off completion during the select input line"""
2011+
nonlocal orig_completer
2012+
nonlocal completion_disabled
2013+
2014+
if self._completion_supported() and not completion_disabled:
2015+
orig_completer = readline.get_completer()
2016+
readline.set_completer(lambda *args, **kwargs: None)
2017+
completion_disabled = True
2018+
2019+
def enable_completion():
2020+
"""Restore tab completion when select is done reading input"""
2021+
nonlocal completion_disabled
2022+
2023+
if self._completion_supported() and completion_disabled:
2024+
readline.set_completer(orig_completer)
2025+
completion_disabled = False
20082026

2027+
# Check we are reading from sys.stdin
2028+
if self.use_rawinput:
2029+
if sys.stdin.isatty():
2030+
try:
20092031
# Deal with the vagaries of readline and ANSI escape codes
20102032
safe_prompt = rl_make_safe_prompt(prompt)
2033+
2034+
# Check if tab completion should be disabled
2035+
with self.sigint_protection:
2036+
if not allow_completion:
2037+
disable_completion()
20112038
line = input(safe_prompt)
2012-
else:
2013-
line = input()
2014-
if self.echo:
2015-
sys.stdout.write('{}{}\n'.format(prompt, line))
2016-
except EOFError:
2017-
line = 'eof'
2018-
finally:
2019-
if sys.stdin.isatty():
2020-
# The prompt is gone. Do not allow asynchronous changes to the terminal.
2021-
self.terminal_lock.acquire()
2039+
finally:
2040+
# Check if we need to reenable tab completion
2041+
with self.sigint_protection:
2042+
if not allow_completion:
2043+
enable_completion()
2044+
else:
2045+
line = input()
2046+
if self.echo:
2047+
sys.stdout.write('{}{}\n'.format(prompt, line))
2048+
2049+
# Otherwise read from self.stdin
20222050
else:
20232051
if self.stdin.isatty():
20242052
# on a tty, print the prompt first, then read the line
@@ -2041,14 +2069,36 @@ def _pseudo_raw_input(self, prompt: str) -> str:
20412069

20422070
return line.rstrip('\r\n')
20432071

2072+
def _read_command_line(self, prompt: str) -> str:
2073+
"""
2074+
Read command line from appropriate stdin
2075+
2076+
:param prompt: prompt to display to user
2077+
:return: command line text of 'eof' if an EOFError was caught
2078+
:raises whatever exceptions are raised by input() except for EOFError
2079+
"""
2080+
try:
2081+
# Wrap in try since terminal_lock may not be locked
2082+
try:
2083+
# Command line is about to be drawn. Allow asynchronous changes to the terminal.
2084+
self.terminal_lock.release()
2085+
except RuntimeError:
2086+
pass
2087+
return self.read_input(prompt, allow_completion=True)
2088+
except EOFError:
2089+
return 'eof'
2090+
finally:
2091+
# Command line is gone. Do not allow asynchronous changes to the terminal.
2092+
self.terminal_lock.acquire()
2093+
20442094
def _set_up_cmd2_readline(self) -> _SavedReadlineSettings:
20452095
"""
20462096
Set up readline with cmd2-specific settings
20472097
:return: Class containing saved readline settings
20482098
"""
20492099
readline_settings = _SavedReadlineSettings()
20502100

2051-
if self.use_rawinput and self.completekey and rl_type != RlType.NONE:
2101+
if self._completion_supported():
20522102

20532103
# Set up readline for our tab completion needs
20542104
if rl_type == RlType.GNU:
@@ -2080,7 +2130,7 @@ def _restore_readline(self, readline_settings: _SavedReadlineSettings):
20802130
Restore saved readline settings
20812131
:param readline_settings: the readline settings to restore
20822132
"""
2083-
if self.use_rawinput and self.completekey and rl_type != RlType.NONE:
2133+
if self._completion_supported():
20842134

20852135
# Restore what we changed in readline
20862136
readline.set_completer(readline_settings.completer)
@@ -2114,7 +2164,7 @@ def _cmdloop(self) -> None:
21142164
while not stop:
21152165
# Get commands from user
21162166
try:
2117-
line = self._pseudo_raw_input(self.prompt)
2167+
line = self._read_command_line(self.prompt)
21182168
except KeyboardInterrupt as ex:
21192169
if self.quit_on_sigint:
21202170
raise ex
@@ -2693,27 +2743,6 @@ def select(self, opts: Union[str, List[str], List[Tuple[Any, Optional[str]]]],
26932743
that the return value can differ from
26942744
the text advertised to the user """
26952745

2696-
completion_disabled = False
2697-
orig_completer = None
2698-
2699-
def disable_completion():
2700-
"""Turn off completion during the select input line"""
2701-
nonlocal orig_completer
2702-
nonlocal completion_disabled
2703-
2704-
if rl_type != RlType.NONE and not completion_disabled:
2705-
orig_completer = readline.get_completer()
2706-
readline.set_completer(lambda *args, **kwargs: None)
2707-
completion_disabled = True
2708-
2709-
def enable_completion():
2710-
"""Restore tab completion when select is done reading input"""
2711-
nonlocal completion_disabled
2712-
2713-
if rl_type != RlType.NONE and completion_disabled:
2714-
readline.set_completer(orig_completer)
2715-
completion_disabled = False
2716-
27172746
local_opts = opts
27182747
if isinstance(opts, str):
27192748
local_opts = list(zip(opts.split(), opts.split()))
@@ -2730,18 +2759,14 @@ def enable_completion():
27302759
self.poutput(' %2d. %s' % (idx + 1, text))
27312760

27322761
while True:
2733-
safe_prompt = rl_make_safe_prompt(prompt)
2734-
27352762
try:
2736-
with self.sigint_protection:
2737-
disable_completion()
2738-
response = input(safe_prompt)
2763+
response = self.read_input(prompt)
27392764
except EOFError:
27402765
response = ''
27412766
self.poutput('\n', end='')
2742-
finally:
2743-
with self.sigint_protection:
2744-
enable_completion()
2767+
except KeyboardInterrupt as ex:
2768+
self.poutput('^C')
2769+
raise ex
27452770

27462771
if not response:
27472772
continue
@@ -2921,7 +2946,7 @@ def _set_up_py_shell_env(self, interp: InteractiveConsole) -> _SavedCmd2Env:
29212946
for item in self._py_history:
29222947
readline.add_history(item)
29232948

2924-
if self.use_rawinput and self.completekey:
2949+
if self._completion_supported():
29252950
# Set up tab completion for the Python console
29262951
# rlcompleter relies on the default settings of the Python readline module
29272952
if rl_type == RlType.GNU:
@@ -2988,7 +3013,7 @@ def _restore_cmd2_env(self, cmd2_env: _SavedCmd2Env) -> None:
29883013
for item in cmd2_env.history:
29893014
readline.add_history(item)
29903015

2991-
if self.use_rawinput and self.completekey:
3016+
if self._completion_supported():
29923017
# Restore cmd2's tab completion settings
29933018
readline.set_completer(cmd2_env.readline_settings.completer)
29943019
readline.set_completer_delims(cmd2_env.readline_settings.delims)
@@ -3715,7 +3740,7 @@ class TestMyAppCase(Cmd2TestCase):
37153740

37163741
def async_alert(self, alert_msg: str, new_prompt: Optional[str] = None) -> None: # pragma: no cover
37173742
"""
3718-
Display an important message to the user while they are at the prompt in between commands.
3743+
Display an important message to the user while they are at a command line prompt.
37193744
To the user it appears as if an alert message is printed above the prompt and their current input
37203745
text and cursor location is left alone.
37213746
@@ -3775,10 +3800,10 @@ def async_alert(self, alert_msg: str, new_prompt: Optional[str] = None) -> None:
37753800

37763801
def async_update_prompt(self, new_prompt: str) -> None: # pragma: no cover
37773802
"""
3778-
Update the prompt while the user is still typing at it. This is good for alerting the user to system
3779-
changes dynamically in between commands. For instance you could alter the color of the prompt to indicate
3780-
a system status or increase a counter to report an event. If you do alter the actual text of the prompt,
3781-
it is best to keep the prompt the same width as what's on screen. Otherwise the user's input text will
3803+
Update the command line prompt while the user is still typing at it. This is good for alerting the user to
3804+
system changes dynamically in between commands. For instance you could alter the color of the prompt to
3805+
indicate a system status or increase a counter to report an event. If you do alter the actual text of the
3806+
prompt, it is best to keep the prompt the same width as what's on screen. Otherwise the user's input text will
37823807
be shifted and the update will not be seamless.
37833808
37843809
Raises a `RuntimeError` if called while another thread holds `terminal_lock`.
@@ -3948,7 +3973,7 @@ def cmdloop(self, intro: Optional[str] = None) -> int:
39483973
original_sigint_handler = signal.getsignal(signal.SIGINT)
39493974
signal.signal(signal.SIGINT, self.sigint_handler)
39503975

3951-
# Grab terminal lock before the prompt has been drawn by readline
3976+
# Grab terminal lock before the command line prompt has been drawn by readline
39523977
self.terminal_lock.acquire()
39533978

39543979
# Always run the preloop first

0 commit comments

Comments
 (0)