• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 inspect, 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                if readline.backend == "editline":
112                    if self.completekey == 'tab':
113                        # libedit uses "^I" instead of "tab"
114                        command_string = "bind ^I rl_complete"
115                    else:
116                        command_string = f"bind {self.completekey} rl_complete"
117                else:
118                    command_string = f"{self.completekey}: complete"
119                readline.parse_and_bind(command_string)
120            except ImportError:
121                pass
122        try:
123            if intro is not None:
124                self.intro = intro
125            if self.intro:
126                self.stdout.write(str(self.intro)+"\n")
127            stop = None
128            while not stop:
129                if self.cmdqueue:
130                    line = self.cmdqueue.pop(0)
131                else:
132                    if self.use_rawinput:
133                        try:
134                            line = input(self.prompt)
135                        except EOFError:
136                            line = 'EOF'
137                    else:
138                        self.stdout.write(self.prompt)
139                        self.stdout.flush()
140                        line = self.stdin.readline()
141                        if not len(line):
142                            line = 'EOF'
143                        else:
144                            line = line.rstrip('\r\n')
145                line = self.precmd(line)
146                stop = self.onecmd(line)
147                stop = self.postcmd(stop, line)
148            self.postloop()
149        finally:
150            if self.use_rawinput and self.completekey:
151                try:
152                    import readline
153                    readline.set_completer(self.old_completer)
154                except ImportError:
155                    pass
156
157
158    def precmd(self, line):
159        """Hook method executed just before the command line is
160        interpreted, but after the input prompt is generated and issued.
161
162        """
163        return line
164
165    def postcmd(self, stop, line):
166        """Hook method executed just after a command dispatch is finished."""
167        return stop
168
169    def preloop(self):
170        """Hook method executed once when the cmdloop() method is called."""
171        pass
172
173    def postloop(self):
174        """Hook method executed once when the cmdloop() method is about to
175        return.
176
177        """
178        pass
179
180    def parseline(self, line):
181        """Parse the line into a command name and a string containing
182        the arguments.  Returns a tuple containing (command, args, line).
183        'command' and 'args' may be None if the line couldn't be parsed.
184        """
185        line = line.strip()
186        if not line:
187            return None, None, line
188        elif line[0] == '?':
189            line = 'help ' + line[1:]
190        elif line[0] == '!':
191            if hasattr(self, 'do_shell'):
192                line = 'shell ' + line[1:]
193            else:
194                return None, None, line
195        i, n = 0, len(line)
196        while i < n and line[i] in self.identchars: i = i+1
197        cmd, arg = line[:i], line[i:].strip()
198        return cmd, arg, line
199
200    def onecmd(self, line):
201        """Interpret the argument as though it had been typed in response
202        to the prompt.
203
204        This may be overridden, but should not normally need to be;
205        see the precmd() and postcmd() methods for useful execution hooks.
206        The return value is a flag indicating whether interpretation of
207        commands by the interpreter should stop.
208
209        """
210        cmd, arg, line = self.parseline(line)
211        if not line:
212            return self.emptyline()
213        if cmd is None:
214            return self.default(line)
215        self.lastcmd = line
216        if line == 'EOF' :
217            self.lastcmd = ''
218        if cmd == '':
219            return self.default(line)
220        else:
221            func = getattr(self, 'do_' + cmd, None)
222            if func is None:
223                return self.default(line)
224            return func(arg)
225
226    def emptyline(self):
227        """Called when an empty line is entered in response to the prompt.
228
229        If this method is not overridden, it repeats the last nonempty
230        command entered.
231
232        """
233        if self.lastcmd:
234            return self.onecmd(self.lastcmd)
235
236    def default(self, line):
237        """Called on an input line when the command prefix is not recognized.
238
239        If this method is not overridden, it prints an error message and
240        returns.
241
242        """
243        self.stdout.write('*** Unknown syntax: %s\n'%line)
244
245    def completedefault(self, *ignored):
246        """Method called to complete an input line when no command-specific
247        complete_*() method is available.
248
249        By default, it returns an empty list.
250
251        """
252        return []
253
254    def completenames(self, text, *ignored):
255        dotext = 'do_'+text
256        return [a[3:] for a in self.get_names() if a.startswith(dotext)]
257
258    def complete(self, text, state):
259        """Return the next possible completion for 'text'.
260
261        If a command has not been entered, then complete against command list.
262        Otherwise try to call complete_<command> to get list of completions.
263        """
264        if state == 0:
265            import readline
266            origline = readline.get_line_buffer()
267            line = origline.lstrip()
268            stripped = len(origline) - len(line)
269            begidx = readline.get_begidx() - stripped
270            endidx = readline.get_endidx() - stripped
271            if begidx>0:
272                cmd, args, foo = self.parseline(line)
273                if cmd == '':
274                    compfunc = self.completedefault
275                else:
276                    try:
277                        compfunc = getattr(self, 'complete_' + cmd)
278                    except AttributeError:
279                        compfunc = self.completedefault
280            else:
281                compfunc = self.completenames
282            self.completion_matches = compfunc(text, line, begidx, endidx)
283        try:
284            return self.completion_matches[state]
285        except IndexError:
286            return None
287
288    def get_names(self):
289        # This method used to pull in base class attributes
290        # at a time dir() didn't do it yet.
291        return dir(self.__class__)
292
293    def complete_help(self, *args):
294        commands = set(self.completenames(*args))
295        topics = set(a[5:] for a in self.get_names()
296                     if a.startswith('help_' + args[0]))
297        return list(commands | topics)
298
299    def do_help(self, arg):
300        'List available commands with "help" or detailed help with "help cmd".'
301        if arg:
302            # XXX check arg syntax
303            try:
304                func = getattr(self, 'help_' + arg)
305            except AttributeError:
306                try:
307                    doc=getattr(self, 'do_' + arg).__doc__
308                    doc = inspect.cleandoc(doc)
309                    if doc:
310                        self.stdout.write("%s\n"%str(doc))
311                        return
312                except AttributeError:
313                    pass
314                self.stdout.write("%s\n"%str(self.nohelp % (arg,)))
315                return
316            func()
317        else:
318            names = self.get_names()
319            cmds_doc = []
320            cmds_undoc = []
321            topics = set()
322            for name in names:
323                if name[:5] == 'help_':
324                    topics.add(name[5:])
325            names.sort()
326            # There can be duplicates if routines overridden
327            prevname = ''
328            for name in names:
329                if name[:3] == 'do_':
330                    if name == prevname:
331                        continue
332                    prevname = name
333                    cmd=name[3:]
334                    if cmd in topics:
335                        cmds_doc.append(cmd)
336                        topics.remove(cmd)
337                    elif getattr(self, name).__doc__:
338                        cmds_doc.append(cmd)
339                    else:
340                        cmds_undoc.append(cmd)
341            self.stdout.write("%s\n"%str(self.doc_leader))
342            self.print_topics(self.doc_header,   cmds_doc,   15,80)
343            self.print_topics(self.misc_header,  sorted(topics),15,80)
344            self.print_topics(self.undoc_header, cmds_undoc, 15,80)
345
346    def print_topics(self, header, cmds, cmdlen, maxcol):
347        if cmds:
348            self.stdout.write("%s\n"%str(header))
349            if self.ruler:
350                self.stdout.write("%s\n"%str(self.ruler * len(header)))
351            self.columnize(cmds, maxcol-1)
352            self.stdout.write("\n")
353
354    def columnize(self, list, displaywidth=80):
355        """Display a list of strings as a compact set of columns.
356
357        Each column is only as wide as necessary.
358        Columns are separated by two spaces (one was not legible enough).
359        """
360        if not list:
361            self.stdout.write("<empty>\n")
362            return
363
364        nonstrings = [i for i in range(len(list))
365                        if not isinstance(list[i], str)]
366        if nonstrings:
367            raise TypeError("list[i] not a string for i in %s"
368                            % ", ".join(map(str, nonstrings)))
369        size = len(list)
370        if size == 1:
371            self.stdout.write('%s\n'%str(list[0]))
372            return
373        # Try every row count from 1 upwards
374        for nrows in range(1, len(list)):
375            ncols = (size+nrows-1) // nrows
376            colwidths = []
377            totwidth = -2
378            for col in range(ncols):
379                colwidth = 0
380                for row in range(nrows):
381                    i = row + nrows*col
382                    if i >= size:
383                        break
384                    x = list[i]
385                    colwidth = max(colwidth, len(x))
386                colwidths.append(colwidth)
387                totwidth += colwidth + 2
388                if totwidth > displaywidth:
389                    break
390            if totwidth <= displaywidth:
391                break
392        else:
393            nrows = len(list)
394            ncols = 1
395            colwidths = [0]
396        for row in range(nrows):
397            texts = []
398            for col in range(ncols):
399                i = row + nrows*col
400                if i >= size:
401                    x = ""
402                else:
403                    x = list[i]
404                texts.append(x)
405            while texts and not texts[-1]:
406                del texts[-1]
407            for col in range(len(texts)):
408                texts[col] = texts[col].ljust(colwidths[col])
409            self.stdout.write("%s\n"%str("  ".join(texts)))
410