Skip to content

Commit 8f32eea

Browse files
committed
Replaced direct calls to self.stdout.write() with calls to self.poutput().
Modified implementation of poutput() to accept an optional "end" argument which can change the ending to something other than a newline or simply suppress adding a newline. Also added a try/except to catch BrokenPipeError exceptions which can occupoutputr if the subprocess output is being piped to closes before the command piping output to it is finished. Updated install docs to note that when using Python 2.7, the subprocess32 module should also be installed.
1 parent 5ef89c4 commit 8f32eea

File tree

2 files changed

+47
-23
lines changed

2 files changed

+47
-23
lines changed

cmd2.py

Lines changed: 40 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,8 @@
1717
Easy transcript-based testing of applications (see examples/example.py)
1818
Bash-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
@@ -62,6 +61,7 @@
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
6463
try:
64+
# noinspection PyPackageRequirements
6565
import subprocess32 as subprocess
6666
except 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
309310
try:
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\nnow: %s\n' % (param_name, current_val, val))
1192+
self.poutput('%s - was: %s\nnow: %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()

docs/install.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,3 +124,10 @@ If you wish to permanently uninstall ``cmd2``, this can also easily be done with
124124

125125
pip uninstall cmd2
126126

127+
Extra requirement for Python 2.7 only
128+
-------------------------------------
129+
If you want to be able to pipe the output of commands to a shell command on Python 2.7, then you will need one
130+
additional package installed:
131+
132+
* subprocess32
133+

0 commit comments

Comments
 (0)