@@ -547,6 +547,230 @@ def strip_ansi(text):
547547 return ANSI_ESCAPE_RE .sub ('' , text )
548548
549549
550+ def _pop_readline_history (clear_history = True ):
551+ """Returns a copy of readline's history and optionally clears it (default)"""
552+ history = [
553+ readline .get_history_item (i )
554+ for i in range (1 , 1 + readline .get_current_history_length ())
555+ ]
556+ if clear_history :
557+ readline .clear_history ()
558+ return history
559+
560+
561+ def _push_readline_history (history , clear_history = True ):
562+ """Restores readline's history and optionally clears it first (default)"""
563+ if clear_history :
564+ readline .clear_history ()
565+ for line in history :
566+ readline .add_history (line )
567+
568+
569+ def _complete_from_cmd (cmd_obj , text , line , begidx , endidx ):
570+ """Complete as though the user was typing inside cmd's cmdloop()"""
571+ from itertools import takewhile
572+ command_subcommand_params = line .split (None , 3 )
573+
574+ if len (command_subcommand_params ) < (3 if text else 2 ):
575+ return cmd_obj .completenames (text )
576+
577+ command , subcommand = command_subcommand_params [:2 ]
578+ n = len (command ) + sum (1 for _ in takewhile (str .isspace , line ))
579+ cfun = getattr (cmd_obj , 'complete_' + subcommand , cmd_obj .complete )
580+ return cfun (text , line [n :], begidx - n , endidx - n )
581+
582+
583+ class AddSubmenu (object ):
584+ """Conveniently add a submenu (Cmd-like class) to a Cmd
585+
586+ e.g. given "class SubMenu(Cmd): ..." then
587+
588+ @AddSubmenu(SubMenu(), 'sub')
589+ class MyCmd(cmd.Cmd):
590+ ....
591+
592+ will have the following effects:
593+ 1. 'sub' will interactively enter the cmdloop of a SubMenu instance
594+ 2. 'sub cmd args' will call do_cmd(args) in a SubMenu instance
595+ 3. 'sub ... [TAB]' will have the same behavior as [TAB] in a SubMenu cmdloop
596+ i.e., autocompletion works the way you think it should
597+ 4. 'help sub [cmd]' will print SubMenu's help (calls its do_help())
598+ """
599+
600+ class _Nonexistent (object ):
601+ """
602+ Used to mark missing attributes.
603+ Disable __dict__ creation since this class does nothing
604+ """
605+ __slots__ = () #
606+
607+ def __init__ (self ,
608+ submenu ,
609+ command ,
610+ aliases = (),
611+ reformat_prompt = "{super_prompt}>> {sub_prompt}" ,
612+ shared_attributes = None ,
613+ require_predefined_shares = True ,
614+ create_subclass = False
615+ ):
616+ """Set up the class decorator
617+
618+ submenu (Cmd): Instance of something cmd.Cmd-like
619+
620+ command (str): The command the user types to access the SubMenu instance
621+
622+ aliases (iterable): More commands that will behave like "command"
623+
624+ reformat_prompt (str): Format str or None to disable
625+ if it's a string, it should contain one or more of:
626+ {super_prompt}: The current cmd's prompt
627+ {command}: The command in the current cmd with which it was called
628+ {sub_prompt}: The subordinate cmd's original prompt
629+ the default is "{super_prompt}{command} {sub_prompt}"
630+
631+ shared_attributes (dict): dict of the form {'subordinate_attr': 'parent_attr'}
632+ the attributes are copied to the submenu at the last moment; the submenu's
633+ attributes are backed up before this and restored afterward
634+
635+ require_predefined_shares: The shared attributes above must be independently
636+ defined in the subordinate Cmd (default: True)
637+
638+ create_subclass: put the modifications in a subclass rather than modifying
639+ the existing class (default: False)
640+ """
641+ self .submenu = submenu
642+ self .command = command
643+ self .aliases = aliases
644+
645+ if reformat_prompt is not None and not isinstance (reformat_prompt , str ):
646+ raise ValueError ("reformat_prompt should be either a format string or None" )
647+ self .reformat_prompt = reformat_prompt
648+
649+ if require_predefined_shares :
650+ for attr in shared_attributes .keys ():
651+ if not hasattr (submenu , attr ):
652+ raise AttributeError ("The shared attribute '{attr}' is not defined in {cmd}. Either define {attr} "
653+ "in {cmd} or set require_predefined_shares=False."
654+ .format (cmd = submenu .__class__ .__name__ , attr = attr ))
655+
656+ self .shared_attributes = {} if shared_attributes is None else shared_attributes
657+ self .create_subclass = create_subclass
658+
659+ def __call__ (self , cmd_obj ):
660+ """Creates a subclass of Cmd wherein the given submenu can be accessed via the given command"""
661+ def enter_submenu (parent_cmd , line ):
662+ """
663+ This function will be bound to do_<submenu> and will change the scope of the CLI to that of the
664+ submenu.
665+ """
666+ submenu = self .submenu
667+ original_attributes = {attr : getattr (submenu , attr , AddSubmenu ._Nonexistent )
668+ for attr in self .shared_attributes .keys ()
669+ }
670+ try :
671+ # copy over any shared attributes
672+ for sub_attr , par_attr in self .shared_attributes .items ():
673+ setattr (submenu , sub_attr , getattr (parent_cmd , par_attr ))
674+
675+ if line .parsed .args :
676+ # Remove the menu argument and execute the command in the submenu
677+ line = submenu .parser_manager .parsed (line .parsed .args )
678+ submenu .precmd (line )
679+ ret = submenu .onecmd (line )
680+ submenu .postcmd (ret , line )
681+ else :
682+ history = _pop_readline_history ()
683+ if self .reformat_prompt is not None :
684+ prompt = submenu .prompt
685+ submenu .prompt = self .reformat_prompt .format (
686+ super_prompt = parent_cmd .prompt ,
687+ command = self .command ,
688+ sub_prompt = prompt ,
689+ )
690+ submenu .cmdloop ()
691+ if self .reformat_prompt is not None :
692+ self .submenu .prompt = prompt
693+ _push_readline_history (history )
694+ finally :
695+ # copy back original attributes
696+ for attr , value in original_attributes .items ():
697+ if attr is not AddSubmenu ._Nonexistent :
698+ setattr (submenu , attr , value )
699+ else :
700+ delattr (submenu , attr )
701+
702+ def complete_submenu (_self , text , line , begidx , endidx ):
703+ """
704+ This function will be bound to complete_<submenu> and will perform the complete commands of the submenu.
705+ """
706+ submenu = self .submenu
707+ original_attributes = {
708+ attr : getattr (submenu , attr , AddSubmenu ._Nonexistent )
709+ for attr in self .shared_attributes .keys ()
710+ }
711+ try :
712+ # copy over any shared attributes
713+ for sub_attr , par_attr in self .shared_attributes .items ():
714+ setattr (submenu , sub_attr , getattr (_self , par_attr ))
715+
716+ return _complete_from_cmd (submenu , text , line , begidx , endidx )
717+ finally :
718+ # copy back original attributes
719+ for attr , value in original_attributes .items ():
720+ if attr is not AddSubmenu ._Nonexistent :
721+ setattr (submenu , attr , value )
722+ else :
723+ delattr (submenu , attr )
724+
725+ original_do_help = cmd_obj .do_help
726+ original_complete_help = cmd_obj .complete_help
727+
728+ def help_submenu (_self , line ):
729+ """
730+ This function will be bound to help_<submenu> and will call the help commands of the submenu.
731+ """
732+ tokens = line .split (None , 1 )
733+ if tokens and (tokens [0 ] == self .command or tokens [0 ] in self .aliases ):
734+ self .submenu .do_help (tokens [1 ] if len (tokens ) == 2 else '' )
735+ else :
736+ original_do_help (_self , line )
737+
738+ def _complete_submenu_help (_self , text , line , begidx , endidx ):
739+ """autocomplete to match help_submenu()'s behavior"""
740+ tokens = line .split (None , 1 )
741+ if len (tokens ) == 2 and (
742+ not (not tokens [1 ].startswith (self .command ) and not any (
743+ tokens [1 ].startswith (alias ) for alias in self .aliases ))
744+ ):
745+ return self .submenu .complete_help (
746+ text ,
747+ tokens [1 ],
748+ begidx - line .index (tokens [1 ]),
749+ endidx - line .index (tokens [1 ]),
750+ )
751+ else :
752+ return original_complete_help (_self , text , line , begidx , endidx )
753+
754+ if self .create_subclass :
755+ class _Cmd (cmd_obj ):
756+ do_help = help_submenu
757+ complete_help = _complete_submenu_help
758+ else :
759+ _Cmd = cmd_obj
760+ _Cmd .do_help = help_submenu
761+ _Cmd .complete_help = _complete_submenu_help
762+
763+ # Create bindings in the parent command to the submenus commands.
764+ setattr (_Cmd , 'do_' + self .command , enter_submenu )
765+ setattr (_Cmd , 'complete_' + self .command , complete_submenu )
766+
767+ # Create additional bindings for aliases
768+ for _alias in self .aliases :
769+ setattr (_Cmd , 'do_' + _alias , enter_submenu )
770+ setattr (_Cmd , 'complete_' + _alias , complete_submenu )
771+ return _Cmd
772+
773+
550774class Cmd (cmd .Cmd ):
551775 """An easy but powerful framework for writing line-oriented command interpreters.
552776
@@ -1386,6 +1610,7 @@ def do_shortcuts(self, _):
13861610 def do_eof (self , _ ):
13871611 """Called when <Ctrl>-D is pressed."""
13881612 # End of script should not exit app, but <Ctrl>-D should.
1613+ print ('' ) # Required for clearing line when exiting submenu
13891614 return self ._STOP_AND_EXIT
13901615
13911616 def do_quit (self , _ ):
0 commit comments