summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xcmd2.py58
-rw-r--r--docs/argument_processing.rst132
-rwxr-xr-xexamples/help_categories.py12
-rw-r--r--tests/conftest.py1
-rw-r--r--tests/test_cmd2.py7
5 files changed, 192 insertions, 18 deletions
diff --git a/cmd2.py b/cmd2.py
index 201d608b..629eee82 100755
--- a/cmd2.py
+++ b/cmd2.py
@@ -79,7 +79,6 @@ except ImportError:
return True
return NotImplemented
-
# Newer versions of pyperclip are released as a single file, but older versions had a more complicated structure
try:
from pyperclip.exceptions import PyperclipException
@@ -113,6 +112,12 @@ if sys.version_info < (3, 5):
else:
from contextlib import redirect_stdout, redirect_stderr
+if sys.version_info > (3, 0):
+ from io import StringIO # Python3
+else:
+ from io import BytesIO as StringIO # Python2
+
+
# Detect whether IPython is installed to determine if the built-in "ipy" command should be included
ipython_available = True
try:
@@ -183,6 +188,7 @@ if six.PY2 and sys.platform.startswith('lin'):
except ImportError:
pass
+
__version__ = '0.8.4'
# Pyparsing enablePackrat() can greatly speed up parsing, but problems have been seen in Python 3 in the past
@@ -212,6 +218,7 @@ REDIRECTION_CHARS = ['|', '<', '>']
# optional attribute, when tagged on a function, allows cmd2 to categorize commands
HELP_CATEGORY = 'help_category'
+HELP_SUMMARY = 'help_summary'
def categorize(func, category):
@@ -358,6 +365,14 @@ def parse_quoted_string(cmdline):
return lexed_arglist
+def with_category(category):
+ """A decorator to apply a category to a command function"""
+ def cat_decorator(func):
+ categorize(func, category)
+ return func
+ return cat_decorator
+
+
def with_argument_list(func):
"""A decorator to alter the arguments passed to a do_* cmd2
method. Default passes a string of whatever the user typed.
@@ -396,6 +411,9 @@ def with_argparser_and_unknown_args(argparser):
if argparser.description is None and func.__doc__:
argparser.description = func.__doc__
+ if func.__doc__:
+ setattr(cmd_wrapper, HELP_SUMMARY, func.__doc__)
+
cmd_wrapper.__doc__ = argparser.format_help()
# Mark this function as having an argparse ArgumentParser (used by do_help)
@@ -435,6 +453,9 @@ def with_argparser(argparser):
if argparser.description is None and func.__doc__:
argparser.description = func.__doc__
+ if func.__doc__:
+ setattr(cmd_wrapper, HELP_SUMMARY, func.__doc__)
+
cmd_wrapper.__doc__ = argparser.format_help()
# Mark this function as having an argparse ArgumentParser (used by do_help)
@@ -2984,21 +3005,44 @@ Usage: Usage: unalias [-a] name [name ...]
help_topics = self.get_help_topics()
for command in cmds:
+ doc = ''
+ # Try to get the documentation string
+ try:
+ # first see if there's a help function implemented
+ func = getattr(self, 'help_' + command)
+ except AttributeError:
+ # Couldn't find a help function
+ try:
+ # Now see if help_summary has been set
+ doc = getattr(self, self._func_named(command)).help_summary
+ except AttributeError:
+ # Last, try to directly ac cess the function's doc-string
+ doc = getattr(self, self._func_named(command)).__doc__
+ else:
+ # we found the help function
+ result = StringIO()
+ # try to redirect system stdout
+ with redirect_stdout(result):
+ # save our internal stdout
+ stdout_orig = self.stdout
+ try:
+ # redirect our internal stdout
+ self.stdout = result
+ func()
+ finally:
+ # restore internal stdout
+ self.stdout = stdout_orig
+ doc = result.getvalue()
+
# Attempt to locate the first documentation block
- doc = getattr(self, self._func_named(command)).__doc__
doc_block = []
found_first = False
- in_usage = False
for doc_line in doc.splitlines():
str(doc_line).strip()
if len(doc_line.strip()) > 0:
- if in_usage or doc_line.startswith('usage: '):
- in_usage = True
- continue
doc_block.append(doc_line.strip())
found_first = True
else:
- in_usage = False
if found_first:
break
diff --git a/docs/argument_processing.rst b/docs/argument_processing.rst
index 15c947fb..08f866b2 100644
--- a/docs/argument_processing.rst
+++ b/docs/argument_processing.rst
@@ -160,6 +160,138 @@ Which yields:
This command can not generate tags with no content, like <br/>
+Grouping Commands
+=================
+
+By default, the ``help`` command displays::
+
+ Documented commands (type help <topic>):
+ ========================================
+ alias findleakers pyscript sessions status vminfo
+ config help quit set stop which
+ connect history redeploy shell thread_dump
+ deploy list resources shortcuts unalias
+ edit load restart sslconnectorciphers undeploy
+ expire py serverinfo start version
+
+If you have a large number of commands, you can optionally group your commands into categories.
+Here's the output from the example ``help_categories.py``::
+
+ Documented commands (type help <topic>):
+
+ Application Management
+ ======================
+ deploy findleakers redeploy sessions stop
+ expire list restart start undeploy
+
+ Connecting
+ ==========
+ connect which
+
+ Server Information
+ ==================
+ resources serverinfo sslconnectorciphers status thread_dump vminfo
+
+ Other
+ =====
+ alias edit history py quit shell unalias
+ config help load pyscript set shortcuts version
+
+
+There are 2 methods of specifying command categories, using the ``@with_category`` decorator or with the
+``categorize()`` function. Once a single command category is detected, the help output switches to a categorized
+mode of display. All commands with an explicit category defined default to the category `Other`.
+
+Using the ``@with_category`` decorator::
+
+ @with_category(CMD_CAT_CONNECTING)
+ def do_which(self, _):
+ """Which command"""
+ self.poutput('Which')
+
+Using the ``categorize()`` function:
+
+ You can call with a single function::
+
+ def do_connect(self, _):
+ """Connect command"""
+ self.poutput('Connect')
+
+ # Tag the above command functions under the category Connecting
+ categorize(do_connect, CMD_CAT_CONNECTING)
+
+ Or with an Iterable container of functions::
+
+ def do_undeploy(self, _):
+ """Undeploy command"""
+ self.poutput('Undeploy')
+
+ def do_stop(self, _):
+ """Stop command"""
+ self.poutput('Stop')
+
+ def do_findleakers(self, _):
+ """Find Leakers command"""
+ self.poutput('Find Leakers')
+
+ # Tag the above command functions under the category Application Management
+ categorize((do_undeploy,
+ do_stop,
+ do_findleakers), CMD_CAT_APP_MGMT)
+
+The ``help`` command also has a verbose option (``help -v`` or ``help --verbose``) that combines
+the help categories with per-command Help Messages::
+
+ Documented commands (type help <topic>):
+
+ Application Management
+ ================================================================================
+ deploy Deploy command
+ expire Expire command
+ findleakers Find Leakers command
+ list List command
+ redeploy Redeploy command
+ restart usage: restart [-h] {now,later,sometime,whenever}
+ sessions Sessions command
+ start Start command
+ stop Stop command
+ undeploy Undeploy command
+
+ Connecting
+ ================================================================================
+ connect Connect command
+ which Which command
+
+ Server Information
+ ================================================================================
+ resources Resources command
+ serverinfo Server Info command
+ sslconnectorciphers SSL Connector Ciphers command is an example of a command that contains
+ multiple lines of help information for the user. Each line of help in a
+ contiguous set of lines will be printed and aligned in the verbose output
+ provided with 'help --verbose'
+ status Status command
+ thread_dump Thread Dump command
+ vminfo VM Info command
+
+ Other
+ ================================================================================
+ alias Define or display aliases
+ config Config command
+ edit Edit a file in a text editor.
+ help List available commands with "help" or detailed help with "help cmd".
+ history usage: history [-h] [-r | -e | -s | -o FILE | -t TRANSCRIPT] [arg]
+ load Runs commands in script file that is encoded as either ASCII or UTF-8 text.
+ py Invoke python command, shell, or script
+ pyscript Runs a python script file inside the console
+ quit Exits this application.
+ set usage: set [-h] [-a] [-l] [settable [settable ...]]
+ shell Execute a command as if at the OS prompt.
+ shortcuts Lists shortcuts (aliases) available.
+ unalias Unsets aliases
+ version Version command
+
+
Receiving an argument list
==========================
diff --git a/examples/help_categories.py b/examples/help_categories.py
index 8a33e62c..e7e3373d 100755
--- a/examples/help_categories.py
+++ b/examples/help_categories.py
@@ -4,7 +4,7 @@
A sample application for tagging categories on commands.
"""
-from cmd2 import Cmd, categorize, __version__, with_argparser
+from cmd2 import Cmd, categorize, __version__, with_argparser, with_category
import argparse
@@ -24,14 +24,14 @@ class HelpCategories(Cmd):
"""Connect command"""
self.poutput('Connect')
+ # Tag the above command functions under the category Connecting
+ categorize(do_connect, CMD_CAT_CONNECTING)
+
+ @with_category(CMD_CAT_CONNECTING)
def do_which(self, _):
"""Which command"""
self.poutput('Which')
- # Tag the above command functions under the category Connecting
- categorize(do_connect, CMD_CAT_CONNECTING)
- categorize(do_which, CMD_CAT_CONNECTING)
-
def do_list(self, _):
"""List command"""
self.poutput('List')
@@ -58,6 +58,7 @@ class HelpCategories(Cmd):
help='Specify when to restart')
@with_argparser(restart_parser)
+ @with_category(CMD_CAT_APP_MGMT)
def do_restart(self, _):
"""Restart command"""
self.poutput('Restart')
@@ -84,7 +85,6 @@ class HelpCategories(Cmd):
do_start,
do_sessions,
do_redeploy,
- do_restart,
do_expire,
do_undeploy,
do_stop,
diff --git a/tests/conftest.py b/tests/conftest.py
index 4170a5e1..837e7504 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -34,7 +34,6 @@ set Sets a settable parameter or shows current settings of param
shell Execute a command as if at the OS prompt.
shortcuts Lists shortcuts (aliases) available.
unalias Unsets aliases
-
"""
# Help text for the history command
diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py
index 0861c073..75d27869 100644
--- a/tests/test_cmd2.py
+++ b/tests/test_cmd2.py
@@ -55,7 +55,7 @@ def test_base_argparse_help(base_app, capsys):
# Verify that "set -h" gives the same output as "help set" and that it starts in a way that makes sense
run_cmd(base_app, 'set -h')
out, err = capsys.readouterr()
- out1 = normalize(out)
+ out1 = normalize(str(out))
out2 = run_cmd(base_app, 'help set')
@@ -1080,12 +1080,11 @@ class HelpCategoriesApp(cmd2.Cmd):
# Need to use this older form of invoking super class constructor to support Python 2.x and Python 3.x
cmd2.Cmd.__init__(self, *args, **kwargs)
+ @cmd2.with_category('Some Category')
def do_diddly(self, arg):
"""This command does diddly"""
pass
- cmd2.categorize(do_diddly, "Some Category")
-
def do_squat(self, arg):
"""This docstring help will never be shown because the help_squat method overrides it."""
pass
@@ -1138,7 +1137,7 @@ def test_help_cat_verbose(helpcat_app):
Custom Category
================================================================================
edit This overrides the edit command and does nothing.
-squat This docstring help will never be shown because the help_squat method overrides it.
+squat This command does diddly squat...
Some Category
================================================================================