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