@@ -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+
292317class 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
303328DisabledCommand = 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
306334class 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