Skip to content

Commit 1241734

Browse files
committed
Keeping track of redirection for each command
1 parent 833089f commit 1241734

File tree

1 file changed

+82
-69
lines changed

1 file changed

+82
-69
lines changed

cmd2/cmd2.py

Lines changed: 82 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,31 @@ def cmd_wrapper(cmd2_instance, statement: Union[Statement, str]):
289289
return arg_decorator
290290

291291

292+
class Statekeeper(object):
293+
"""Class used to save and restore state during load and py commands as well as when redirecting output or pipes."""
294+
def __init__(self, obj: Any, attribs: Iterable) -> None:
295+
"""Use the instance attributes as a generic key-value store to copy instance attributes from outer object.
296+
297+
:param obj: instance of cmd2.Cmd derived class (your application instance)
298+
:param attribs: tuple of strings listing attributes of obj to save a copy of
299+
"""
300+
self.obj = obj
301+
self.attribs = attribs
302+
if self.obj:
303+
self._save()
304+
305+
def _save(self) -> None:
306+
"""Create copies of attributes from self.obj inside this Statekeeper instance."""
307+
for attrib in self.attribs:
308+
setattr(self, attrib, getattr(self.obj, attrib))
309+
310+
def restore(self) -> None:
311+
"""Overwrite attributes in self.obj with the saved values stored in this Statekeeper instance."""
312+
if self.obj:
313+
for attrib in self.attribs:
314+
setattr(self.obj, attrib, getattr(self, attrib))
315+
316+
292317
class EmbeddedConsoleExit(SystemExit):
293318
"""Custom exception class for use with the py command."""
294319
pass
@@ -302,6 +327,9 @@ class EmptyStatement(Exception):
302327
# Contains data about a disabled command which is used to restore its original functions when the command is enabled
303328
DisabledCommand = namedtuple('DisabledCommand', ['command_function', 'help_function'])
304329

330+
# Used to restore state after redirection ends
331+
RedirectionSavedState = namedtuple('RedirectionSavedState', ['self_stdout', 'sys_stdout', 'pipe_proc'])
332+
305333

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

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

@@ -1717,9 +1741,17 @@ def onecmd_plus_hooks(self, line: str) -> bool:
17171741
# we need to run the finalization hooks
17181742
raise EmptyStatement
17191743

1744+
# Keep track of whether or not we were already redirecting before this command
1745+
already_redirecting = self.redirecting
1746+
1747+
# Handle any redirection for this command
1748+
saved_state = self._redirect_output(statement)
1749+
1750+
# See if we need to update self.redirecting
1751+
if not already_redirecting:
1752+
self.redirecting = all(val is not None for val in saved_state)
1753+
17201754
try:
1721-
if self.allow_redirection:
1722-
self._redirect_output(statement)
17231755
timestart = datetime.datetime.now()
17241756
if self._in_py:
17251757
self._last_result = None
@@ -1747,8 +1779,10 @@ def onecmd_plus_hooks(self, line: str) -> bool:
17471779
if self.timing:
17481780
self.pfeedback('Elapsed: %s' % str(datetime.datetime.now() - timestart))
17491781
finally:
1750-
if self.allow_redirection and self.redirecting:
1751-
self._restore_output(statement)
1782+
self._restore_output(statement, saved_state)
1783+
if not already_redirecting:
1784+
self.redirecting = False
1785+
17521786
except EmptyStatement:
17531787
# don't do anything, but do allow command finalization hooks to run
17541788
pass
@@ -1873,48 +1907,47 @@ def _complete_statement(self, line: str) -> Statement:
18731907
raise EmptyStatement()
18741908
return statement
18751909

1876-
def _redirect_output(self, statement: Statement) -> None:
1910+
def _redirect_output(self, statement: Statement) -> RedirectionSavedState:
18771911
"""Handles output redirection for >, >>, and |.
18781912
18791913
:param statement: a parsed statement from the user
1914+
:return: A RedirectionSavedState object. All elements will be None if no redirection was done.
18801915
"""
18811916
import io
18821917
import subprocess
18831918

1884-
if statement.pipe_to:
1885-
self.kept_state = Statekeeper(self, ('stdout',))
1919+
# Default to no redirection
1920+
ret_val = RedirectionSavedState(None, None, None)
1921+
1922+
if not self.allow_redirection:
1923+
return ret_val
18861924

1925+
if statement.pipe_to:
18871926
# Create a pipe with read and write sides
18881927
read_fd, write_fd = os.pipe()
18891928

18901929
# Open each side of the pipe and set stdout accordingly
1891-
# noinspection PyTypeChecker
1892-
self.stdout = io.open(write_fd, 'w')
1893-
self.redirecting = True
1894-
# noinspection PyTypeChecker
18951930
subproc_stdin = io.open(read_fd, 'r')
1931+
new_stdout = io.open(write_fd, 'w')
18961932

18971933
# We want Popen to raise an exception if it fails to open the process. Thus we don't set shell to True.
18981934
try:
1899-
self.pipe_proc = subprocess.Popen(statement.pipe_to, stdin=subproc_stdin)
1935+
pipe_proc = subprocess.Popen(statement.pipe_to, stdin=subproc_stdin)
1936+
ret_val = RedirectionSavedState(self_stdout=self.stdout,
1937+
sys_stdout=None,
1938+
pipe_proc=self.pipe_proc)
1939+
self.stdout = new_stdout
1940+
self.pipe_proc = pipe_proc
19001941
except Exception as ex:
19011942
self.perror('Not piping because - {}'.format(ex), traceback_war=False)
1902-
1903-
# Restore stdout to what it was and close the pipe
1904-
self.stdout.close()
19051943
subproc_stdin.close()
1906-
self.pipe_proc = None
1907-
self.kept_state.restore()
1908-
self.kept_state = None
1909-
self.redirecting = False
1944+
new_stdout.close()
19101945

19111946
elif statement.output:
19121947
import tempfile
19131948
if (not statement.output_to) and (not self.can_clip):
19141949
raise EnvironmentError("Cannot redirect to paste buffer; install 'pyperclip' and re-run to enable")
1915-
self.kept_state = Statekeeper(self, ('stdout',))
1916-
self.kept_sys = Statekeeper(sys, ('stdout',))
1917-
self.redirecting = True
1950+
19181951
if statement.output_to:
19191952
# going to a file
19201953
mode = 'w'
@@ -1923,24 +1956,34 @@ def _redirect_output(self, statement: Statement) -> None:
19231956
if statement.output == constants.REDIRECTION_APPEND:
19241957
mode = 'a'
19251958
try:
1926-
sys.stdout = self.stdout = open(statement.output_to, mode)
1959+
new_stdout = open(statement.output_to, mode)
1960+
ret_val = RedirectionSavedState(self_stdout=self.stdout,
1961+
sys_stdout=sys.stdout,
1962+
pipe_proc=None)
1963+
sys.stdout = self.stdout = new_stdout
19271964
except OSError as ex:
19281965
self.perror('Not redirecting because - {}'.format(ex), traceback_war=False)
1929-
self.redirecting = False
19301966
else:
19311967
# going to a paste buffer
1932-
sys.stdout = self.stdout = tempfile.TemporaryFile(mode="w+")
1968+
new_stdout = tempfile.TemporaryFile(mode="w+")
1969+
ret_val = RedirectionSavedState(self_stdout=self.stdout,
1970+
sys_stdout=sys.stdout,
1971+
pipe_proc=None)
1972+
sys.stdout = self.stdout = new_stdout
19331973
if statement.output == constants.REDIRECTION_APPEND:
19341974
self.poutput(get_paste_buffer())
19351975

1936-
def _restore_output(self, statement: Statement) -> None:
1976+
return ret_val
1977+
1978+
def _restore_output(self, statement: Statement, saved_state: RedirectionSavedState) -> None:
19371979
"""Handles restoring state after output redirection as well as
19381980
the actual pipe operation if present.
19391981
19401982
:param statement: Statement object which contains the parsed input from the user
1983+
:param saved_state: contains information needed to restore state data
19411984
"""
1942-
# If we have redirected output to a file or the clipboard or piped it to a shell command, then restore state
1943-
if self.kept_state is not None:
1985+
# Check if self.stdout was redirected
1986+
if saved_state.self_stdout is not None:
19441987
# If we redirected output to the clipboard
19451988
if statement.output and not statement.output_to:
19461989
self.stdout.seek(0)
@@ -1952,21 +1995,16 @@ def _restore_output(self, statement: Statement) -> None:
19521995
except BrokenPipeError:
19531996
pass
19541997
finally:
1955-
# Restore self.stdout
1956-
self.kept_state.restore()
1957-
self.kept_state = None
1998+
self.stdout = saved_state.self_stdout
19581999

1959-
# If we were piping output to a shell command, then close the subprocess the shell command was running in
1960-
if self.pipe_proc is not None:
2000+
# Check if output was being piped to a process
2001+
if saved_state.pipe_proc is not None:
19612002
self.pipe_proc.communicate()
1962-
self.pipe_proc = None
1963-
1964-
# Restore sys.stdout if need be
1965-
if self.kept_sys is not None:
1966-
self.kept_sys.restore()
1967-
self.kept_sys = None
2003+
self.pipe_proc = saved_state.pipe_proc
19682004

1969-
self.redirecting = False
2005+
# Check if sys.stdout was redirected
2006+
if saved_state.sys_stdout is not None:
2007+
sys.stdout = saved_state.sys_stdout
19702008

19712009
def cmd_func(self, command: str) -> Optional[Callable]:
19722010
"""
@@ -3952,28 +3990,3 @@ def register_cmdfinalization_hook(self, func: Callable[[plugin.CommandFinalizati
39523990
"""Register a hook to be called after a command is completed, whether it completes successfully or not."""
39533991
self._validate_cmdfinalization_callable(func)
39543992
self._cmdfinalization_hooks.append(func)
3955-
3956-
3957-
class Statekeeper(object):
3958-
"""Class used to save and restore state during load and py commands as well as when redirecting output or pipes."""
3959-
def __init__(self, obj: Any, attribs: Iterable) -> None:
3960-
"""Use the instance attributes as a generic key-value store to copy instance attributes from outer object.
3961-
3962-
:param obj: instance of cmd2.Cmd derived class (your application instance)
3963-
:param attribs: tuple of strings listing attributes of obj to save a copy of
3964-
"""
3965-
self.obj = obj
3966-
self.attribs = attribs
3967-
if self.obj:
3968-
self._save()
3969-
3970-
def _save(self) -> None:
3971-
"""Create copies of attributes from self.obj inside this Statekeeper instance."""
3972-
for attrib in self.attribs:
3973-
setattr(self, attrib, getattr(self.obj, attrib))
3974-
3975-
def restore(self) -> None:
3976-
"""Overwrite attributes in self.obj with the saved values stored in this Statekeeper instance."""
3977-
if self.obj:
3978-
for attrib in self.attribs:
3979-
setattr(self.obj, attrib, getattr(self, attrib))

0 commit comments

Comments
 (0)