Skip to content

Commit 29ef268

Browse files
committed
Tab-completion of subcommand names is now supported
1 parent 86d81c3 commit 29ef268

File tree

2 files changed

+103
-10
lines changed

2 files changed

+103
-10
lines changed

cmd2.py

Lines changed: 99 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -272,10 +272,13 @@ def cmd_wrapper(self, cmdline):
272272
return cmd_wrapper
273273

274274

275-
def with_argparser_and_unknown_args(argparser):
276-
"""A decorator to alter a cmd2 method to populate its ``args``
277-
argument by parsing arguments with the given instance of
278-
argparse.ArgumentParser, but also returning unknown args as a list.
275+
def with_argparser_and_unknown_args(argparser, subcommand_names=None):
276+
"""A decorator to alter a cmd2 method to populate its ``args`` argument by parsing arguments with the given
277+
instance of argparse.ArgumentParser, but also returning unknown args as a list.
278+
279+
:param argparser: argparse.ArgumentParser - given instance of ArgumentParser
280+
:param subcommand_names: List[str] - list of subcommand names for this parser (used for tab-completion)
281+
:return: function that gets passed parsed args and a list of unknown args
279282
"""
280283
def arg_decorator(func):
281284
def cmd_wrapper(instance, cmdline):
@@ -296,15 +299,22 @@ def cmd_wrapper(instance, cmdline):
296299
# Mark this function as having an argparse ArgumentParser (used by do_help)
297300
cmd_wrapper.__dict__['has_parser'] = True
298301

302+
# If there are subcommands, store their names to support tab-completion of subcommand names
303+
if subcommand_names is not None:
304+
cmd_wrapper.__dict__['subcommand_names'] = subcommand_names
305+
299306
return cmd_wrapper
300307

301308
return arg_decorator
302309

303310

304-
def with_argument_parser(argparser):
305-
"""A decorator to alter a cmd2 method to populate its ``args``
306-
argument by parsing arguments with the given instance of
307-
argparse.ArgumentParser.
311+
def with_argument_parser(argparser, subcommand_names=None):
312+
"""A decorator to alter a cmd2 method to populate its ``args`` argument by parsing arguments
313+
with the given instance of argparse.ArgumentParser.
314+
315+
:param argparser: argparse.ArgumentParser - given instance of ArgumentParser
316+
:param subcommand_names: List[str] - list of subcommand names for this parser (used for tab-completion)
317+
:return: function that gets passed parsed args
308318
"""
309319
def arg_decorator(func):
310320
def cmd_wrapper(instance, cmdline):
@@ -325,6 +335,10 @@ def cmd_wrapper(instance, cmdline):
325335
# Mark this function as having an argparse ArgumentParser (used by do_help)
326336
cmd_wrapper.__dict__['has_parser'] = True
327337

338+
# If there are subcommands, store their names to support tab-completion of subcommand names
339+
if subcommand_names is not None:
340+
cmd_wrapper.__dict__['subcommand_names'] = subcommand_names
341+
328342
return cmd_wrapper
329343

330344
return arg_decorator
@@ -743,7 +757,7 @@ def colorize(self, val, color):
743757

744758
# noinspection PyMethodOverriding
745759
def completenames(self, text, line, begidx, endidx):
746-
"""Override of cmd2 method which completes command names both for command completion and help."""
760+
"""Override of cmd method which completes command names both for command completion and help."""
747761
command = text
748762
if self.case_insensitive:
749763
command = text.lower()
@@ -757,6 +771,82 @@ def completenames(self, text, line, begidx, endidx):
757771

758772
return cmd_completion
759773

774+
def complete_subcommand(self, text, line, begidx, endidx):
775+
"""Readline tab-completion method for completing argparse sub-command names."""
776+
cmd, args, foo = self.parseline(line)
777+
arglist = args.split()
778+
779+
if cmd + ' ' + args == line:
780+
funcname = self._func_named(cmd)
781+
if funcname:
782+
# Check to see if this function was decorated with an argparse ArgumentParser
783+
func = getattr(self, funcname)
784+
subcommand_names = func.__dict__.get('subcommand_names', None)
785+
786+
# If this command has subcommands
787+
if subcommand_names is not None:
788+
arg = ''
789+
if arglist:
790+
arg = arglist[0]
791+
792+
matches = [sc for sc in subcommand_names if sc.startswith(arg)]
793+
794+
# If completing the sub-command name and get exactly 1 result and are at end of line, add a space
795+
if len(matches) == 1 and endidx == len(line):
796+
matches[0] += ' '
797+
return matches
798+
799+
return []
800+
801+
def complete(self, text, state):
802+
"""Override of cmd method which returns the next possible completion for 'text'.
803+
804+
If a command has not been entered, then complete against command list.
805+
Otherwise try to call complete_<command> to get list of completions.
806+
"""
807+
if state == 0:
808+
import readline
809+
origline = readline.get_line_buffer()
810+
line = origline.lstrip()
811+
stripped = len(origline) - len(line)
812+
begidx = readline.get_begidx() - stripped
813+
endidx = readline.get_endidx() - stripped
814+
if begidx>0:
815+
cmd, args, foo = self.parseline(line)
816+
if cmd == '':
817+
compfunc = self.completedefault
818+
else:
819+
arglist = args.split()
820+
821+
compfunc = None
822+
# If the user has entered no more than a single argument after the command name
823+
if len(arglist) <= 1 and cmd + ' ' + args == line:
824+
funcname = self._func_named(cmd)
825+
if funcname:
826+
# Check to see if this function was decorated with an argparse ArgumentParser
827+
func = getattr(self, funcname)
828+
subcommand_names = func.__dict__.get('subcommand_names', None)
829+
830+
# If this command has subcommands
831+
if subcommand_names is not None:
832+
compfunc = self.complete_subcommand
833+
834+
if compfunc is None:
835+
# This command either doesn't have sub-commands or the user is past the point of entering one
836+
try:
837+
compfunc = getattr(self, 'complete_' + cmd)
838+
except AttributeError:
839+
compfunc = self.completedefault
840+
else:
841+
compfunc = self.completenames
842+
843+
self.completion_matches = compfunc(text, line, begidx, endidx)
844+
845+
try:
846+
return self.completion_matches[state]
847+
except IndexError:
848+
return None
849+
760850
def precmd(self, statement):
761851
"""Hook method executed just before the command is processed by ``onecmd()`` and after adding it to the history.
762852

examples/subcommands.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,10 @@ def bar(self, args):
4242
parser_bar.add_argument('z', help='string')
4343
parser_bar.set_defaults(func=bar)
4444

45-
@with_argument_parser(base_parser)
45+
# Create a list of subcommand names, which is used to enable tab-completion of sub-commands
46+
subcommands = ['foo', 'bar']
47+
48+
@with_argument_parser(base_parser, subcommands)
4649
def do_base(self, args):
4750
"""Base command help"""
4851
try:

0 commit comments

Comments
 (0)