Skip to content

Commit a0d829e

Browse files
authored
Merge pull request #114 from python-cmd2/exclude_help
Added ability to exclude some commands from help menu
2 parents bfd5061 + 36fdee7 commit a0d829e

4 files changed

Lines changed: 95 additions & 46 deletions

File tree

cmd2.py

Lines changed: 87 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -576,7 +576,6 @@ class Cmd(cmd.Cmd):
576576
Line-oriented command interpreters are often useful for test harnesses, internal tools, and rapid prototypes.
577577
"""
578578
# Attributes which are NOT dynamically settable at runtime
579-
580579
allow_cli_args = True # Should arguments passed on the command-line be processed as commands?
581580
allow_redirection = True # Should output redirection and pipes be allowed
582581
blankLinesAllowed = False
@@ -588,6 +587,7 @@ class Cmd(cmd.Cmd):
588587
default_to_shell = False # Attempt to run unrecognized commands as shell commands
589588
defaultExtension = 'txt' # For ``save``, ``load``, etc.
590589
excludeFromHistory = '''run r list l history hi ed edit li eof'''.split()
590+
exclude_from_help = ['do_eof'] # Commands to exclude from the help menu
591591

592592
# make sure your terminators are not in legalChars!
593593
legalChars = u'!#$%.:?@_-' + pyparsing.alphanums + pyparsing.alphas8bit
@@ -666,11 +666,13 @@ def __init__(self, completekey='tab', stdin=None, stdout=None, use_ipython=False
666666
# noinspection PyUnresolvedReferences
667667
self.keywords = self.reserved_words + [fname[3:] for fname in dir(self)
668668
if fname.startswith('do_')]
669-
self.parser_manager = ParserManager(redirector=self.redirector, terminators=self.terminators, multilineCommands=self.multilineCommands,
670-
legalChars=self.legalChars, commentGrammars=self.commentGrammars,
671-
commentInProgress=self.commentInProgress, case_insensitive=self.case_insensitive,
672-
blankLinesAllowed=self.blankLinesAllowed, prefixParser=self.prefixParser,
673-
preparse=self.preparse, postparse=self.postparse, shortcuts=self.shortcuts)
669+
self.parser_manager = ParserManager(redirector=self.redirector, terminators=self.terminators,
670+
multilineCommands=self.multilineCommands,
671+
legalChars=self.legalChars, commentGrammars=self.commentGrammars,
672+
commentInProgress=self.commentInProgress,
673+
case_insensitive=self.case_insensitive,
674+
blankLinesAllowed=self.blankLinesAllowed, prefixParser=self.prefixParser,
675+
preparse=self.preparse, postparse=self.postparse, shortcuts=self.shortcuts)
674676
self._transcript_files = transcript_files
675677

676678
# Used to enable the ability for a Python script to quit the application
@@ -1126,19 +1128,60 @@ def do_cmdenvironment(self, args):
11261128
def do_help(self, arg):
11271129
"""List available commands with "help" or detailed help with "help cmd"."""
11281130
if arg:
1131+
# Getting help for a specific command
11291132
funcname = self._func_named(arg)
11301133
if funcname:
11311134
fn = getattr(self, funcname)
11321135
try:
1136+
# Use Optparse help for @options commands
11331137
fn.optionParser.print_help(file=self.stdout)
11341138
except AttributeError:
1139+
# No special behavior needed, delegate to cmd base class do_help()
11351140
cmd.Cmd.do_help(self, funcname[3:])
11361141
else:
1137-
cmd.Cmd.do_help(self, arg)
1142+
# Show a menu of what commands help can be gotten for
1143+
self._help_menu()
1144+
1145+
def _help_menu(self):
1146+
"""Show a list of commands which help can be displayed for.
1147+
"""
1148+
# Get a list of all method names
1149+
names = self.get_names()
1150+
1151+
# Remove any command names which are explicitly excluded from the help menu
1152+
for name in self.exclude_from_help:
1153+
names.remove(name)
1154+
1155+
cmds_doc = []
1156+
cmds_undoc = []
1157+
help_dict = {}
1158+
for name in names:
1159+
if name[:5] == 'help_':
1160+
help_dict[name[5:]] = 1
1161+
names.sort()
1162+
# There can be duplicates if routines overridden
1163+
prevname = ''
1164+
for name in names:
1165+
if name[:3] == 'do_':
1166+
if name == prevname:
1167+
continue
1168+
prevname = name
1169+
command = name[3:]
1170+
if command in help_dict:
1171+
cmds_doc.append(command)
1172+
del help_dict[command]
1173+
elif getattr(self, name).__doc__:
1174+
cmds_doc.append(command)
1175+
else:
1176+
cmds_undoc.append(command)
1177+
self.stdout.write("%s\n" % str(self.doc_leader))
1178+
self.print_topics(self.doc_header, cmds_doc, 15, 80)
1179+
self.print_topics(self.misc_header, list(help_dict.keys()), 15, 80)
1180+
self.print_topics(self.undoc_header, cmds_undoc, 15, 80)
11381181

11391182
# noinspection PyUnusedLocal
11401183
def do_shortcuts(self, args):
1141-
"""Lists single-key shortcuts available."""
1184+
"""Lists shortcuts (aliases) available."""
11421185
result = "\n".join('%s: %s' % (sc[0], sc[1]) for sc in sorted(self.shortcuts))
11431186
self.stdout.write("Single-key shortcuts for other commands:\n{}\n".format(result))
11441187

@@ -1466,7 +1509,7 @@ def do_py(self, arg):
14661509
py: Enters interactive Python mode.
14671510
End with ``Ctrl-D`` (Unix) / ``Ctrl-Z`` (Windows), ``quit()``, '`exit()``.
14681511
Non-python commands can be issued with ``cmd("your command")``.
1469-
Run python code from external files with ``run("filename.py")``
1512+
Run python code from external script files with ``run("filename.py")``
14701513
"""
14711514
if self._in_py:
14721515
self.perror("Recursively entering interactive Python consoles is not allowed.", traceback_war=False)
@@ -1759,7 +1802,7 @@ def _read_file_or_url(self, fname):
17591802
def do__relative_load(self, arg=None):
17601803
"""Runs commands in script at file or URL.
17611804
1762-
Usage: load [file_path]
1805+
Usage: _relative_load [file_path]
17631806
17641807
optional argument:
17651808
file_path a file path or URL pointing to a script
@@ -1769,6 +1812,8 @@ def do__relative_load(self, arg=None):
17691812
17701813
If this is called from within an already-running script, the filename will be interpreted
17711814
relative to the already-running script's directory.
1815+
1816+
NOTE: This command is intended to only be used within text file scripts.
17721817
"""
17731818
if arg:
17741819
arg = arg.split(None, 1)
@@ -1922,28 +1967,31 @@ def cmdloop(self, intro=None):
19221967
self.postloop()
19231968

19241969

1970+
# noinspection PyPep8Naming
19251971
class ParserManager:
1926-
1927-
def __init__(self, redirector, terminators, multilineCommands, legalChars, commentGrammars,
1928-
commentInProgress, case_insensitive, blankLinesAllowed, prefixParser,
1929-
preparse, postparse, shortcuts):
1930-
"Creates and uses parsers for user input according to app's paramters."
1972+
"""
1973+
Class which encapsulates all of the pyparsing parser functionality for cmd2 in a single location.
1974+
"""
1975+
def __init__(self, redirector, terminators, multilineCommands, legalChars, commentGrammars, commentInProgress,
1976+
case_insensitive, blankLinesAllowed, prefixParser, preparse, postparse, shortcuts):
1977+
"""Creates and uses parsers for user input according to app's paramters."""
19311978

19321979
self.commentGrammars = commentGrammars
19331980
self.preparse = preparse
19341981
self.postparse = postparse
19351982
self.shortcuts = shortcuts
19361983

1937-
self.main_parser = self._build_main_parser(
1938-
redirector=redirector, terminators=terminators, multilineCommands=multilineCommands,
1939-
legalChars=legalChars,
1940-
commentInProgress=commentInProgress, case_insensitive=case_insensitive,
1941-
blankLinesAllowed=blankLinesAllowed, prefixParser=prefixParser)
1942-
self.input_source_parser = self._build_input_source_parser(legalChars=legalChars, commentInProgress=commentInProgress)
1984+
self.main_parser = self._build_main_parser(redirector=redirector, terminators=terminators,
1985+
multilineCommands=multilineCommands, legalChars=legalChars,
1986+
commentInProgress=commentInProgress,
1987+
case_insensitive=case_insensitive,
1988+
blankLinesAllowed=blankLinesAllowed, prefixParser=prefixParser)
1989+
self.input_source_parser = self._build_input_source_parser(legalChars=legalChars,
1990+
commentInProgress=commentInProgress)
19431991

19441992
def _build_main_parser(self, redirector, terminators, multilineCommands, legalChars,
19451993
commentInProgress, case_insensitive, blankLinesAllowed, prefixParser):
1946-
"Builds a PyParsing parser for interpreting user commands."
1994+
"""Builds a PyParsing parser for interpreting user commands."""
19471995

19481996
# Build several parsing components that are eventually compiled into overall parser
19491997
output_destination_parser = (pyparsing.Literal(redirector * 2) |
@@ -1959,7 +2007,8 @@ def _build_main_parser(self, redirector, terminators, multilineCommands, legalCh
19592007
pipe = pyparsing.Keyword('|', identChars='|')
19602008
do_not_parse = self.commentGrammars | commentInProgress | pyparsing.quotedString
19612009
after_elements = \
1962-
pyparsing.Optional(pipe + pyparsing.SkipTo(output_destination_parser ^ string_end, ignore=do_not_parse)('pipeTo')) + \
2010+
pyparsing.Optional(pipe + pyparsing.SkipTo(output_destination_parser ^ string_end,
2011+
ignore=do_not_parse)('pipeTo')) + \
19632012
pyparsing.Optional(output_destination_parser +
19642013
pyparsing.SkipTo(string_end,
19652014
ignore=do_not_parse).setParseAction(lambda x: x[0].strip())('outputTo'))
@@ -1972,24 +2021,23 @@ def _build_main_parser(self, redirector, terminators, multilineCommands, legalCh
19722021
blankLineTerminator = (pyparsing.lineEnd + pyparsing.lineEnd)('terminator')
19732022
blankLineTerminator.setResultsName('terminator')
19742023
blankLineTerminationParser = ((multilineCommand ^ oneline_command) +
1975-
pyparsing.SkipTo(blankLineTerminator,
1976-
ignore=do_not_parse).setParseAction(
1977-
lambda x: x[0].strip())('args') +
1978-
blankLineTerminator)('statement')
2024+
pyparsing.SkipTo(blankLineTerminator, ignore=do_not_parse).setParseAction(
2025+
lambda x: x[0].strip())('args') + blankLineTerminator)('statement')
19792026

19802027
multilineParser = (((multilineCommand ^ oneline_command) +
1981-
pyparsing.SkipTo(terminator_parser,
1982-
ignore=do_not_parse).setParseAction(
1983-
lambda x: x[0].strip())('args') + terminator_parser)('statement') +
1984-
pyparsing.SkipTo(output_destination_parser ^ pipe ^ string_end, ignore=do_not_parse).setParseAction(
1985-
lambda x: x[0].strip())('suffix') + after_elements)
2028+
pyparsing.SkipTo(terminator_parser,
2029+
ignore=do_not_parse).setParseAction(lambda x: x[0].strip())('args') +
2030+
terminator_parser)('statement') +
2031+
pyparsing.SkipTo(output_destination_parser ^ pipe ^ string_end,
2032+
ignore=do_not_parse).setParseAction(lambda x: x[0].strip())('suffix') +
2033+
after_elements)
19862034
multilineParser.ignore(commentInProgress)
19872035

19882036
singleLineParser = ((oneline_command +
1989-
pyparsing.SkipTo(terminator_parser ^ string_end ^ pipe ^ output_destination_parser,
1990-
ignore=do_not_parse).setParseAction(
1991-
lambda x: x[0].strip())('args'))('statement') +
1992-
pyparsing.Optional(terminator_parser) + after_elements)
2037+
pyparsing.SkipTo(terminator_parser ^ string_end ^ pipe ^ output_destination_parser,
2038+
ignore=do_not_parse).setParseAction(
2039+
lambda x: x[0].strip())('args'))('statement') +
2040+
pyparsing.Optional(terminator_parser) + after_elements)
19932041

19942042
blankLineTerminationParser = blankLineTerminationParser.setResultsName('statement')
19952043

@@ -2003,8 +2051,9 @@ def _build_main_parser(self, redirector, terminators, multilineCommands, legalCh
20032051
parser.ignore(self.commentGrammars)
20042052
return parser
20052053

2006-
def _build_input_source_parser(self, legalChars, commentInProgress):
2007-
"Builds a PyParsing parser for alternate user input sources (from file, pipe, etc.)"
2054+
@staticmethod
2055+
def _build_input_source_parser(legalChars, commentInProgress):
2056+
"""Builds a PyParsing parser for alternate user input sources (from file, pipe, etc.)"""
20082057

20092058
input_mark = pyparsing.Literal('<')
20102059
input_mark.setParseAction(lambda x: '')
@@ -2049,8 +2098,6 @@ def parsed(self, raw):
20492098
return p
20502099

20512100

2052-
2053-
20542101
class HistoryItem(str):
20552102
"""Class used to represent an item in the History list.
20562103

tests/conftest.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
# Help text for base cmd2.Cmd application
1616
BASE_HELP = """Documented commands (type help <topic>):
1717
========================================
18-
_relative_load edit help list pause quit save shell show
19-
cmdenvironment eof history load py run set shortcuts
18+
_relative_load edit history load py run set shortcuts
19+
cmdenvironment help list pause quit save shell show
2020
"""
2121

2222
# Help text for the history command

tests/test_transcript.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,9 @@ def test_base_with_transcript(_cmdline_app):
106106
107107
Documented commands (type help <topic>):
108108
========================================
109-
_relative_load edit help list orate py run say shell show
110-
cmdenvironment eof history load pause quit save set shortcuts speak
109+
_relative_load help load py save shell speak
110+
cmdenvironment history orate quit say shortcuts
111+
edit list pause run set show
111112
112113
(Cmd) help say
113114
Repeats what you tell me to.

tests/transcript.txt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22

33
Documented commands (type help <topic>):
44
========================================
5-
_relative_load edit help list orate py run say shell show
6-
cmdenvironment eof history load pause quit save set shortcuts speak
5+
_relative_load help load py save shell speak
6+
cmdenvironment history orate quit say shortcuts
7+
edit list pause run set show
78

89
(Cmd) help say
910
Repeats what you tell me to.

0 commit comments

Comments
 (0)