Skip to content

Commit 2af4473

Browse files
authored
Merge pull request #297 from python-cmd2/paged_output
Added support for paged output
2 parents a6f0e06 + 0979d63 commit 2af4473

File tree

5 files changed

+102
-7
lines changed

5 files changed

+102
-7
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
* ``exclude_from_help`` and ``excludeFromHistory`` are now instance instead of class attributes
1717
* Added flag and index based tab completion helper functions
1818
* See [tab_completion.py](https://github.com/python-cmd2/cmd2/blob/master/examples/tab_completion.py)
19+
* Added support for displaying output which won't fit on the screen via a pager using ``ppaged()``
20+
* See [paged_output.py](https://github.com/python-cmd2/cmd2/blob/master/examples/paged_output.py)
1921
* Attributes Removed (**can cause breaking changes**)
2022
* ``abbrev`` - Removed support for abbreviated commands
2123
* Good tab completion makes this unnecessary and its presence could cause harmful unintended actions

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ Main Features
2626
- Redirect command output to file with `>`, `>>`; input from file with `<`
2727
- Bare `>`, `>>` with no filename send output to paste buffer (clipboard)
2828
- `py` enters interactive Python console (opt-in `ipy` for IPython console)
29+
- Option to display long output using a pager with ``cmd2.Cmd.ppaged()``
2930
- Multi-line commands
3031
- Special-character command shortcuts (beyond cmd's `@` and `!`)
3132
- Settable environment parameters

cmd2.py

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1133,6 +1133,12 @@ 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+
1139+
# If this string is non-empty, then this warning message will print if a broken pipe error occurs while printing
1140+
self.broken_pipe_warning = ''
1141+
11361142
# ----- Methods related to presenting output to the user -----
11371143

11381144
@property
@@ -1171,10 +1177,10 @@ def poutput(self, msg, end='\n'):
11711177
self.stdout.write(end)
11721178
except BROKEN_PIPE_ERROR:
11731179
# This occurs if a command's output is being piped to another process and that process closes before the
1174-
# command is finished. We intentionally don't print a warning message here since we know that stdout
1175-
# will be restored by the _restore_output() method. If you would like your application to print a
1176-
# warning message, then override this method.
1177-
pass
1180+
# command is finished. If you would like your application to print a warning message, then set the
1181+
# broken_pipe_warning attribute to the message you want printed.
1182+
if self.broken_pipe_warning:
1183+
sys.stderr.write(self.broken_pipe_warning)
11781184

11791185
def perror(self, errmsg, exception_type=None, traceback_war=True):
11801186
""" Print error message to sys.stderr and if debug is true, print an exception Traceback if one exists.
@@ -1207,6 +1213,56 @@ def pfeedback(self, msg):
12071213
else:
12081214
sys.stderr.write("{}\n".format(msg))
12091215

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

@@ -1612,6 +1669,7 @@ def _redirect_output(self, statement):
16121669
self.pipe_proc = None
16131670
self.kept_state.restore()
16141671
self.kept_state = None
1672+
self.redirecting = False
16151673

16161674
# Re-raise the exception
16171675
raise ex
@@ -1620,6 +1678,7 @@ def _redirect_output(self, statement):
16201678
raise EnvironmentError('Cannot redirect to paste buffer; install ``xclip`` and re-run to enable')
16211679
self.kept_state = Statekeeper(self, ('stdout',))
16221680
self.kept_sys = Statekeeper(sys, ('stdout',))
1681+
self.redirecting = True
16231682
if statement.parsed.outputTo:
16241683
mode = 'w'
16251684
if statement.parsed.output == 2 * self.redirector:
@@ -1662,6 +1721,8 @@ def _restore_output(self, statement):
16621721
self.kept_sys.restore()
16631722
self.kept_sys = None
16641723

1724+
self.redirecting = False
1725+
16651726
def _func_named(self, arg):
16661727
"""Gets the method name associated with a given command.
16671728

docs/unfreefeatures.rst

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,21 +147,23 @@ There are a couple functions which can globally effect how arguments are parsed
147147
.. _argparse: https://docs.python.org/3/library/argparse.html
148148

149149

150-
poutput, pfeedback, perror
151-
==========================
150+
poutput, pfeedback, perror, ppaged
151+
==================================
152152

153153
Standard ``cmd`` applications produce their output with ``self.stdout.write('output')`` (or with ``print``,
154154
but ``print`` decreases output flexibility). ``cmd2`` applications can use
155-
``self.poutput('output')``, ``self.pfeedback('message')``, and ``self.perror('errmsg')``
155+
``self.poutput('output')``, ``self.pfeedback('message')``, ``self.perror('errmsg')``, and ``self.ppaged('text')``
156156
instead. These methods have these advantages:
157157

158158
- Handle output redirection to file and/or pipe appropriately
159159
- More concise
160160
- ``.pfeedback()`` destination is controlled by :ref:`quiet` parameter.
161+
- Option to display long output using a pager via ``ppaged()``
161162

162163
.. automethod:: cmd2.Cmd.poutput
163164
.. automethod:: cmd2.Cmd.perror
164165
.. automethod:: cmd2.Cmd.pfeedback
166+
.. automethod:: cmd2.Cmd.ppaged
165167

166168

167169
color

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)