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
6261# If using Python 2.7, try to use the subprocess32 package backported from Python 3.2 due to various improvements
6362# NOTE: The feature to pipe output to a shell command won't work correctly in Python 2.7 without this
6463try :
64+ # noinspection PyPackageRequirements
6565 import subprocess32 as subprocess
6666except ImportError :
6767 import subprocess
@@ -306,6 +306,7 @@ def new_func(instance, arg):
306306
307307
308308# Can we access the clipboard? Should always be true on Windows and Mac, but only sometimes on Linux
309+ # noinspection PyUnresolvedReferences
309310try :
310311 if six .PY3 and sys .platform .startswith ('linux' ):
311312 # Avoid extraneous output to stderr from xclip when clipboard is empty at cost of overwriting clipboard contents
@@ -559,12 +560,26 @@ def _finalize_app_parameters(self):
559560 # Make sure settable parameters are sorted alphabetically by key
560561 self .settable = collections .OrderedDict (sorted (self .settable .items (), key = lambda t : t [0 ]))
561562
562- def poutput (self , msg ):
563- """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+ """
564572 if msg :
565- self .stdout .write (msg )
566- if msg [- 1 ] != '\n ' :
567- 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
568583
569584 def perror (self , errmsg , exception_type = None , traceback_war = True ):
570585 """ Print error message to sys.stderr and if debug is true, print an exception Traceback if one exists.
@@ -776,7 +791,7 @@ def _redirect_output(self, statement):
776791 # Create a pipe with read and write sides
777792 read_fd , write_fd = os .pipe ()
778793
779- # Make sure that self.stdout.write () expects unicode strings in Python 3 and byte strings in Python 2
794+ # Make sure that self.poutput () expects unicode strings in Python 3 and byte strings in Python 2
780795 write_mode = 'w'
781796 read_mode = 'r'
782797 if six .PY2 :
@@ -789,7 +804,7 @@ def _redirect_output(self, statement):
789804 # noinspection PyTypeChecker
790805 subproc_stdin = io .open (read_fd , read_mode )
791806
792- # If you don't set shell=True, subprocess failure will throw an exception
807+ # We want Popen to raise an exception if it fails to open the process. Thus we don't set shell to True.
793808 try :
794809 self .pipe_proc = subprocess .Popen (shlex .split (statement .parsed .pipeTo ), stdin = subproc_stdin )
795810 except Exception as ex :
@@ -815,7 +830,7 @@ def _redirect_output(self, statement):
815830 else :
816831 sys .stdout = self .stdout = tempfile .TemporaryFile (mode = "w+" )
817832 if statement .parsed .output == '>>' :
818- self .stdout . write (get_paste_buffer ())
833+ self .poutput (get_paste_buffer ())
819834
820835 def _restore_output (self , statement ):
821836 """Handles restoring state after output redirection as well as the actual pipe operation if present.
@@ -830,7 +845,10 @@ def _restore_output(self, statement):
830845 write_to_paste_buffer (self .stdout .read ())
831846
832847 # Close the file or pipe that stdout was redirected to
833- self .stdout .close ()
848+ try :
849+ self .stdout .close ()
850+ except BrokenPipeError :
851+ pass
834852
835853 # If we were piping output to a shell command, then close the subprocess the shell command was running in
836854 if self .pipe_proc is not None :
@@ -943,7 +961,7 @@ def pseudo_raw_input(self, prompt):
943961 except EOFError :
944962 line = 'eof'
945963 else :
946- self .stdout . write (safe_prompt )
964+ self .poutput (safe_prompt , end = '' )
947965 self .stdout .flush ()
948966 line = self .stdin .readline ()
949967 if not len (line ):
@@ -986,7 +1004,7 @@ def _cmdloop(self):
9861004
9871005 # If echo is on and in the middle of running a script, then echo the line to the output
9881006 if self .echo and self ._current_script_dir is not None :
989- self .stdout . write (line + '\n ' )
1007+ self .poutput (line + '\n ' )
9901008
9911009 # Run the command along with all associated pre and post hooks
9921010 stop = self .onecmd_plus_hooks (line )
@@ -1007,7 +1025,7 @@ def _cmdloop(self):
10071025 # noinspection PyUnusedLocal
10081026 def do_cmdenvironment (self , args ):
10091027 """Summary report of interactive parameters."""
1010- self .stdout . write ("""
1028+ self .poutput ("""
10111029 Commands are case-sensitive: {}
10121030 Commands may be terminated with: {}
10131031 Arguments at invocation allowed: {}
@@ -1065,7 +1083,7 @@ def _help_menu(self):
10651083 cmds_doc .append (command )
10661084 else :
10671085 cmds_undoc .append (command )
1068- self .stdout . write ("%s\n " % str (self .doc_leader ))
1086+ self .poutput ("%s\n " % str (self .doc_leader ))
10691087 self .print_topics (self .doc_header , cmds_doc , 15 , 80 )
10701088 self .print_topics (self .misc_header , list (help_dict .keys ()), 15 , 80 )
10711089 self .print_topics (self .undoc_header , cmds_undoc , 15 , 80 )
@@ -1074,7 +1092,7 @@ def _help_menu(self):
10741092 def do_shortcuts (self , args ):
10751093 """Lists shortcuts (aliases) available."""
10761094 result = "\n " .join ('%s: %s' % (sc [0 ], sc [1 ]) for sc in sorted (self .shortcuts ))
1077- self .stdout . write ("Shortcuts for other commands:\n {}\n " .format (result ))
1095+ self .poutput ("Shortcuts for other commands:\n {}\n " .format (result ))
10781096
10791097 # noinspection PyUnusedLocal
10801098 def do_eof (self , arg ):
@@ -1119,9 +1137,8 @@ def select(self, opts, prompt='Your choice? '):
11191137 result = fulloptions [response - 1 ][0 ]
11201138 break
11211139 except (ValueError , IndexError ):
1122- self .stdout .write ("{!r} isn't a valid choice. Pick a number "
1123- "between 1 and {}:\n " .format (
1124- response , len (fulloptions )))
1140+ self .poutput ("{!r} isn't a valid choice. Pick a number between 1 and {}:\n " .format (response ,
1141+ len (fulloptions )))
11251142 return result
11261143
11271144 @options ([make_option ('-l' , '--long' , action = "store_true" , help = "describe function of parameter" )])
@@ -1172,7 +1189,7 @@ def do_set(self, arg):
11721189 else :
11731190 val = cast (current_val , val )
11741191 setattr (self , param_name , val )
1175- 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 ))
11761193 if current_val != val :
11771194 try :
11781195 onchange_hook = getattr (self , '_onchange_%s' % param_name )
@@ -1535,7 +1552,7 @@ def do_history(self, arg, opts):
15351552 if opts .script :
15361553 self .poutput (hi )
15371554 else :
1538- self .stdout . write (hi .pr ())
1555+ self .poutput (hi .pr ())
15391556
15401557 def _last_matching (self , arg ):
15411558 """Return the last item from the history list that matches arg. Or if arg not provided, return last item.
@@ -1839,7 +1856,7 @@ def cmdloop(self, intro=None):
18391856
18401857 # Print the intro, if there is one, right after the preloop
18411858 if self .intro is not None :
1842- self .stdout . write (str (self .intro ) + "\n " )
1859+ self .poutput (str (self .intro ) + "\n " )
18431860
18441861 # And then call _cmdloop() to enter the main loop
18451862 self ._cmdloop ()
0 commit comments