1717Easy transcript-based testing of applications (see examples/example.py)
1818Bash-style ``select`` available
1919
20- Note that redirection with > and | will only work if `self.stdout.write()`
21- is used in place of `print`. The standard library's `cmd` module is
22- written to use `self.stdout.write()`,
20+ Note that redirection with > and | will only work if `self.poutput()`
21+ is used in place of `print`.
2322
2423- Catherine Devlin, Jan 03 2008 - catherinedevlin.blogspot.com
2524
3029import collections
3130import datetime
3231import glob
32+ import io
3333import optparse
3434import os
3535import platform
3636import re
3737import shlex
3838import six
39- import subprocess
4039import sys
4140import tempfile
4241import traceback
5958# itertools.zip() for Python 2 or zip() for Python 3 - produces an iterator in both cases
6059from six .moves import zip
6160
61+ # If using Python 2.7, try to use the subprocess32 package backported from Python 3.2 due to various improvements
62+ # NOTE: The feature to pipe output to a shell command won't work correctly in Python 2.7 without this
63+ try :
64+ # noinspection PyPackageRequirements
65+ import subprocess32 as subprocess
66+ except ImportError :
67+ import subprocess
68+
6269# Detect whether IPython is installed to determine if the built-in "ipy" command should be included
6370ipython_available = True
6471try :
@@ -299,6 +306,7 @@ def new_func(instance, arg):
299306
300307
301308# Can we access the clipboard? Should always be true on Windows and Mac, but only sometimes on Linux
309+ # noinspection PyUnresolvedReferences
302310try :
303311 if six .PY3 and sys .platform .startswith ('linux' ):
304312 # Avoid extraneous output to stderr from xclip when clipboard is empty at cost of overwriting clipboard contents
@@ -513,9 +521,6 @@ def __init__(self, completekey='tab', stdin=None, stdout=None, use_ipython=False
513521 self .kept_state = None
514522 self .kept_sys = None
515523
516- # Used for a temp file during a pipe (needed tempfile instead of real pipe for Python 3.x prior to 3.5)
517- self ._temp_filename = None
518-
519524 # Codes used for exit conditions
520525 self ._STOP_AND_EXIT = True # cmd convention
521526
@@ -531,6 +536,9 @@ def __init__(self, completekey='tab', stdin=None, stdout=None, use_ipython=False
531536 # Used load command to store the current script dir as a LIFO queue to support _relative_load command
532537 self ._script_dir = []
533538
539+ # Used when piping command output to a shell command
540+ self .pipe_proc = None
541+
534542 # ----- Methods related to presenting output to the user -----
535543
536544 @property
@@ -552,12 +560,26 @@ def _finalize_app_parameters(self):
552560 # Make sure settable parameters are sorted alphabetically by key
553561 self .settable = collections .OrderedDict (sorted (self .settable .items (), key = lambda t : t [0 ]))
554562
555- def poutput (self , msg ):
556- """Convenient shortcut for self.stdout.write(); adds newline if necessary."""
563+ def poutput (self , msg , end = '\n ' ):
564+ """Convenient shortcut for self.stdout.write(); by default adds newline to end if not already present.
565+
566+ Also handles BrokenPipeError exceptions for when a commands's output has been piped to another process and
567+ that process terminates before than command is finished executing.
568+
569+ :param msg: str - message to print to current stdout
570+ :param end: str - string appended after the end of the message, default a newline
571+ """
557572 if msg :
558- self .stdout .write (msg )
559- if msg [- 1 ] != '\n ' :
560- self .stdout .write ('\n ' )
573+ try :
574+ self .stdout .write (msg )
575+ if not msg .endswith (end ):
576+ self .stdout .write (end )
577+ except BrokenPipeError :
578+ # This occurs if a command's output is being piped to another process and that process closes before the
579+ # command is finished. We intentionally don't print a warning message here since we know that stdout
580+ # will be restored by the _restore_output() method. If you would like your application to print a
581+ # warning message, then override this method.
582+ pass
561583
562584 def perror (self , errmsg , exception_type = None , traceback_war = True ):
563585 """ Print error message to sys.stderr and if debug is true, print an exception Traceback if one exists.
@@ -765,18 +787,36 @@ def _redirect_output(self, statement):
765787 """
766788 if statement .parsed .pipeTo :
767789 self .kept_state = Statekeeper (self , ('stdout' ,))
768- self .kept_sys = Statekeeper (sys , ('stdout' ,))
769- sys .stdout = self .stdout
770790
771- # NOTE: We couldn't get a real pipe working via subprocess for Python 3.x prior to 3.5.
772- # So to allow compatibility with Python 2.7 and 3.3+ we are redirecting output to a temporary file.
773- # And once command is complete we are the temp file as stdin for the shell command to pipe to.
774- # TODO: Once support for Python 3.x prior to 3.5 is no longer necessary, replace with a real subprocess pipe
791+ # Create a pipe with read and write sides
792+ read_fd , write_fd = os .pipe ()
775793
776- # Redirect stdout to a temporary file
777- fd , self ._temp_filename = tempfile .mkstemp ()
778- os .close (fd )
779- self .stdout = open (self ._temp_filename , 'w' )
794+ # Make sure that self.poutput() expects unicode strings in Python 3 and byte strings in Python 2
795+ write_mode = 'w'
796+ read_mode = 'r'
797+ if six .PY2 :
798+ write_mode = 'wb'
799+ read_mode = 'rb'
800+
801+ # Open each side of the pipe and set stdout accordingly
802+ # noinspection PyTypeChecker
803+ self .stdout = io .open (write_fd , write_mode )
804+ # noinspection PyTypeChecker
805+ subproc_stdin = io .open (read_fd , read_mode )
806+
807+ # We want Popen to raise an exception if it fails to open the process. Thus we don't set shell to True.
808+ try :
809+ self .pipe_proc = subprocess .Popen (shlex .split (statement .parsed .pipeTo ), stdin = subproc_stdin )
810+ except Exception as ex :
811+ # Restore stdout to what it was and close the pipe
812+ self .stdout .close ()
813+ subproc_stdin .close ()
814+ self .pipe_proc = None
815+ self .kept_state .restore ()
816+ self .kept_state = None
817+
818+ # Re-raise the exception
819+ raise ex
780820 elif statement .parsed .output :
781821 if (not statement .parsed .outputTo ) and (not can_clip ):
782822 raise EnvironmentError ('Cannot redirect to paste buffer; install ``xclip`` and re-run to enable' )
@@ -790,39 +830,39 @@ def _redirect_output(self, statement):
790830 else :
791831 sys .stdout = self .stdout = tempfile .TemporaryFile (mode = "w+" )
792832 if statement .parsed .output == '>>' :
793- self .stdout . write (get_paste_buffer ())
833+ self .poutput (get_paste_buffer ())
794834
795835 def _restore_output (self , statement ):
796836 """Handles restoring state after output redirection as well as the actual pipe operation if present.
797837
798838 :param statement: ParsedString - subclass of str which also contains pyparsing ParseResults instance
799839 """
800- if self .kept_state :
840+ # If we have redirected output to a file or the clipboard or piped it to a shell command, then restore state
841+ if self .kept_state is not None :
842+ # If we redirected output to the clipboard
843+ if statement .parsed .output and not statement .parsed .outputTo :
844+ self .stdout .seek (0 )
845+ write_to_paste_buffer (self .stdout .read ())
846+
847+ # Close the file or pipe that stdout was redirected to
801848 try :
802- if statement .parsed .output :
803- if not statement .parsed .outputTo :
804- self .stdout .seek (0 )
805- write_to_paste_buffer (self .stdout .read ())
806- finally :
807849 self .stdout .close ()
808- self .kept_state .restore ()
809- self .kept_sys .restore ()
810- self .kept_state = None
850+ except BrokenPipeError :
851+ pass
811852
812- if statement .parsed .pipeTo :
813- # Pipe the contents of tempfile to the specified shell command
814- with open (self ._temp_filename ) as fd :
815- pipe_proc = subprocess .Popen (shlex .split (statement .parsed .pipeTo ), stdin = fd ,
816- stdout = subprocess .PIPE )
817- output , _ = pipe_proc .communicate ()
853+ # If we were piping output to a shell command, then close the subprocess the shell command was running in
854+ if self .pipe_proc is not None :
855+ self .pipe_proc .communicate ()
856+ self .pipe_proc = None
818857
819- if six .PY3 :
820- self .stdout .write (output .decode ())
821- else :
822- self .stdout .write (output )
858+ # Restore self.stdout
859+ self .kept_state .restore ()
860+ self .kept_state = None
823861
824- os .remove (self ._temp_filename )
825- self ._temp_filename = None
862+ # Restore sys.stdout if need be
863+ if self .kept_sys is not None :
864+ self .kept_sys .restore ()
865+ self .kept_sys = None
826866
827867 def _func_named (self , arg ):
828868 """Gets the method name associated with a given command.
@@ -921,7 +961,7 @@ def pseudo_raw_input(self, prompt):
921961 except EOFError :
922962 line = 'eof'
923963 else :
924- self .stdout . write (safe_prompt )
964+ self .poutput (safe_prompt , end = '' )
925965 self .stdout .flush ()
926966 line = self .stdin .readline ()
927967 if not len (line ):
@@ -964,7 +1004,7 @@ def _cmdloop(self):
9641004
9651005 # If echo is on and in the middle of running a script, then echo the line to the output
9661006 if self .echo and self ._current_script_dir is not None :
967- self .stdout . write (line + '\n ' )
1007+ self .poutput (line + '\n ' )
9681008
9691009 # Run the command along with all associated pre and post hooks
9701010 stop = self .onecmd_plus_hooks (line )
@@ -985,7 +1025,7 @@ def _cmdloop(self):
9851025 # noinspection PyUnusedLocal
9861026 def do_cmdenvironment (self , args ):
9871027 """Summary report of interactive parameters."""
988- self .stdout . write ("""
1028+ self .poutput ("""
9891029 Commands are case-sensitive: {}
9901030 Commands may be terminated with: {}
9911031 Arguments at invocation allowed: {}
@@ -1043,7 +1083,7 @@ def _help_menu(self):
10431083 cmds_doc .append (command )
10441084 else :
10451085 cmds_undoc .append (command )
1046- self .stdout . write ("%s\n " % str (self .doc_leader ))
1086+ self .poutput ("%s\n " % str (self .doc_leader ))
10471087 self .print_topics (self .doc_header , cmds_doc , 15 , 80 )
10481088 self .print_topics (self .misc_header , list (help_dict .keys ()), 15 , 80 )
10491089 self .print_topics (self .undoc_header , cmds_undoc , 15 , 80 )
@@ -1052,7 +1092,7 @@ def _help_menu(self):
10521092 def do_shortcuts (self , args ):
10531093 """Lists shortcuts (aliases) available."""
10541094 result = "\n " .join ('%s: %s' % (sc [0 ], sc [1 ]) for sc in sorted (self .shortcuts ))
1055- self .stdout . write ("Shortcuts for other commands:\n {}\n " .format (result ))
1095+ self .poutput ("Shortcuts for other commands:\n {}\n " .format (result ))
10561096
10571097 # noinspection PyUnusedLocal
10581098 def do_eof (self , arg ):
@@ -1097,9 +1137,8 @@ def select(self, opts, prompt='Your choice? '):
10971137 result = fulloptions [response - 1 ][0 ]
10981138 break
10991139 except (ValueError , IndexError ):
1100- self .stdout .write ("{!r} isn't a valid choice. Pick a number "
1101- "between 1 and {}:\n " .format (
1102- response , len (fulloptions )))
1140+ self .poutput ("{!r} isn't a valid choice. Pick a number between 1 and {}:\n " .format (response ,
1141+ len (fulloptions )))
11031142 return result
11041143
11051144 @options ([make_option ('-l' , '--long' , action = "store_true" , help = "describe function of parameter" )])
@@ -1150,7 +1189,7 @@ def do_set(self, arg):
11501189 else :
11511190 val = cast (current_val , val )
11521191 setattr (self , param_name , val )
1153- self .stdout . write ('%s - was: %s\n now: %s\n ' % (param_name , current_val , val ))
1192+ self .poutput ('%s - was: %s\n now: %s\n ' % (param_name , current_val , val ))
11541193 if current_val != val :
11551194 try :
11561195 onchange_hook = getattr (self , '_onchange_%s' % param_name )
@@ -1513,7 +1552,7 @@ def do_history(self, arg, opts):
15131552 if opts .script :
15141553 self .poutput (hi )
15151554 else :
1516- self .stdout . write (hi .pr ())
1555+ self .poutput (hi .pr ())
15171556
15181557 def _last_matching (self , arg ):
15191558 """Return the last item from the history list that matches arg. Or if arg not provided, return last item.
@@ -1817,7 +1856,7 @@ def cmdloop(self, intro=None):
18171856
18181857 # Print the intro, if there is one, right after the preloop
18191858 if self .intro is not None :
1820- self .stdout . write (str (self .intro ) + "\n " )
1859+ self .poutput (str (self .intro ) + "\n " )
18211860
18221861 # And then call _cmdloop() to enter the main loop
18231862 self ._cmdloop ()
0 commit comments