From 66b6e192b9d0437029f8fd7aa9f381bb4e9ed248 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Martin=20v=2E=20L=C3=B6wis?= <martin@v.loewis.de>
Date: Sat, 28 Jul 2001 14:44:03 +0000
Subject: Patch #416224: add readline completion to cmd.Cmd.

---
 Doc/lib/libcmd.tex | 24 ++++++++++++-
 Lib/cmd.py         | 99 +++++++++++++++++++++++++++++++++++++++++++++---------
 Lib/pstats.py      |  1 +
 Misc/NEWS          |  2 ++
 4 files changed, 110 insertions(+), 16 deletions(-)

diff --git a/Doc/lib/libcmd.tex b/Doc/lib/libcmd.tex
index 847aa20..e3be7b4 100644
--- a/Doc/lib/libcmd.tex
+++ b/Doc/lib/libcmd.tex
@@ -11,12 +11,18 @@ line-oriented command interpreters.  These are often useful for
 test harnesses, administrative tools, and prototypes that will
 later be wrapped in a more sophisticated interface.
 
-\begin{classdesc}{Cmd}{}
+\begin{classdesc}{Cmd}{\optional{completekey}}
 A \class{Cmd} instance or subclass instance is a line-oriented
 interpreter framework.  There is no good reason to instantiate
 \class{Cmd} itself; rather, it's useful as a superclass of an
 interpreter class you define yourself in order to inherit
 \class{Cmd}'s methods and encapsulate action methods.
+
+The optional argument is the \refmodule{readline} name of a completion
+key; it defaults to \code{``tab''}. If \var{completekey} is not
+\code{None} and \module{readline} is available, command completion is
+done automatically.
+
 \end{classdesc}
 
 \subsection{Cmd Objects}
@@ -47,6 +53,16 @@ the method \method{do_help()}.  As another special case, a line
 beginning with the character \character{!} is dispatched to the
 method \method{do_shell} (if such a method is defined).
 
+If completion is enabled, completing commands will be done
+automatically, and completing of commands args is done by calling
+\method{complete_foo()} with arguments \samp{text}, \samp{line},
+\samp{begidx}, \samp{endidx}.  \samp{text} is string we are matching
+against, all returned matches must begin with it.  \samp{line} is the
+current input line (lstripped), \samp{begidx} and \samp{endidx} are
+the beginning and end indexes of the text being matched, which could
+be used to provide different completion depending upon which position
+the argument is in.
+
 All subclasses of \class{Cmd} inherit a predefined \method{do_help}.
 This method, called with an argument \code{bar}, invokes the
 corresponding method \method{help_bar()}.  With no argument,
@@ -72,6 +88,12 @@ recognized. If this method is not overridden, it prints an
 error message and returns.
 \end{methoddesc}
 
+\begin{methoddesc}{completedefault}{text, line, begidx, endidx}
+Method called to complete an input line when no command-specific
+\code{complete_} method is available. By default, it returns an
+empty list.
+\end{methoddesc}
+
 \begin{methoddesc}{precmd}{}
 Hook method executed just before the command line is interpreted, but
 after the input prompt is generated and issued.  This
diff --git a/Lib/cmd.py b/Lib/cmd.py
index eacd498..423494a 100644
--- a/Lib/cmd.py
+++ b/Lib/cmd.py
@@ -15,10 +15,20 @@ Interpreters constructed with this class obey the following conventions:
    commands, miscellaneous help topics, and undocumented commands.
 6. The command '?' is a synonym for `help'.  The command '!' is a synonym
    for `shell', if a do_shell method exists.
+7. If completion is enabled, completing commands will be done automatically,
+   and completing of commands args is done by calling complete_foo() with
+   arguments text, line, begidx, endidx.  text is string we are matching
+   against, all returned matches must begin with it.  line is the current
+   input line (lstripped), begidx and endidx are the beginning and end
+   indexes of the text being matched, which could be used to provide 
+   different completion depending upon which position the argument is in.
 
 The `default' method may be overridden to intercept commands for which there
 is no do_ method.
 
+The `completedefault' method may be overridden to intercept completions for
+commands that have no complete_ method. 
+
 The data member `self.ruler' sets the character used to draw separator lines
 in the help messages.  If empty, no ruler line is drawn.  It defaults to "=".
 
@@ -56,7 +66,14 @@ class Cmd:
     nohelp = "*** No help on %s"
     use_rawinput = 1
 
-    def __init__(self): pass
+    def __init__(self, completekey='tab'): 
+        if completekey:
+            try:
+                import readline
+                readline.set_completer(self.complete)
+                readline.parse_and_bind(completekey+": complete")
+            except ImportError:
+                pass
 
     def cmdloop(self, intro=None):
         self.preloop()
@@ -99,21 +116,29 @@ class Cmd:
     def postloop(self):
         pass
 
-    def onecmd(self, line):
+    def parseline(self, line):
         line = line.strip()
         if not line:
-            return self.emptyline()
+            return None, None, line
         elif line[0] == '?':
             line = 'help ' + line[1:]
         elif line[0] == '!':
             if hasattr(self, 'do_shell'):
                 line = 'shell ' + line[1:]
             else:
-                return self.default(line)
-        self.lastcmd = line
+                return None, None, line
         i, n = 0, len(line)
         while i < n and line[i] in self.identchars: i = i+1
         cmd, arg = line[:i], line[i:].strip()
+        return cmd, arg, line
+    
+    def onecmd(self, line):
+        cmd, arg, line = self.parseline(line)
+        if not line:
+            return self.emptyline()
+        if cmd is None:
+            return self.default(line)
+        self.lastcmd = line
         if cmd == '':
             return self.default(line)
         else:
@@ -130,6 +155,59 @@ class Cmd:
     def default(self, line):
         print '*** Unknown syntax:', line
 
+    def completedefault(self, *ignored):
+        return []
+
+    def completenames(self, text, *ignored):
+        dotext = 'do_'+text
+        return [a[3:] for a in self.get_names() if a.startswith(dotext)]
+
+    def complete(self, text, state):
+        """Return the next possible completion for 'text'.
+
+        If a command has not been entered, then complete against command list.
+        Otherwise try to call complete_<command> to get list of completions.
+        """
+        if state == 0:
+            import readline
+            origline = readline.get_line_buffer()
+            line = origline.lstrip()
+            stripped = len(origline) - len(line)
+            begidx = readline.get_begidx() - stripped
+            endidx = readline.get_endidx() - stripped
+            if begidx>0:
+                cmd, args, foo = self.parseline(line)
+                if cmd == '':
+                    compfunc = self.completedefault
+                else:
+                    try:
+                        compfunc = getattr(self, 'complete_' + cmd)
+                    except AttributeError:
+                        compfunc = self.completedefault
+            else:
+                compfunc = self.completenames
+            self.completion_matches = compfunc(text, line, begidx, endidx)
+        try:
+            return self.completion_matches[state]
+        except IndexError:
+            return None
+    
+    def get_names(self):
+        # Inheritance says we have to look in class and
+        # base classes; order is not important.
+        names = []
+        classes = [self.__class__]
+        while classes:
+            aclass = classes[0]
+            if aclass.__bases__:
+                classes = classes + list(aclass.__bases__)
+            names = names + dir(aclass)
+            del classes[0]
+        return names
+
+    def complete_help(self, *args):
+        return self.completenames(*args)
+
     def do_help(self, arg):
         if arg:
             # XXX check arg syntax
@@ -147,16 +225,7 @@ class Cmd:
                 return
             func()
         else:
-            # Inheritance says we have to look in class and
-            # base classes; order is not important.
-            names = []
-            classes = [self.__class__]
-            while classes:
-                aclass = classes[0]
-                if aclass.__bases__:
-                    classes = classes + list(aclass.__bases__)
-                names = names + dir(aclass)
-                del classes[0]
+            names = self.get_names()
             cmds_doc = []
             cmds_undoc = []
             help = {}
diff --git a/Lib/pstats.py b/Lib/pstats.py
index c1183f9..20a6cd1 100644
--- a/Lib/pstats.py
+++ b/Lib/pstats.py
@@ -538,6 +538,7 @@ if __name__ == '__main__':
 
     class ProfileBrowser(cmd.Cmd):
         def __init__(self, profile=None):
+            cmd.Cmd.__init__(self)
             self.prompt = "% "
             if profile:
                 self.stats = Stats(profile)
diff --git a/Misc/NEWS b/Misc/NEWS
index 643564e..290d637 100644
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -14,6 +14,8 @@ Library
   value using the minimal quoting required for the value; more
   reliable than using xml.sax.saxutils.escape() for attribute values.
 
+- Readline completion support for cmd.Cmd was added.
+
 New platforms
 
 C API
-- 
cgit v0.12

