diff options
| author | Todd Leonhardt <todd.leonhardt@gmail.com> | 2018-03-07 20:38:24 -0500 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2018-03-07 20:38:24 -0500 |
| commit | 2af44735fd85cdbcfb2b43adbb3de1271ba37157 (patch) | |
| tree | 5cf1a191d33f396f7081c915cbb1ec038795e458 | |
| parent | a6f0e06a350f2ab65d3bf635fa7aae0b655ed44a (diff) | |
| parent | 0979d63854bc71dab4c1596c49260539e522216b (diff) | |
| download | cmd2-git-2af44735fd85cdbcfb2b43adbb3de1271ba37157.tar.gz | |
Merge pull request #297 from python-cmd2/paged_output
Added support for paged output
| -rw-r--r-- | CHANGELOG.md | 2 | ||||
| -rwxr-xr-x | README.md | 1 | ||||
| -rwxr-xr-x | cmd2.py | 69 | ||||
| -rw-r--r-- | docs/unfreefeatures.rst | 8 | ||||
| -rwxr-xr-x | examples/paged_output.py | 29 |
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 @@ -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 @@ -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() |
