Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
69 changes: 65 additions & 4 deletions cmd2.py
Original file line number Diff line number Diff line change
Expand Up @@ -1133,6 +1133,12 @@ def __init__(self, completekey='tab', stdin=None, stdout=None, persistent_histor
# 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
Expand Down Expand Up @@ -1171,10 +1177,10 @@ def poutput(self, msg, end='\n'):
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.
Expand Down Expand Up @@ -1207,6 +1213,56 @@ def pfeedback(self, msg):
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.
Expand Down Expand Up @@ -1599,6 +1655,7 @@ def _redirect_output(self, statement):
# 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)

Expand All @@ -1612,6 +1669,7 @@ def _redirect_output(self, statement):
self.pipe_proc = None
self.kept_state.restore()
self.kept_state = None
self.redirecting = False

# Re-raise the exception
raise ex
Expand All @@ -1620,6 +1678,7 @@ def _redirect_output(self, statement):
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:
Expand Down Expand Up @@ -1662,6 +1721,8 @@ def _restore_output(self, statement):
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.

Expand Down
8 changes: 5 additions & 3 deletions docs/unfreefeatures.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 29 additions & 0 deletions examples/paged_output.py
Original file line number Diff line number Diff line change
@@ -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()