cmd.py 14.6 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
"""A generic class to build line-oriented command interpreters.

Interpreters constructed with this class obey the following conventions:

1. End of file on input is processed as the command 'EOF'.
2. A command is parsed out of each line by collecting the prefix composed
   of characters in the identchars member.
3. A command `foo' is dispatched to a method 'do_foo()'; the do_ method
   is passed a single argument consisting of the remainder of the line.
4. Typing an empty line repeats the last command.  (Actually, it calls the
   method `emptyline', which may be overridden in a subclass.)
5. There is a predefined `help' method.  Given an argument `topic', it
   calls the command `help_topic'.  With no arguments, it lists all topics
   with defined help_ functions, broken into up to three topics; documented
   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.
18 19 20 21 22
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
Tim Peters's avatar
Tim Peters committed
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 26 27 28

The `default' method may be overridden to intercept commands for which there
is no do_ method.

29
The `completedefault' method may be overridden to intercept completions for
Tim Peters's avatar
Tim Peters committed
30
commands that have no complete_ method.
31

32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
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 "=".

If the value of `self.intro' is nonempty when the cmdloop method is called,
it is printed out on interpreter startup.  This value may be overridden
via an optional argument to the cmdloop() method.

The data members `self.doc_header', `self.misc_header', and
`self.undoc_header' set the headers used for the help function's
listings of documented functions, miscellaneous topics, and undocumented
functions respectively.

These interpreters use raw_input; thus, if the readline module is loaded,
they automatically support Emacs-like command history and editing features.
"""
Guido van Rossum's avatar
Guido van Rossum committed
47

48
import string
Guido van Rossum's avatar
Guido van Rossum committed
49

50 51
__all__ = ["Cmd"]

Guido van Rossum's avatar
Guido van Rossum committed
52
PROMPT = '(Cmd) '
53
IDENTCHARS = string.ascii_letters + string.digits + '_'
Guido van Rossum's avatar
Guido van Rossum committed
54 55

class Cmd:
56 57 58 59 60 61 62 63 64 65 66
    """A simple framework for writing 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.

    A Cmd instance or subclass instance is a line-oriented interpreter
    framework.  There is no good reason to instantiate Cmd itself; rather,
    it's useful as a superclass of an interpreter class you define yourself
    in order to inherit Cmd's methods and encapsulate action methods.

    """
67 68 69 70 71 72 73 74 75 76
    prompt = PROMPT
    identchars = IDENTCHARS
    ruler = '='
    lastcmd = ''
    intro = None
    doc_leader = ""
    doc_header = "Documented commands (type help <topic>):"
    misc_header = "Miscellaneous help topics:"
    undoc_header = "Undocumented commands:"
    nohelp = "*** No help on %s"
77
    use_rawinput = 1
78

79
    def __init__(self, completekey='tab', stdin=None, stdout=None):
80 81
        """Instantiate a line-oriented interpreter framework.

Tim Peters's avatar
Tim Peters committed
82 83 84
        The optional argument 'completekey' is the readline name of a
        completion key; it defaults to the Tab key. If completekey is
        not None and the readline module is available, command completion
85 86 87
        is done automatically. The optional arguments stdin and stdout
        specify alternate input and output file objects; if not specified,
        sys.stdin and sys.stdout are used.
88 89

        """
90 91 92 93 94 95 96 97 98
        import sys
        if stdin is not None:
            self.stdin = stdin
        else:
            self.stdin = sys.stdin
        if stdout is not None:
            self.stdout = stdout
        else:
            self.stdout = sys.stdout
99
        self.cmdqueue = []
Michael W. Hudson's avatar
Michael W. Hudson committed
100
        self.completekey = completekey
101 102

    def cmdloop(self, intro=None):
103 104 105 106 107 108
        """Repeatedly issue a prompt, accept input, parse an initial prefix
        off the received input, and dispatch to action methods, passing them
        the remainder of the line as argument.

        """

109
        self.preloop()
Michael W. Hudson's avatar
Michael W. Hudson committed
110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126
        if self.use_rawinput and self.completekey:
            try:
                import readline
                self.old_completer = readline.get_completer()
                readline.set_completer(self.complete)
                readline.parse_and_bind(self.completekey+": complete")
            except ImportError:
                pass
        try:
            if intro is not None:
                self.intro = intro
            if self.intro:
                self.stdout.write(str(self.intro)+"\n")
            stop = None
            while not stop:
                if self.cmdqueue:
                    line = self.cmdqueue.pop(0)
127
                else:
Michael W. Hudson's avatar
Michael W. Hudson committed
128 129 130 131 132
                    if self.use_rawinput:
                        try:
                            line = raw_input(self.prompt)
                        except EOFError:
                            line = 'EOF'
133
                    else:
Michael W. Hudson's avatar
Michael W. Hudson committed
134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151
                        self.stdout.write(self.prompt)
                        self.stdout.flush()
                        line = self.stdin.readline()
                        if not len(line):
                            line = 'EOF'
                        else:
                            line = line[:-1] # chop \n
                line = self.precmd(line)
                stop = self.onecmd(line)
                stop = self.postcmd(stop, line)
            self.postloop()
        finally:
            if self.use_rawinput and self.completekey:
                try:
                    import readline
                    readline.set_completer(self.old_completer)
                except ImportError:
                    pass
Tim Peters's avatar
Tim Peters committed
152

153 154

    def precmd(self, line):
155 156 157 158
        """Hook method executed just before the command line is
        interpreted, but after the input prompt is generated and issued.

        """
159 160 161
        return line

    def postcmd(self, stop, line):
162
        """Hook method executed just after a command dispatch is finished."""
163 164 165
        return stop

    def preloop(self):
166
        """Hook method executed once when the cmdloop() method is called."""
Michael W. Hudson's avatar
Michael W. Hudson committed
167
        pass
168 169

    def postloop(self):
170 171 172 173
        """Hook method executed once when the cmdloop() method is about to
        return.

        """
Michael W. Hudson's avatar
Michael W. Hudson committed
174
        pass
Tim Peters's avatar
Tim Peters committed
175

176
    def parseline(self, line):
Andrew M. Kuchling's avatar
Andrew M. Kuchling committed
177 178 179 180
        """Parse the line into a command name and a string containing
        the arguments.  Returns a tuple containing (command, args, line).
        'command' and 'args' may be None if the line couldn't be parsed.
        """
181
        line = line.strip()
182
        if not line:
183
            return None, None, line
184 185 186
        elif line[0] == '?':
            line = 'help ' + line[1:]
        elif line[0] == '!':
187
            if hasattr(self, 'do_shell'):
188
                line = 'shell ' + line[1:]
189
            else:
190
                return None, None, line
191 192
        i, n = 0, len(line)
        while i < n and line[i] in self.identchars: i = i+1
193
        cmd, arg = line[:i], line[i:].strip()
194
        return cmd, arg, line
Tim Peters's avatar
Tim Peters committed
195

196
    def onecmd(self, line):
197 198 199 200 201 202 203 204 205
        """Interpret the argument as though it had been typed in response
        to the prompt.

        This may be overridden, but should not normally need to be;
        see the precmd() and postcmd() methods for useful execution hooks.
        The return value is a flag indicating whether interpretation of
        commands by the interpreter should stop.

        """
206 207 208 209 210 211
        cmd, arg, line = self.parseline(line)
        if not line:
            return self.emptyline()
        if cmd is None:
            return self.default(line)
        self.lastcmd = line
212 213 214 215 216 217 218 219 220 221
        if cmd == '':
            return self.default(line)
        else:
            try:
                func = getattr(self, 'do_' + cmd)
            except AttributeError:
                return self.default(line)
            return func(arg)

    def emptyline(self):
222 223 224 225 226 227
        """Called when an empty line is entered in response to the prompt.

        If this method is not overridden, it repeats the last nonempty
        command entered.

        """
228 229 230 231
        if self.lastcmd:
            return self.onecmd(self.lastcmd)

    def default(self, line):
232 233 234 235 236 237
        """Called on an input line when the command prefix is not recognized.

        If this method is not overridden, it prints an error message and
        returns.

        """
238
        self.stdout.write('*** Unknown syntax: %s\n'%line)
239

240
    def completedefault(self, *ignored):
241 242 243 244 245 246
        """Method called to complete an input line when no command-specific
        complete_*() method is available.

        By default, it returns an empty list.

        """
247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281
        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
Tim Peters's avatar
Tim Peters committed
282

283 284 285 286 287 288
    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:
289
            aclass = classes.pop(0)
290 291 292 293 294 295 296 297
            if aclass.__bases__:
                classes = classes + list(aclass.__bases__)
            names = names + dir(aclass)
        return names

    def complete_help(self, *args):
        return self.completenames(*args)

298 299 300 301 302
    def do_help(self, arg):
        if arg:
            # XXX check arg syntax
            try:
                func = getattr(self, 'help_' + arg)
303
            except AttributeError:
304 305 306
                try:
                    doc=getattr(self, 'do_' + arg).__doc__
                    if doc:
307
                        self.stdout.write("%s\n"%str(doc))
308
                        return
309
                except AttributeError:
310
                    pass
311
                self.stdout.write("%s\n"%str(self.nohelp % (arg,)))
312 313 314
                return
            func()
        else:
315
            names = self.get_names()
316 317 318 319 320 321 322 323 324 325 326 327 328 329 330
            cmds_doc = []
            cmds_undoc = []
            help = {}
            for name in names:
                if name[:5] == 'help_':
                    help[name[5:]]=1
            names.sort()
            # There can be duplicates if routines overridden
            prevname = ''
            for name in names:
                if name[:3] == 'do_':
                    if name == prevname:
                        continue
                    prevname = name
                    cmd=name[3:]
331
                    if cmd in help:
332 333 334 335 336 337
                        cmds_doc.append(cmd)
                        del help[cmd]
                    elif getattr(self, name).__doc__:
                        cmds_doc.append(cmd)
                    else:
                        cmds_undoc.append(cmd)
338
            self.stdout.write("%s\n"%str(self.doc_leader))
339 340 341 342 343 344
            self.print_topics(self.doc_header,   cmds_doc,   15,80)
            self.print_topics(self.misc_header,  help.keys(),15,80)
            self.print_topics(self.undoc_header, cmds_undoc, 15,80)

    def print_topics(self, header, cmds, cmdlen, maxcol):
        if cmds:
345
            self.stdout.write("%s\n"%str(header))
346
            if self.ruler:
347
                self.stdout.write("%s\n"%str(self.ruler * len(header)))
348
            self.columnize(cmds, maxcol-1)
349
            self.stdout.write("\n")
350 351 352 353 354 355 356 357

    def columnize(self, list, displaywidth=80):
        """Display a list of strings as a compact set of columns.

        Each column is only as wide as necessary.
        Columns are separated by two spaces (one was not legible enough).
        """
        if not list:
358
            self.stdout.write("<empty>\n")
359 360 361 362 363 364 365 366
            return
        nonstrings = [i for i in range(len(list))
                        if not isinstance(list[i], str)]
        if nonstrings:
            raise TypeError, ("list[i] not a string for i in %s" %
                              ", ".join(map(str, nonstrings)))
        size = len(list)
        if size == 1:
367
            self.stdout.write('%s\n'%str(list[0]))
368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404
            return
        # Try every row count from 1 upwards
        for nrows in range(1, len(list)):
            ncols = (size+nrows-1) // nrows
            colwidths = []
            totwidth = -2
            for col in range(ncols):
                colwidth = 0
                for row in range(nrows):
                    i = row + nrows*col
                    if i >= size:
                        break
                    x = list[i]
                    colwidth = max(colwidth, len(x))
                colwidths.append(colwidth)
                totwidth += colwidth + 2
                if totwidth > displaywidth:
                    break
            if totwidth <= displaywidth:
                break
        else:
            nrows = len(list)
            ncols = 1
            colwidths = [0]
        for row in range(nrows):
            texts = []
            for col in range(ncols):
                i = row + nrows*col
                if i >= size:
                    x = ""
                else:
                    x = list[i]
                texts.append(x)
            while texts and not texts[-1]:
                del texts[-1]
            for col in range(len(texts)):
                texts[col] = texts[col].ljust(colwidths[col])
405
            self.stdout.write("%s\n"%str("  ".join(texts)))