@@ -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
303303DisabledCommand = 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
306312class 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
@@ -3982,28 +3985,3 @@ def register_cmdfinalization_hook(self, func: Callable[[plugin.CommandFinalizati
39823985 """Register a hook to be called after a command is completed, whether it completes successfully or not."""
39833986 self ._validate_cmdfinalization_callable (func )
39843987 self ._cmdfinalization_hooks .append (func )
3985-
3986-
3987- class Statekeeper (object ):
3988- """Class used to save and restore state during load and py commands as well as when redirecting output or pipes."""
3989- def __init__ (self , obj : Any , attribs : Iterable ) -> None :
3990- """Use the instance attributes as a generic key-value store to copy instance attributes from outer object.
3991-
3992- :param obj: instance of cmd2.Cmd derived class (your application instance)
3993- :param attribs: tuple of strings listing attributes of obj to save a copy of
3994- """
3995- self .obj = obj
3996- self .attribs = attribs
3997- if self .obj :
3998- self ._save ()
3999-
4000- def _save (self ) -> None :
4001- """Create copies of attributes from self.obj inside this Statekeeper instance."""
4002- for attrib in self .attribs :
4003- setattr (self , attrib , getattr (self .obj , attrib ))
4004-
4005- def restore (self ) -> None :
4006- """Overwrite attributes in self.obj with the saved values stored in this Statekeeper instance."""
4007- if self .obj :
4008- for attrib in self .attribs :
4009- setattr (self .obj , attrib , getattr (self , attrib ))
0 commit comments