summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTodd Leonhardt <todd.leonhardt@gmail.com>2018-03-07 20:38:24 -0500
committerGitHub <noreply@github.com>2018-03-07 20:38:24 -0500
commit2af44735fd85cdbcfb2b43adbb3de1271ba37157 (patch)
tree5cf1a191d33f396f7081c915cbb1ec038795e458
parenta6f0e06a350f2ab65d3bf635fa7aae0b655ed44a (diff)
parent0979d63854bc71dab4c1596c49260539e522216b (diff)
downloadcmd2-git-2af44735fd85cdbcfb2b43adbb3de1271ba37157.tar.gz
Merge pull request #297 from python-cmd2/paged_output
Added support for paged output
-rw-r--r--CHANGELOG.md2
-rwxr-xr-xREADME.md1
-rwxr-xr-xcmd2.py69
-rw-r--r--docs/unfreefeatures.rst8
-rwxr-xr-xexamples/paged_output.py29
5 files changed, 102 insertions, 7 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index aec8e5a1..c19feaa0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,6 +16,8 @@
* ``exclude_from_help`` and ``excludeFromHistory`` are now instance instead of class attributes
* Added flag and index based tab completion helper functions
* See [tab_completion.py](https://github.com/python-cmd2/cmd2/blob/master/examples/tab_completion.py)
+ * Added support for displaying output which won't fit on the screen via a pager using ``ppaged()``
+ * See [paged_output.py](https://github.com/python-cmd2/cmd2/blob/master/examples/paged_output.py)
* Attributes Removed (**can cause breaking changes**)
* ``abbrev`` - Removed support for abbreviated commands
* Good tab completion makes this unnecessary and its presence could cause harmful unintended actions
diff --git a/README.md b/README.md
index 863a4c1d..1c9c5957 100755
--- a/README.md
+++ b/README.md
@@ -26,6 +26,7 @@ Main Features
- Redirect command output to file with `>`, `>>`; input from file with `<`
- Bare `>`, `>>` with no filename send output to paste buffer (clipboard)
- `py` enters interactive Python console (opt-in `ipy` for IPython console)
+- Option to display long output using a pager with ``cmd2.Cmd.ppaged()``
- Multi-line commands
- Special-character command shortcuts (beyond cmd's `@` and `!`)
- Settable environment parameters
diff --git a/cmd2.py b/cmd2.py
index 0b01c545..fe59da1b 100755
--- a/cmd2.py
+++ b/cmd2.py
@@ -1133,6 +1133,12 @@ class Cmd(cmd.Cmd):
# Used by complete() for readline tab completion
self.completion_matches = []
+ # Used to keep track of whether we are redirecting or piping output
+ self.redirecting = False
+
+ # If this string is non-empty, then this warning message will print if a broken pipe error occurs while printing
+ self.broken_pipe_warning = ''
+
# ----- Methods related to presenting output to the user -----
@property
@@ -1171,10 +1177,10 @@ class Cmd(cmd.Cmd):
self.stdout.write(end)
except BROKEN_PIPE_ERROR:
# This occurs if a command's output is being piped to another process and that process closes before the
- # command is finished. We intentionally don't print a warning message here since we know that stdout
- # will be restored by the _restore_output() method. If you would like your application to print a
- # warning message, then override this method.
- pass
+ # command is finished. If you would like your application to print a warning message, then set the
+ # broken_pipe_warning attribute to the message you want printed.
+ if self.broken_pipe_warning:
+ sys.stderr.write(self.broken_pipe_warning)
def perror(self, errmsg, exception_type=None, traceback_war=True):
""" Print error message to sys.stderr and if debug is true, print an exception Traceback if one exists.
@@ -1207,6 +1213,56 @@ class Cmd(cmd.Cmd):
else:
sys.stderr.write("{}\n".format(msg))
+ def ppaged(self, msg, end='\n'):
+ """Print output using a pager if it would go off screen and stdout isn't currently being redirected.
+
+ Never uses a pager inside of a script (Python or text) or when output is being redirected or piped.
+
+ :param msg: str - message to print to current stdout - anything convertible to a str with '{}'.format() is OK
+ :param end: str - string appended after the end of the message if not already present, default a newline
+ """
+ if msg is not None and msg != '':
+ try:
+ msg_str = '{}'.format(msg)
+ if not msg_str.endswith(end):
+ msg_str += end
+
+ # Don't attempt to use a pager that can block if redirecting or running a script (either text or Python)
+ if not self.redirecting and not self._in_py and not self._script_dir:
+ if sys.platform.startswith('win'):
+ pager_cmd = 'more'
+ else:
+ # Here is the meaning of the various flags we are using with the less command:
+ # -S causes lines longer than the screen width to be chopped (truncated) rather than wrapped
+ # -R causes ANSI "color" escape sequences to be output in raw form (i.e. colors are displayed)
+ # -X disables sending the termcap initialization and deinitialization strings to the terminal
+ # -F causes less to automatically exit if the entire file can be displayed on the first screen
+ pager_cmd = 'less -SRXF'
+ self.pipe_proc = subprocess.Popen(pager_cmd, shell=True, stdin=subprocess.PIPE)
+ try:
+ self.pipe_proc.stdin.write(msg_str.encode('utf-8', 'replace'))
+ self.pipe_proc.stdin.close()
+ except (IOError, KeyboardInterrupt):
+ pass
+
+ # Less doesn't respect ^C, but catches it for its own UI purposes (aborting search etc. inside less)
+ while True:
+ try:
+ self.pipe_proc.wait()
+ except KeyboardInterrupt:
+ pass
+ else:
+ break
+ self.pipe_proc = None
+ else:
+ self.stdout.write(msg_str)
+ except BROKEN_PIPE_ERROR:
+ # This occurs if a command's output is being piped to another process and that process closes before the
+ # command is finished. If you would like your application to print a warning message, then set the
+ # broken_pipe_warning attribute to the message you want printed.
+ if self.broken_pipe_warning:
+ sys.stderr.write(self.broken_pipe_warning)
+
def colorize(self, val, color):
"""Given a string (``val``), returns that string wrapped in UNIX-style
special characters that turn on (and then off) text color and style.
@@ -1599,6 +1655,7 @@ class Cmd(cmd.Cmd):
# Open each side of the pipe and set stdout accordingly
# noinspection PyTypeChecker
self.stdout = io.open(write_fd, write_mode)
+ self.redirecting = True
# noinspection PyTypeChecker
subproc_stdin = io.open(read_fd, read_mode)
@@ -1612,6 +1669,7 @@ class Cmd(cmd.Cmd):
self.pipe_proc = None
self.kept_state.restore()
self.kept_state = None
+ self.redirecting = False
# Re-raise the exception
raise ex
@@ -1620,6 +1678,7 @@ class Cmd(cmd.Cmd):
raise EnvironmentError('Cannot redirect to paste buffer; install ``xclip`` and re-run to enable')
self.kept_state = Statekeeper(self, ('stdout',))
self.kept_sys = Statekeeper(sys, ('stdout',))
+ self.redirecting = True
if statement.parsed.outputTo:
mode = 'w'
if statement.parsed.output == 2 * self.redirector:
@@ -1662,6 +1721,8 @@ class Cmd(cmd.Cmd):
self.kept_sys.restore()
self.kept_sys = None
+ self.redirecting = False
+
def _func_named(self, arg):
"""Gets the method name associated with a given command.
diff --git a/docs/unfreefeatures.rst b/docs/unfreefeatures.rst
index e92bf2d6..2d6c8c3c 100644
--- a/docs/unfreefeatures.rst
+++ b/docs/unfreefeatures.rst
@@ -147,21 +147,23 @@ There are a couple functions which can globally effect how arguments are parsed
.. _argparse: https://docs.python.org/3/library/argparse.html
-poutput, pfeedback, perror
-==========================
+poutput, pfeedback, perror, ppaged
+==================================
Standard ``cmd`` applications produce their output with ``self.stdout.write('output')`` (or with ``print``,
but ``print`` decreases output flexibility). ``cmd2`` applications can use
-``self.poutput('output')``, ``self.pfeedback('message')``, and ``self.perror('errmsg')``
+``self.poutput('output')``, ``self.pfeedback('message')``, ``self.perror('errmsg')``, and ``self.ppaged('text')``
instead. These methods have these advantages:
- Handle output redirection to file and/or pipe appropriately
- More concise
- ``.pfeedback()`` destination is controlled by :ref:`quiet` parameter.
+- Option to display long output using a pager via ``ppaged()``
.. automethod:: cmd2.Cmd.poutput
.. automethod:: cmd2.Cmd.perror
.. automethod:: cmd2.Cmd.pfeedback
+.. automethod:: cmd2.Cmd.ppaged
color
diff --git a/examples/paged_output.py b/examples/paged_output.py
new file mode 100755
index 00000000..171c1b3e
--- /dev/null
+++ b/examples/paged_output.py
@@ -0,0 +1,29 @@
+#!/usr/bin/env python
+# coding=utf-8
+"""A simple example demonstrating the using paged output via the ppaged() method.
+"""
+import functools
+
+import cmd2
+from cmd2 import with_argument_list
+
+
+class PagedOutput(cmd2.Cmd):
+ """ Example cmd2 application where we create commands that just print the arguments they are called with."""
+
+ def __init__(self):
+ cmd2.Cmd.__init__(self)
+
+ @with_argument_list
+ def do_page_file(self, args):
+ """Read in a text file and display its output in a pager."""
+ with open(args[0], 'r') as f:
+ text = f.read()
+ self.ppaged(text)
+
+ complete_page_file = functools.partial(cmd2.path_complete)
+
+
+if __name__ == '__main__':
+ app = PagedOutput()
+ app.cmdloop()