Skip to content

Commit 73535e1

Browse files
authored
Merge pull request #810 from python-cmd2/read_input
cmd2-specific input() function
2 parents 0fc04d2 + c474c4c commit 73535e1

File tree

4 files changed

+245
-221
lines changed

4 files changed

+245
-221
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## 0.9.21 (TBD, 2019)
2+
* Bug Fixes
3+
* Fixed bug where pipe processes were not being stopped by Ctrl-C on Linux/Mac
4+
* Enhancements
5+
* Added `read_input()` function that is used to read from stdin. Unlike the Python built-in `input()`, it also has
6+
an argument to disable tab completion while input is being entered.
7+
18
## 0.9.20 (November 12, 2019)
29
* Bug Fixes
310
* Fixed bug where setting `use_ipython` to False removed ipy command from the entire `cmd2.Cmd` class instead of

cmd2/cmd2.py

Lines changed: 92 additions & 68 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,59 @@ 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
1994-
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
1996+
def read_input(self, prompt: str, *, allow_completion: bool = False) -> str:
19981997
"""
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
1998+
Read input from appropriate stdin value. Also allows you to disable tab completion while input is being read.
1999+
:param prompt: prompt to display to user
2000+
:param allow_completion: if True, then tab completion of commands is enabled. This generally should be
2001+
set to False unless reading the command line. Defaults to False.
2002+
:return: the line read from stdin with all trailing new lines removed
2003+
:raises any exceptions raised by input() and stdin.readline()
2004+
"""
2005+
completion_disabled = False
2006+
orig_completer = None
2007+
2008+
def disable_completion():
2009+
"""Turn off completion while entering input"""
2010+
nonlocal orig_completer
2011+
nonlocal completion_disabled
2012+
2013+
if self._completion_supported() and not completion_disabled:
2014+
orig_completer = readline.get_completer()
2015+
readline.set_completer(lambda *args, **kwargs: None)
2016+
completion_disabled = True
2017+
2018+
def enable_completion():
2019+
"""Restore tab completion when finished entering input"""
2020+
nonlocal completion_disabled
2021+
2022+
if self._completion_supported() and completion_disabled:
2023+
readline.set_completer(orig_completer)
2024+
completion_disabled = False
20082025

2026+
# Check we are reading from sys.stdin
2027+
if self.use_rawinput:
2028+
if sys.stdin.isatty():
2029+
try:
20092030
# Deal with the vagaries of readline and ANSI escape codes
20102031
safe_prompt = rl_make_safe_prompt(prompt)
2032+
2033+
with self.sigint_protection:
2034+
# Check if tab completion should be disabled
2035+
if not allow_completion:
2036+
disable_completion()
20112037
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()
2038+
finally:
2039+
with self.sigint_protection:
2040+
# Check if we need to re-enable tab completion
2041+
if not allow_completion:
2042+
enable_completion()
2043+
else:
2044+
line = input()
2045+
if self.echo:
2046+
sys.stdout.write('{}{}\n'.format(prompt, line))
2047+
2048+
# Otherwise read from self.stdin
20222049
else:
20232050
if self.stdin.isatty():
20242051
# on a tty, print the prompt first, then read the line
@@ -2041,14 +2068,36 @@ def _pseudo_raw_input(self, prompt: str) -> str:
20412068

20422069
return line.rstrip('\r\n')
20432070

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

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

20532102
# Set up readline for our tab completion needs
20542103
if rl_type == RlType.GNU:
@@ -2080,7 +2129,7 @@ def _restore_readline(self, readline_settings: _SavedReadlineSettings):
20802129
Restore saved readline settings
20812130
:param readline_settings: the readline settings to restore
20822131
"""
2083-
if self.use_rawinput and self.completekey and rl_type != RlType.NONE:
2132+
if self._completion_supported():
20842133

20852134
# Restore what we changed in readline
20862135
readline.set_completer(readline_settings.completer)
@@ -2114,7 +2163,7 @@ def _cmdloop(self) -> None:
21142163
while not stop:
21152164
# Get commands from user
21162165
try:
2117-
line = self._pseudo_raw_input(self.prompt)
2166+
line = self._read_command_line(self.prompt)
21182167
except KeyboardInterrupt as ex:
21192168
if self.quit_on_sigint:
21202169
raise ex
@@ -2693,27 +2742,6 @@ def select(self, opts: Union[str, List[str], List[Tuple[Any, Optional[str]]]],
26932742
that the return value can differ from
26942743
the text advertised to the user """
26952744

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-
27172745
local_opts = opts
27182746
if isinstance(opts, str):
27192747
local_opts = list(zip(opts.split(), opts.split()))
@@ -2730,18 +2758,14 @@ def enable_completion():
27302758
self.poutput(' %2d. %s' % (idx + 1, text))
27312759

27322760
while True:
2733-
safe_prompt = rl_make_safe_prompt(prompt)
2734-
27352761
try:
2736-
with self.sigint_protection:
2737-
disable_completion()
2738-
response = input(safe_prompt)
2762+
response = self.read_input(prompt)
27392763
except EOFError:
27402764
response = ''
27412765
self.poutput('\n', end='')
2742-
finally:
2743-
with self.sigint_protection:
2744-
enable_completion()
2766+
except KeyboardInterrupt as ex:
2767+
self.poutput('^C')
2768+
raise ex
27452769

27462770
if not response:
27472771
continue
@@ -2921,7 +2945,7 @@ def _set_up_py_shell_env(self, interp: InteractiveConsole) -> _SavedCmd2Env:
29212945
for item in self._py_history:
29222946
readline.add_history(item)
29232947

2924-
if self.use_rawinput and self.completekey:
2948+
if self._completion_supported():
29252949
# Set up tab completion for the Python console
29262950
# rlcompleter relies on the default settings of the Python readline module
29272951
if rl_type == RlType.GNU:
@@ -2988,7 +3012,7 @@ def _restore_cmd2_env(self, cmd2_env: _SavedCmd2Env) -> None:
29883012
for item in cmd2_env.history:
29893013
readline.add_history(item)
29903014

2991-
if self.use_rawinput and self.completekey:
3015+
if self._completion_supported():
29923016
# Restore cmd2's tab completion settings
29933017
readline.set_completer(cmd2_env.readline_settings.completer)
29943018
readline.set_completer_delims(cmd2_env.readline_settings.delims)
@@ -3715,7 +3739,7 @@ class TestMyAppCase(Cmd2TestCase):
37153739

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

37763800
def async_update_prompt(self, new_prompt: str) -> None: # pragma: no cover
37773801
"""
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
3802+
Update the command line prompt while the user is still typing at it. This is good for alerting the user to
3803+
system changes dynamically in between commands. For instance you could alter the color of the prompt to
3804+
indicate a system status or increase a counter to report an event. If you do alter the actual text of the
3805+
prompt, it is best to keep the prompt the same width as what's on screen. Otherwise the user's input text will
37823806
be shifted and the update will not be seamless.
37833807
37843808
Raises a `RuntimeError` if called while another thread holds `terminal_lock`.
@@ -3948,7 +3972,7 @@ def cmdloop(self, intro: Optional[str] = None) -> int:
39483972
original_sigint_handler = signal.getsignal(signal.SIGINT)
39493973
signal.signal(signal.SIGINT, self.sigint_handler)
39503974

3951-
# Grab terminal lock before the prompt has been drawn by readline
3975+
# Grab terminal lock before the command line prompt has been drawn by readline
39523976
self.terminal_lock.acquire()
39533977

39543978
# Always run the preloop first

cmd2/utils.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -520,10 +520,15 @@ def send_sigint(self) -> None:
520520
"""Send a SIGINT to the process similar to if <Ctrl>+C were pressed."""
521521
import signal
522522
if sys.platform.startswith('win'):
523-
signal_to_send = signal.CTRL_C_EVENT
523+
self._proc.send_signal(signal.CTRL_C_EVENT)
524524
else:
525-
signal_to_send = signal.SIGINT
526-
self._proc.send_signal(signal_to_send)
525+
# Since cmd2 uses shell=True in its Popen calls, we need to send the SIGINT to
526+
# the whole process group to make sure it propagates further than the shell
527+
try:
528+
group_id = os.getpgid(self._proc.pid)
529+
os.killpg(group_id, signal.SIGINT)
530+
except ProcessLookupError:
531+
return
527532

528533
def terminate(self) -> None:
529534
"""Terminate the process"""

0 commit comments

Comments
 (0)