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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@
* Added ``matches_sort_key`` to override the default way tab completion matches are sorted
* Added ``StdSim.pause_storage`` member which when True will cause ``StdSim`` to not save the output sent to it.
See documentation for ``CommandResult`` in ``pyscript_bridge.py`` for reasons pausing the storage can be useful.
* Added ability to disable/enable individual commands and entire categories of commands. When a command
is disabled, it will not show up in the help menu or tab complete. If a user tries to run the command
or call help on it, a command-specific message supplied by the developer will be printed. The following
commands were added to support this feature.
* ``enable_command()``
* ``enable_category()``
* ``disable_command()``
* ``disable_category()``
* Potentially breaking changes
* Made ``cmd2_app`` a positional and required argument of ``AutoCompleter`` since certain functionality now
requires that it can't be ``None``.
Expand Down
119 changes: 113 additions & 6 deletions cmd2/cmd2.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import re
import sys
import threading
from collections import namedtuple
from typing import Any, Callable, Dict, List, Mapping, Optional, Tuple, Type, Union, IO

import colorama
Expand Down Expand Up @@ -279,6 +280,10 @@ class EmptyStatement(Exception):
pass


# Contains data about a disabled command which is used to restore its original functions when the command is enabled
DisabledCommand = namedtuple('DisabledCommand', ['command_function', 'help_function'])


class Cmd(cmd.Cmd):
"""An easy but powerful framework for writing line-oriented command interpreters.

Expand Down Expand Up @@ -521,6 +526,11 @@ def __init__(self, completekey: str = 'tab', stdin=None, stdout=None, persistent
# being printed by a command.
self.terminal_lock = threading.RLock()

# Commands that have been disabled from use. This is to support commands that are only available
# during specific states of the application. This dictionary's keys are the command names and its
# values are DisabledCommand objects.
self.disabled_commands = dict()

# ----- Methods related to presenting output to the user -----

@property
Expand Down Expand Up @@ -1562,14 +1572,19 @@ def get_all_commands(self) -> List[str]:
if name.startswith(COMMAND_FUNC_PREFIX) and callable(getattr(self, name))]

def get_visible_commands(self) -> List[str]:
"""Returns a list of commands that have not been hidden."""
"""Returns a list of commands that have not been hidden or disabled."""
commands = self.get_all_commands()

# Remove the hidden commands
for name in self.hidden_commands:
if name in commands:
commands.remove(name)

# Remove the disabled commands
for name in self.disabled_commands:
if name in commands:
commands.remove(name)

return commands

def get_alias_names(self) -> List[str]:
Expand Down Expand Up @@ -1953,7 +1968,7 @@ def cmd_func_name(self, command: str) -> str:
def onecmd(self, statement: Union[Statement, str]) -> bool:
""" This executes the actual do_* method for a command.

If the command provided doesn't exist, then it executes _default() instead.
If the command provided doesn't exist, then it executes default() instead.

:param statement: intended to be a Statement instance parsed command from the input stream, alternative
acceptance of a str is present only for backward compatibility with cmd
Expand All @@ -1969,8 +1984,9 @@ def onecmd(self, statement: Union[Statement, str]) -> bool:
else:
func = self.cmd_func(statement.command)
if func:
# Since we have a valid command store it in the history
if statement.command not in self.exclude_from_history:
# Check to see if this command should be stored in history
if statement.command not in self.exclude_from_history \
and statement.command not in self.disabled_commands:
self.history.append(statement)

stop = func(statement)
Expand Down Expand Up @@ -3186,13 +3202,15 @@ def do_history(self, args: argparse.Namespace) -> None:

# -v must be used alone with no other options
if args.verbose:
if args.clear or args.edit or args.output_file or args.run or args.transcript or args.expanded or args.script:
if args.clear or args.edit or args.output_file or args.run or args.transcript \
or args.expanded or args.script:
self.poutput("-v can not be used with any other options")
self.poutput(self.history_parser.format_usage())
return

# -s and -x can only be used if none of these options are present: [-c -r -e -o -t]
if (args.script or args.expanded) and (args.clear or args.edit or args.output_file or args.run or args.transcript):
if (args.script or args.expanded) \
and (args.clear or args.edit or args.output_file or args.run or args.transcript):
self.poutput("-s and -x can not be used with -c, -r, -e, -o, or -t")
self.poutput(self.history_parser.format_usage())
return
Expand Down Expand Up @@ -3598,6 +3616,95 @@ def set_window_title(self, title: str) -> None: # pragma: no cover
else:
raise RuntimeError("another thread holds terminal_lock")

def enable_command(self, command: str) -> None:
"""
Enable a command by restoring its functions
:param command: the command being enabled
"""
# If the commands is already enabled, then return
if command not in self.disabled_commands:
return

help_func_name = HELP_FUNC_PREFIX + command

# Restore the command and help functions to their original values
dc = self.disabled_commands[command]
setattr(self, self.cmd_func_name(command), dc.command_function)

if dc.help_function is None:
delattr(self, help_func_name)
else:
setattr(self, help_func_name, dc.help_function)

# Remove the disabled command entry
del self.disabled_commands[command]

def enable_category(self, category: str) -> None:
"""
Enable an entire category of commands
:param category: the category to enable
"""
for cmd_name in list(self.disabled_commands):
dc = self.disabled_commands[cmd_name]
cmd_category = getattr(dc.command_function, HELP_CATEGORY, None)
if cmd_category is not None and cmd_category == category:
self.enable_command(cmd_name)

def disable_command(self, command: str, message_to_print: str) -> None:
"""
Disable a command and overwrite its functions
:param command: the command being disabled
:param message_to_print: what to print when this command is run or help is called on it while disabled
"""
import functools

# If the commands is already disabled, then return
if command in self.disabled_commands:
return

# Make sure this is an actual command
command_function = self.cmd_func(command)
if command_function is None:
raise AttributeError("{} does not refer to a command".format(command))

help_func_name = HELP_FUNC_PREFIX + command

# Add the disabled command record
self.disabled_commands[command] = DisabledCommand(command_function=command_function,
help_function=getattr(self, help_func_name, None))

# Overwrite the command and help functions to print the message
new_func = functools.partial(self._report_disabled_command_usage, message_to_print=message_to_print)
setattr(self, self.cmd_func_name(command), new_func)
setattr(self, help_func_name, new_func)

def disable_category(self, category: str, message_to_print: str) -> None:
"""
Disable an entire category of commands
:param category: the category to disable
:param message_to_print: what to print when anything in this category is run or help is called on it
while disabled
"""
all_commands = self.get_all_commands()

for cmd_name in all_commands:
func = self.cmd_func(cmd_name)
cmd_category = getattr(func, HELP_CATEGORY, None)

# If this command is in the category, then disable it
if cmd_category is not None and cmd_category == category:
self.disable_command(cmd_name, message_to_print)

# noinspection PyUnusedLocal
def _report_disabled_command_usage(self, *args, message_to_print: str, **kwargs) -> None:
"""
Report when a disabled command has been run or had help called on it
:param args: not used
:param message_to_print: the message reporting that the command is disabled
:param kwargs: not used
"""
self.poutput(message_to_print)

def cmdloop(self, intro: Optional[str] = None) -> None:
"""This is an outer wrapper around _cmdloop() which deals with extra features provided by cmd2.

Expand Down
132 changes: 0 additions & 132 deletions docs/argument_processing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -195,138 +195,6 @@ Which yields:
.. _argparse: https://docs.python.org/3/library/argparse.html


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 available
unalias Unsets aliases
version Version command


Receiving an argument list
==========================

Expand Down
Loading