@@ -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
0 commit comments