Skip to content

Commit e89bd4f

Browse files
authored
Merge pull request #653 from python-cmd2/pyscript_fix
Pyscript fix
2 parents 57dd827 + a4ff5ed commit e89bd4f

File tree

4 files changed

+95
-116
lines changed

4 files changed

+95
-116
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
## 0.9.12 (March TBD, 2019)
2+
* Bug Fixes
3+
* Fixed a bug in how redirection and piping worked inside ``py`` or ``pyscript`` commands
24
* Enhancements
35
* Added ability to include command name placeholders in the message printed when trying to run a disabled command.
46
* See docstring for ``disable_command()`` or ``disable_category()`` for more details.

cmd2/cmd2.py

Lines changed: 91 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,12 @@ class EmptyStatement(Exception):
302302
# Contains data about a disabled command which is used to restore its original functions when the command is enabled
303303
DisabledCommand = namedtuple('DisabledCommand', ['command_function', 'help_function'])
304304

305+
# Used to restore state after redirection ends
306+
# redirecting and piping are used to know what needs to be restored
307+
RedirectionSavedState = utils.namedtuple_with_defaults('RedirectionSavedState',
308+
['redirecting', 'self_stdout', 'sys_stdout',
309+
'piping', 'pipe_proc'])
310+
305311

306312
class Cmd(cmd.Cmd):
307313
"""An easy but powerful framework for writing line-oriented command interpreters.
@@ -412,10 +418,6 @@ def __init__(self, completekey: str = 'tab', stdin=None, stdout=None, persistent
412418
# Built-in commands don't make use of this. It is purely there for user-defined commands and convenience.
413419
self._last_result = None
414420

415-
# Used to save state during a redirection
416-
self.kept_state = None
417-
self.kept_sys = None
418-
419421
# Codes used for exit conditions
420422
self._STOP_AND_EXIT = True # cmd convention
421423

@@ -1717,9 +1719,17 @@ def onecmd_plus_hooks(self, line: str) -> bool:
17171719
# we need to run the finalization hooks
17181720
raise EmptyStatement
17191721

1722+
# Keep track of whether or not we were already redirecting before this command
1723+
already_redirecting = self.redirecting
1724+
1725+
# Handle any redirection for this command
1726+
saved_state = self._redirect_output(statement)
1727+
1728+
# See if we need to update self.redirecting
1729+
if not already_redirecting:
1730+
self.redirecting = saved_state.redirecting or saved_state.piping
1731+
17201732
try:
1721-
if self.allow_redirection:
1722-
self._redirect_output(statement)
17231733
timestart = datetime.datetime.now()
17241734
if self._in_py:
17251735
self._last_result = None
@@ -1747,8 +1757,10 @@ def onecmd_plus_hooks(self, line: str) -> bool:
17471757
if self.timing:
17481758
self.pfeedback('Elapsed: %s' % str(datetime.datetime.now() - timestart))
17491759
finally:
1750-
if self.allow_redirection and self.redirecting:
1751-
self._restore_output(statement)
1760+
self._restore_output(statement, saved_state)
1761+
if not already_redirecting:
1762+
self.redirecting = False
1763+
17521764
except EmptyStatement:
17531765
# don't do anything, but do allow command finalization hooks to run
17541766
pass
@@ -1848,29 +1860,9 @@ def _complete_statement(self, line: str) -> Statement:
18481860
# if we get here we must have:
18491861
# - a multiline command with no terminator
18501862
# - a multiline command with unclosed quotation marks
1851-
if not self.quit_on_sigint:
1852-
try:
1853-
self.at_continuation_prompt = True
1854-
newline = self.pseudo_raw_input(self.continuation_prompt)
1855-
if newline == 'eof':
1856-
# they entered either a blank line, or we hit an EOF
1857-
# for some other reason. Turn the literal 'eof'
1858-
# into a blank line, which serves as a command
1859-
# terminator
1860-
newline = '\n'
1861-
self.poutput(newline)
1862-
line = '{}\n{}'.format(statement.raw, newline)
1863-
except KeyboardInterrupt:
1864-
self.poutput('^C')
1865-
statement = self.statement_parser.parse('')
1866-
break
1867-
finally:
1868-
self.at_continuation_prompt = False
1869-
else:
1863+
try:
18701864
self.at_continuation_prompt = True
18711865
newline = self.pseudo_raw_input(self.continuation_prompt)
1872-
self.at_continuation_prompt = False
1873-
18741866
if newline == 'eof':
18751867
# they entered either a blank line, or we hit an EOF
18761868
# for some other reason. Turn the literal 'eof'
@@ -1879,53 +1871,59 @@ def _complete_statement(self, line: str) -> Statement:
18791871
newline = '\n'
18801872
self.poutput(newline)
18811873
line = '{}\n{}'.format(statement.raw, newline)
1874+
except KeyboardInterrupt as ex:
1875+
if self.quit_on_sigint:
1876+
raise ex
1877+
else:
1878+
self.poutput('^C')
1879+
statement = self.statement_parser.parse('')
1880+
break
1881+
finally:
1882+
self.at_continuation_prompt = False
18821883

18831884
if not statement.command:
18841885
raise EmptyStatement()
18851886
return statement
18861887

1887-
def _redirect_output(self, statement: Statement) -> None:
1888+
def _redirect_output(self, statement: Statement) -> RedirectionSavedState:
18881889
"""Handles output redirection for >, >>, and |.
18891890
18901891
:param statement: a parsed statement from the user
1892+
:return: A RedirectionSavedState object
18911893
"""
18921894
import io
18931895
import subprocess
18941896

1895-
if statement.pipe_to:
1896-
self.kept_state = Statekeeper(self, ('stdout',))
1897+
ret_val = RedirectionSavedState(redirecting=False, piping=False)
1898+
1899+
if not self.allow_redirection:
1900+
return ret_val
18971901

1902+
if statement.pipe_to:
18981903
# Create a pipe with read and write sides
18991904
read_fd, write_fd = os.pipe()
19001905

19011906
# Open each side of the pipe and set stdout accordingly
1902-
# noinspection PyTypeChecker
1903-
self.stdout = io.open(write_fd, 'w')
1904-
self.redirecting = True
1905-
# noinspection PyTypeChecker
1906-
subproc_stdin = io.open(read_fd, 'r')
1907+
pipe_read = io.open(read_fd, 'r')
1908+
pipe_write = io.open(write_fd, 'w')
19071909

19081910
# We want Popen to raise an exception if it fails to open the process. Thus we don't set shell to True.
19091911
try:
1910-
self.pipe_proc = subprocess.Popen(statement.pipe_to, stdin=subproc_stdin)
1912+
pipe_proc = subprocess.Popen(statement.pipe_to, stdin=pipe_read, stdout=self.stdout)
1913+
ret_val = RedirectionSavedState(redirecting=True, self_stdout=self.stdout,
1914+
piping=True, pipe_proc=self.pipe_proc)
1915+
self.stdout = pipe_write
1916+
self.pipe_proc = pipe_proc
19111917
except Exception as ex:
19121918
self.perror('Not piping because - {}'.format(ex), traceback_war=False)
1913-
1914-
# Restore stdout to what it was and close the pipe
1915-
self.stdout.close()
1916-
subproc_stdin.close()
1917-
self.pipe_proc = None
1918-
self.kept_state.restore()
1919-
self.kept_state = None
1920-
self.redirecting = False
1919+
pipe_read.close()
1920+
pipe_write.close()
19211921

19221922
elif statement.output:
19231923
import tempfile
19241924
if (not statement.output_to) and (not self.can_clip):
19251925
raise EnvironmentError("Cannot redirect to paste buffer; install 'pyperclip' and re-run to enable")
1926-
self.kept_state = Statekeeper(self, ('stdout',))
1927-
self.kept_sys = Statekeeper(sys, ('stdout',))
1928-
self.redirecting = True
1926+
19291927
if statement.output_to:
19301928
# going to a file
19311929
mode = 'w'
@@ -1934,24 +1932,30 @@ def _redirect_output(self, statement: Statement) -> None:
19341932
if statement.output == constants.REDIRECTION_APPEND:
19351933
mode = 'a'
19361934
try:
1937-
sys.stdout = self.stdout = open(statement.output_to, mode)
1935+
new_stdout = open(statement.output_to, mode)
1936+
ret_val = RedirectionSavedState(redirecting=True, self_stdout=self.stdout, sys_stdout=sys.stdout)
1937+
sys.stdout = self.stdout = new_stdout
19381938
except OSError as ex:
19391939
self.perror('Not redirecting because - {}'.format(ex), traceback_war=False)
1940-
self.redirecting = False
19411940
else:
19421941
# going to a paste buffer
1943-
sys.stdout = self.stdout = tempfile.TemporaryFile(mode="w+")
1942+
new_stdout = tempfile.TemporaryFile(mode="w+")
1943+
ret_val = RedirectionSavedState(redirecting=True, self_stdout=self.stdout, sys_stdout=sys.stdout)
1944+
sys.stdout = self.stdout = new_stdout
19441945
if statement.output == constants.REDIRECTION_APPEND:
19451946
self.poutput(get_paste_buffer())
19461947

1947-
def _restore_output(self, statement: Statement) -> None:
1948+
return ret_val
1949+
1950+
def _restore_output(self, statement: Statement, saved_state: RedirectionSavedState) -> None:
19481951
"""Handles restoring state after output redirection as well as
19491952
the actual pipe operation if present.
19501953
19511954
:param statement: Statement object which contains the parsed input from the user
1955+
:param saved_state: contains information needed to restore state data
19521956
"""
1953-
# If we have redirected output to a file or the clipboard or piped it to a shell command, then restore state
1954-
if self.kept_state is not None:
1957+
# Check if self.stdout was redirected
1958+
if saved_state.redirecting:
19551959
# If we redirected output to the clipboard
19561960
if statement.output and not statement.output_to:
19571961
self.stdout.seek(0)
@@ -1963,21 +1967,16 @@ def _restore_output(self, statement: Statement) -> None:
19631967
except BrokenPipeError:
19641968
pass
19651969
finally:
1966-
# Restore self.stdout
1967-
self.kept_state.restore()
1968-
self.kept_state = None
1970+
self.stdout = saved_state.self_stdout
19691971

1970-
# If we were piping output to a shell command, then close the subprocess the shell command was running in
1971-
if self.pipe_proc is not None:
1972-
self.pipe_proc.communicate()
1973-
self.pipe_proc = None
1972+
# Check if sys.stdout was redirected
1973+
if saved_state.sys_stdout is not None:
1974+
sys.stdout = saved_state.sys_stdout
19741975

1975-
# Restore sys.stdout if need be
1976-
if self.kept_sys is not None:
1977-
self.kept_sys.restore()
1978-
self.kept_sys = None
1979-
1980-
self.redirecting = False
1976+
# Check if output was being piped to a process
1977+
if saved_state.piping:
1978+
self.pipe_proc.communicate()
1979+
self.pipe_proc = saved_state.pipe_proc
19811980

19821981
def cmd_func(self, command: str) -> Optional[Callable]:
19831982
"""
@@ -2159,10 +2158,10 @@ def _cmdloop(self) -> bool:
21592158
# Set GNU readline's rl_basic_quote_characters to NULL so it won't automatically add a closing quote
21602159
# We don't need to worry about setting rl_completion_suppress_quote since we never declared
21612160
# rl_completer_quote_characters.
2162-
old_basic_quotes = ctypes.cast(rl_basic_quote_characters, ctypes.c_void_p).value
2161+
saved_basic_quotes = ctypes.cast(rl_basic_quote_characters, ctypes.c_void_p).value
21632162
rl_basic_quote_characters.value = None
21642163

2165-
old_completer = readline.get_completer()
2164+
saved_completer = readline.get_completer()
21662165
readline.set_completer(self.complete)
21672166

21682167
# Break words on whitespace and quotes when tab completing
@@ -2172,7 +2171,7 @@ def _cmdloop(self) -> bool:
21722171
# If redirection is allowed, then break words on those characters too
21732172
completer_delims += ''.join(constants.REDIRECTION_CHARS)
21742173

2175-
old_delims = readline.get_completer_delims()
2174+
saved_delims = readline.get_completer_delims()
21762175
readline.set_completer_delims(completer_delims)
21772176

21782177
# Enable tab completion
@@ -2189,27 +2188,27 @@ def _cmdloop(self) -> bool:
21892188
self.poutput('{}{}'.format(self.prompt, line))
21902189
else:
21912190
# Otherwise, read a command from stdin
2192-
if not self.quit_on_sigint:
2193-
try:
2194-
line = self.pseudo_raw_input(self.prompt)
2195-
except KeyboardInterrupt:
2191+
try:
2192+
line = self.pseudo_raw_input(self.prompt)
2193+
except KeyboardInterrupt as ex:
2194+
if self.quit_on_sigint:
2195+
raise ex
2196+
else:
21962197
self.poutput('^C')
21972198
line = ''
2198-
else:
2199-
line = self.pseudo_raw_input(self.prompt)
22002199

22012200
# Run the command along with all associated pre and post hooks
22022201
stop = self.onecmd_plus_hooks(line)
22032202
finally:
22042203
if self.use_rawinput and self.completekey and rl_type != RlType.NONE:
22052204

22062205
# Restore what we changed in readline
2207-
readline.set_completer(old_completer)
2208-
readline.set_completer_delims(old_delims)
2206+
readline.set_completer(saved_completer)
2207+
readline.set_completer_delims(saved_delims)
22092208

22102209
if rl_type == RlType.GNU:
22112210
readline.set_completion_display_matches_hook(None)
2212-
rl_basic_quote_characters.value = old_basic_quotes
2211+
rl_basic_quote_characters.value = saved_basic_quotes
22132212
elif rl_type == RlType.PYREADLINE:
22142213
# noinspection PyUnresolvedReferences
22152214
readline.rl.mode._display_completions = orig_pyreadline_display
@@ -3070,7 +3069,7 @@ def py_quit():
30703069
# Set up tab completion for the Python console
30713070
# rlcompleter relies on the default settings of the Python readline module
30723071
if rl_type == RlType.GNU:
3073-
old_basic_quotes = ctypes.cast(rl_basic_quote_characters, ctypes.c_void_p).value
3072+
saved_basic_quotes = ctypes.cast(rl_basic_quote_characters, ctypes.c_void_p).value
30743073
rl_basic_quote_characters.value = orig_rl_basic_quotes
30753074

30763075
if 'gnureadline' in sys.modules:
@@ -3082,7 +3081,7 @@ def py_quit():
30823081

30833082
sys.modules['readline'] = sys.modules['gnureadline']
30843083

3085-
old_delims = readline.get_completer_delims()
3084+
saved_delims = readline.get_completer_delims()
30863085
readline.set_completer_delims(orig_rl_delims)
30873086

30883087
# rlcompleter will not need cmd2's custom display function
@@ -3095,15 +3094,18 @@ def py_quit():
30953094

30963095
# Save off the current completer and set a new one in the Python console
30973096
# Make sure it tab completes from its locals() dictionary
3098-
old_completer = readline.get_completer()
3097+
saved_completer = readline.get_completer()
30993098
interp.runcode("from rlcompleter import Completer")
31003099
interp.runcode("import readline")
31013100
interp.runcode("readline.set_completer(Completer(locals()).complete)")
31023101

31033102
# Set up sys module for the Python console
31043103
self._reset_py_display()
3105-
keepstate = Statekeeper(sys, ('stdin', 'stdout'))
3104+
3105+
saved_sys_stdout = sys.stdout
31063106
sys.stdout = self.stdout
3107+
3108+
saved_sys_stdin = sys.stdin
31073109
sys.stdin = self.stdin
31083110

31093111
cprt = 'Type "help", "copyright", "credits" or "license" for more information.'
@@ -3121,7 +3123,8 @@ def py_quit():
31213123
pass
31223124

31233125
finally:
3124-
keepstate.restore()
3126+
sys.stdout = saved_sys_stdout
3127+
sys.stdin = saved_sys_stdin
31253128

31263129
# Set up readline for cmd2
31273130
if rl_type != RlType.NONE:
@@ -3139,11 +3142,11 @@ def py_quit():
31393142

31403143
if self.use_rawinput and self.completekey:
31413144
# Restore cmd2's tab completion settings
3142-
readline.set_completer(old_completer)
3143-
readline.set_completer_delims(old_delims)
3145+
readline.set_completer(saved_completer)
3146+
readline.set_completer_delims(saved_delims)
31443147

31453148
if rl_type == RlType.GNU:
3146-
rl_basic_quote_characters.value = old_basic_quotes
3149+
rl_basic_quote_characters.value = saved_basic_quotes
31473150

31483151
if 'gnureadline' in sys.modules:
31493152
# Restore what the readline module pointed to
@@ -3963,28 +3966,3 @@ def register_cmdfinalization_hook(self, func: Callable[[plugin.CommandFinalizati
39633966
"""Register a hook to be called after a command is completed, whether it completes successfully or not."""
39643967
self._validate_cmdfinalization_callable(func)
39653968
self._cmdfinalization_hooks.append(func)
3966-
3967-
3968-
class Statekeeper(object):
3969-
"""Class used to save and restore state during load and py commands as well as when redirecting output or pipes."""
3970-
def __init__(self, obj: Any, attribs: Iterable) -> None:
3971-
"""Use the instance attributes as a generic key-value store to copy instance attributes from outer object.
3972-
3973-
:param obj: instance of cmd2.Cmd derived class (your application instance)
3974-
:param attribs: tuple of strings listing attributes of obj to save a copy of
3975-
"""
3976-
self.obj = obj
3977-
self.attribs = attribs
3978-
if self.obj:
3979-
self._save()
3980-
3981-
def _save(self) -> None:
3982-
"""Create copies of attributes from self.obj inside this Statekeeper instance."""
3983-
for attrib in self.attribs:
3984-
setattr(self, attrib, getattr(self.obj, attrib))
3985-
3986-
def restore(self) -> None:
3987-
"""Overwrite attributes in self.obj with the saved values stored in this Statekeeper instance."""
3988-
if self.obj:
3989-
for attrib in self.attribs:
3990-
setattr(self.obj, attrib, getattr(self, attrib))

0 commit comments

Comments
 (0)