Skip to content

Commit c18b2a8

Browse files
committed
Added ppaged() method for printing output via a pager
Also: - Added paged_output.py example - Modified cmd2 so it keeps track of when output is being redirected so it doesn't attempt to usage a pager in this case
1 parent a6f0e06 commit c18b2a8

File tree

2 files changed

+87
-1
lines changed

2 files changed

+87
-1
lines changed

cmd2.py

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1133,6 +1133,9 @@ def __init__(self, completekey='tab', stdin=None, stdout=None, persistent_histor
11331133
# Used by complete() for readline tab completion
11341134
self.completion_matches = []
11351135

1136+
# Used to keep track of whether we are redirecting or piping output
1137+
self.redirecting = False
1138+
11361139
# ----- Methods related to presenting output to the user -----
11371140

11381141
@property
@@ -1154,7 +1157,7 @@ def _finalize_app_parameters(self):
11541157
# Make sure settable parameters are sorted alphabetically by key
11551158
self.settable = collections.OrderedDict(sorted(self.settable.items(), key=lambda t: t[0]))
11561159

1157-
def poutput(self, msg, end='\n'):
1160+
def poutput(self, msg, end=os.linesep):
11581161
"""Convenient shortcut for self.stdout.write(); by default adds newline to end if not already present.
11591162
11601163
Also handles BrokenPipeError exceptions for when a commands's output has been piped to another process and
@@ -1207,6 +1210,55 @@ def pfeedback(self, msg):
12071210
else:
12081211
sys.stderr.write("{}\n".format(msg))
12091212

1213+
def ppaged(self, msg, end=os.linesep):
1214+
"""Print output using a pager if it would go off screen and stdout isn't currently being redirected.
1215+
1216+
Never uses a pager inside of a script (Python or text) or when output is being redirected or piped.
1217+
1218+
:param msg: str - message to print to current stdout - anything convertible to a str with '{}'.format() is OK
1219+
:param end: str - string appended after the end of the message if not already present, default a newline
1220+
"""
1221+
if msg is not None and msg != '':
1222+
try:
1223+
msg_str = '{}'.format(msg)
1224+
if not msg_str.endswith(end):
1225+
msg_str += end
1226+
1227+
# Don't attempt to use a pager that can block if redirecting or running a script (either text or Python)
1228+
if not self.redirecting and not self._in_py and not self._script_dir:
1229+
# Here is the meaning of the various flags we are using with the less command:
1230+
# -S causes lines longer than the screen width to be chopped (truncated) rather than wrapped
1231+
# -R causes ANSI "color" escape sequences to be output in raw form (i.e. colors are displayed)
1232+
# -X disables sending the termcap initialization and deinitialization strings to the terminal
1233+
# -F causes less to automatically exit if the entire file can be displayed on the first screen
1234+
pager_cmd = 'less -SRXF'
1235+
if sys.platform.startswith('win'):
1236+
pager_cmd = 'more'
1237+
self.pipe_proc = subprocess.Popen(pager_cmd, shell=True, stdin=subprocess.PIPE)
1238+
try:
1239+
self.pipe_proc.stdin.write(msg_str.encode('utf-8', 'replace'))
1240+
self.pipe_proc.stdin.close()
1241+
except (IOError, KeyboardInterrupt):
1242+
pass
1243+
1244+
# Less doesn't respect ^C, but catches it for its own UI purposes (aborting search etc. inside less)
1245+
while True:
1246+
try:
1247+
self.pipe_proc.wait()
1248+
except KeyboardInterrupt:
1249+
pass
1250+
else:
1251+
break
1252+
self.pipe_proc = None
1253+
else:
1254+
self.stdout.write(msg_str)
1255+
except BROKEN_PIPE_ERROR:
1256+
# This occurs if a command's output is being piped to another process and that process closes before the
1257+
# command is finished. We intentionally don't print a warning message here since we know that stdout
1258+
# will be restored by the _restore_output() method. If you would like your application to print a
1259+
# warning message, then override this method.
1260+
pass
1261+
12101262
def colorize(self, val, color):
12111263
"""Given a string (``val``), returns that string wrapped in UNIX-style
12121264
special characters that turn on (and then off) text color and style.
@@ -1599,6 +1651,7 @@ def _redirect_output(self, statement):
15991651
# Open each side of the pipe and set stdout accordingly
16001652
# noinspection PyTypeChecker
16011653
self.stdout = io.open(write_fd, write_mode)
1654+
self.redirecting = True
16021655
# noinspection PyTypeChecker
16031656
subproc_stdin = io.open(read_fd, read_mode)
16041657

@@ -1612,6 +1665,7 @@ def _redirect_output(self, statement):
16121665
self.pipe_proc = None
16131666
self.kept_state.restore()
16141667
self.kept_state = None
1668+
self.redirecting = False
16151669

16161670
# Re-raise the exception
16171671
raise ex
@@ -1620,6 +1674,7 @@ def _redirect_output(self, statement):
16201674
raise EnvironmentError('Cannot redirect to paste buffer; install ``xclip`` and re-run to enable')
16211675
self.kept_state = Statekeeper(self, ('stdout',))
16221676
self.kept_sys = Statekeeper(sys, ('stdout',))
1677+
self.redirecting = True
16231678
if statement.parsed.outputTo:
16241679
mode = 'w'
16251680
if statement.parsed.output == 2 * self.redirector:
@@ -1662,6 +1717,8 @@ def _restore_output(self, statement):
16621717
self.kept_sys.restore()
16631718
self.kept_sys = None
16641719

1720+
self.redirecting = False
1721+
16651722
def _func_named(self, arg):
16661723
"""Gets the method name associated with a given command.
16671724

examples/paged_output.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#!/usr/bin/env python
2+
# coding=utf-8
3+
"""A simple example demonstrating the using paged output via the ppaged() method.
4+
"""
5+
import functools
6+
7+
import cmd2
8+
from cmd2 import with_argument_list
9+
10+
11+
class PagedOutput(cmd2.Cmd):
12+
""" Example cmd2 application where we create commands that just print the arguments they are called with."""
13+
14+
def __init__(self):
15+
cmd2.Cmd.__init__(self)
16+
17+
@with_argument_list
18+
def do_page_file(self, args):
19+
"""Read in a text file and display its output in a pager."""
20+
with open(args[0], 'r') as f:
21+
text = f.read()
22+
self.ppaged(text)
23+
24+
complete_page_file = functools.partial(cmd2.path_complete)
25+
26+
27+
if __name__ == '__main__':
28+
app = PagedOutput()
29+
app.cmdloop()

0 commit comments

Comments
 (0)