1"""A generic class to build line-oriented command interpreters.
2
3Interpreters constructed with this class obey the following conventions:
4
51. End of file on input is processed as the command 'EOF'.
62. A command is parsed out of each line by collecting the prefix composed
7   of characters in the identchars member.
83. A command `foo' is dispatched to a method 'do_foo()'; the do_ method
9   is passed a single argument consisting of the remainder of the line.
104. Typing an empty line repeats the last command.  (Actually, it calls the
11   method `emptyline', which may be overridden in a subclass.)
125. There is a predefined `help' method.  Given an argument `topic', it
13   calls the command `help_topic'.  With no arguments, it lists all topics
14   with defined help_ functions, broken into up to three topics; documented
15   commands, miscellaneous help topics, and undocumented commands.
166. The command '?' is a synonym for `help'.  The command '!' is a synonym
17   for `shell', if a do_shell method exists.
187. If completion is enabled, completing commands will be done automatically,
19   and completing of commands args is done by calling complete_foo() with
20   arguments text, line, begidx, endidx.  text is string we are matching
21   against, all returned matches must begin with it.  line is the current
22   input line (lstripped), begidx and endidx are the beginning and end
23   indexes of the text being matched, which could be used to provide
24   different completion depending upon which position the argument is in.
25
26The `default' method may be overridden to intercept commands for which there
27is no do_ method.
28
29The `completedefault' method may be overridden to intercept completions for
30commands that have no complete_ method.
31
32The data member `self.ruler' sets the character used to draw separator lines
33in the help messages.  If empty, no ruler line is drawn.  It defaults to "=".
34
35If the value of `self.intro' is nonempty when the cmdloop method is called,
36it is printed out on interpreter startup.  This value may be overridden
37via an optional argument to the cmdloop() method.
38
39The data members `self.doc_header', `self.misc_header', and
40`self.undoc_header' set the headers used for the help function's
41listings of documented functions, miscellaneous topics, and undocumented
42functions respectively.
43
44These interpreters use raw_input; thus, if the readline module is loaded,
45they automatically support Emacs-like command history and editing features.
46"""
47
48import string
49
50__all__ = ["Cmd"]
51
52PROMPT = '(Cmd) '
53IDENTCHARS = string.ascii_letters + string.digits + '_'
54
55
56class Cmd:
57    """A simple framework for writing line-oriented command interpreters.
58
59    These are often useful for test harnesses, administrative tools, and
60    prototypes that will later be wrapped in a more sophisticated interface.
61
62    A Cmd instance or subclass instance is a line-oriented interpreter
63    framework.  There is no good reason to instantiate Cmd itself; rather,
64    it's useful as a superclass of an interpreter class you define yourself
65    in order to inherit Cmd's methods and encapsulate action methods.
66
67    """
68    prompt = PROMPT
69    identchars = IDENTCHARS
70    ruler = '='
71    lastcmd = ''
72    intro = None
73    doc_leader = ""
74    doc_header = "Documented commands (type help <topic>):"
75    misc_header = "Miscellaneous help topics:"
76    undoc_header = "Undocumented commands:"
77    nohelp = "*** No help on %s"
78    use_rawinput = 1
79
80    def __init__(self, completekey='tab', stdin=None, stdout=None):
81        """Instantiate a line-oriented interpreter framework.
82
83        The optional argument 'completekey' is the readline name of a
84        completion key; it defaults to the Tab key. If completekey is
85        not None and the readline module is available, command completion
86        is done automatically. The optional arguments stdin and stdout
87        specify alternate input and output file objects; if not specified,
88        sys.stdin and sys.stdout are used.
89
90        """
91        import sys
92        if stdin is not None:
93            self.stdin = stdin
94        else:
95            self.stdin = sys.stdin
96        if stdout is not None:
97            self.stdout = stdout
98        else:
99            self.stdout = sys.stdout
100        self.cmdqueue = []
101        self.completekey = completekey
102
103    def cmdloop(self, intro=None):
104        """Repeatedly issue a prompt, accept input, parse an initial prefix
105        off the received input, and dispatch to action methods, passing them
106        the remainder of the line as argument.
107
108        """
109
110        self.preloop()
111        if self.use_rawinput and self.completekey:
112            try:
113                import readline
114                self.old_completer = readline.get_completer()
115                readline.set_completer(self.complete)
116                readline.parse_and_bind(self.completekey+": complete")
117            except ImportError:
118                pass
119        try:
120            if intro is not None:
121                self.intro = intro
122            if self.intro:
123                self.stdout.write(str(self.intro)+"\n")
124            stop = None
125            while not stop:
126                if self.cmdqueue:
127                    line = self.cmdqueue.pop(0)
128                else:
129                    self.preinput()
130                    if self.use_rawinput:
131                        try:
132                            line = input(self.prompt)
133                        except EOFError:
134                            line = 'EOF'
135                    else:
136                        self.stdout.write(self.prompt)
137                        self.stdout.flush()
138                        line = self.stdin.readline()
139                        if not len(line):
140                            line = 'EOF'
141                        else:
142                            line = line[:-1]  # chop \n
143                    line = self.postinput(line)
144                line = self.precmd(line)
145                stop = self.onecmd(line)
146                stop = self.postcmd(stop, line)
147            self.postloop()
148        finally:
149            if self.use_rawinput and self.completekey:
150                try:
151                    import readline
152                    readline.set_completer(self.old_completer)
153                except ImportError:
154                    pass
155
156    def precmd(self, line):
157        """Hook method executed just before the command line is
158        interpreted, but after the input prompt is generated and issued.
159
160        """
161        return line
162
163    def postcmd(self, stop, line):
164        """Hook method executed just after a command dispatch is finished."""
165        return stop
166
167    def preinput(self):
168        """Hook method executed just before an input line is read."""
169
170    def postinput(self, line):
171        """Hook method executed just after an input line is read."""
172        return line
173
174    def preloop(self):
175        """Hook method executed once when the cmdloop() method is called."""
176        pass
177
178    def postloop(self):
179        """Hook method executed once when the cmdloop() method is about to
180        return.
181
182        """
183        pass
184
185    def parseline(self, line):
186        line = line.strip()
187        if not line:
188            return None, None, line
189        elif line[0] == '?':
190            line = 'help ' + line[1:]
191        elif line[0] == '!':
192            if hasattr(self, 'do_shell'):
193                line = 'shell ' + line[1:]
194            else:
195                return None, None, line
196        i, n = 0, len(line)
197        while i < n and line[i] in self.identchars:
198            i = i+1
199        cmd, arg = line[:i], line[i:].strip()
200        return cmd, arg, line
201
202    def onecmd(self, line):
203        """Interpret the argument as though it had been typed in response
204        to the prompt.
205
206        This may be overridden, but should not normally need to be;
207        see the precmd() and postcmd() methods for useful execution hooks.
208        The return value is a flag indicating whether interpretation of
209        commands by the interpreter should stop.
210
211        """
212        cmd, arg, line = self.parseline(line)
213        if not line:
214            return self.emptyline()
215        if cmd is None:
216            return self.default(line)
217        self.lastcmd = line
218        if cmd == '':
219            return self.default(line)
220        else:
221            try:
222                func = getattr(self, 'do_' + cmd)
223            except AttributeError:
224                return self.default(line)
225            return func(arg)
226
227    def emptyline(self):
228        """Called when an empty line is entered in response to the prompt.
229
230        If this method is not overridden, it repeats the last nonempty
231        command entered.
232
233        """
234        if self.lastcmd:
235            return self.onecmd(self.lastcmd)
236
237    def default(self, line):
238        """Called on an input line when the command prefix is not recognized.
239
240        If this method is not overridden, it prints an error message and
241        returns.
242
243        """
244        self.stdout.write('*** Unknown syntax: %s\n' % line)
245
246    def completedefault(self, *ignored):
247        """Method called to complete an input line when no command-specific
248        complete_*() method is available.
249
250        By default, it returns an empty list.
251
252        """
253        return []
254
255    def completenames(self, text, *ignored):
256        dotext = 'do_'+text
257        return [a[3:] for a in self.get_names() if a.startswith(dotext)]
258
259    def complete(self, text, state):
260        """Return the next possible completion for 'text'.
261
262        If a command has not been entered, then complete against command list.
263        Otherwise try to call complete_<command> to get list of completions.
264        """
265        if state == 0:
266            import readline
267            origline = readline.get_line_buffer()
268            line = origline.lstrip()
269            stripped = len(origline) - len(line)
270            begidx = readline.get_begidx() - stripped
271            endidx = readline.get_endidx() - stripped
272            if begidx > 0:
273                cmd, args, foo = self.parseline(line)
274                if cmd == '':
275                    compfunc = self.completedefault
276                else:
277                    try:
278                        compfunc = getattr(self, 'complete_' + cmd)
279                    except AttributeError:
280                        compfunc = self.completedefault
281            else:
282                compfunc = self.completenames
283            self.completion_matches = compfunc(text, line, begidx, endidx)
284        try:
285            return self.completion_matches[state]
286        except IndexError:
287            return None
288
289    def get_names(self):
290        # Inheritance says we have to look in class and
291        # base classes; order is not important.
292        names = []
293        classes = [self.__class__]
294        while classes:
295            aclass = classes.pop(0)
296            if aclass.__bases__:
297                classes = classes + list(aclass.__bases__)
298            names = names + dir(aclass)
299        return names
300
301    def complete_help(self, *args):
302        return self.completenames(*args)
303
304    def do_help(self, arg):
305        if arg:
306            # XXX check arg syntax
307            try:
308                func = getattr(self, 'help_' + arg)
309            except AttributeError:
310                try:
311                    doc = getattr(self, 'do_' + arg).__doc__
312                    if doc:
313                        self.stdout.write("%s\n" % str(doc))
314                        return
315                except AttributeError:
316                    pass
317                self.stdout.write("%s\n" % str(self.nohelp % (arg,)))
318                return
319            func()
320        else:
321            names = self.get_names()
322            cmds_doc = []
323            cmds_undoc = []
324            help = {}
325            for name in names:
326                if name[:5] == 'help_':
327                    help[name[5:]] = 1
328            names.sort()
329            # There can be duplicates if routines overridden
330            prevname = ''
331            for name in names:
332                if name[:3] == 'do_':
333                    if name == prevname:
334                        continue
335                    prevname = name
336                    cmd = name[3:]
337                    if cmd in help:
338                        cmds_doc.append(cmd)
339                        del help[cmd]
340                    elif getattr(self, name).__doc__:
341                        cmds_doc.append(cmd)
342                    else:
343                        cmds_undoc.append(cmd)
344            self.stdout.write("%s\n" % str(self.doc_leader))
345            self.print_topics(self.doc_header,   cmds_doc,   15, 80)
346            self.print_topics(self.misc_header,  list(help.keys()), 15, 80)
347            self.print_topics(self.undoc_header, cmds_undoc, 15, 80)
348
349    def print_topics(self, header, cmds, cmdlen, maxcol):
350        if cmds:
351            self.stdout.write("%s\n" % str(header))
352            if self.ruler:
353                self.stdout.write("%s\n" % str(self.ruler * len(header)))
354            self.columnize(cmds, maxcol-1)
355            self.stdout.write("\n")
356
357    def columnize(self, list, displaywidth=80):
358        """Display a list of strings as a compact set of columns.
359
360        Each column is only as wide as necessary.
361        Columns are separated by two spaces (one was not legible enough).
362        """
363        if not list:
364            self.stdout.write("<empty>\n")
365            return
366        nonstrings = [i for i in range(len(list))
367                      if not isinstance(list[i], str)]
368        if nonstrings:
369            raise TypeError("list[i] not a string for i in %s" %
370                            ", ".join(map(str, nonstrings)))
371        size = len(list)
372        if size == 1:
373            self.stdout.write('%s\n' % str(list[0]))
374            return
375        # Try every row count from 1 upwards
376        for nrows in range(1, len(list)):
377            ncols = (size+nrows-1) // nrows
378            colwidths = []
379            totwidth = -2
380            for col in range(ncols):
381                colwidth = 0
382                for row in range(nrows):
383                    i = row + nrows*col
384                    if i >= size:
385                        break
386                    x = list[i]
387                    colwidth = max(colwidth, len(x))
388                colwidths.append(colwidth)
389                totwidth += colwidth + 2
390                if totwidth > displaywidth:
391                    break
392            if totwidth <= displaywidth:
393                break
394        else:
395            nrows = len(list)
396            ncols = 1
397            colwidths = [0]
398        for row in range(nrows):
399            texts = []
400            for col in range(ncols):
401                i = row + nrows*col
402                if i >= size:
403                    x = ""
404                else:
405                    x = list[i]
406                texts.append(x)
407            while texts and not texts[-1]:
408                del texts[-1]
409            for col in range(len(texts)):
410                texts[col] = texts[col].ljust(colwidths[col])
411            self.stdout.write("%s\n" % str("  ".join(texts)))
412