cmd.py 14.5 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
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.
"""
Guido van Rossum's avatar
Guido van Rossum committed
44

45
import string, sys
Guido van Rossum's avatar
Guido van Rossum committed
46

47 48
__all__ = ["Cmd"]

Guido van Rossum's avatar
Guido van Rossum committed
49
PROMPT = '(Cmd) '
50
IDENTCHARS = string.ascii_letters + string.digits + '_'
Guido van Rossum's avatar
Guido van Rossum committed
51 52

class Cmd:
53 54 55 56 57 58 59 60 61 62 63
    """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.

    """
64 65 66 67 68 69 70 71 72 73
    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"
74
    use_rawinput = 1
75

76
    def __init__(self, completekey='tab', stdin=None, stdout=None):
77 78
        """Instantiate a line-oriented interpreter framework.

Tim Peters's avatar
Tim Peters committed
79 80 81
        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
82 83 84
        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.
85 86

        """
87 88 89 90 91 92 93 94
        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
95
        self.cmdqueue = []
Michael W. Hudson's avatar
Michael W. Hudson committed
96
        self.completekey = completekey
97 98

    def cmdloop(self, intro=None):
99 100 101 102 103 104
        """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.

        """

105
        self.preloop()
Michael W. Hudson's avatar
Michael W. Hudson committed
106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122
        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)
123
                else:
Michael W. Hudson's avatar
Michael W. Hudson committed
124 125
                    if self.use_rawinput:
                        try:
126
                            line = input(self.prompt)
Michael W. Hudson's avatar
Michael W. Hudson committed
127 128
                        except EOFError:
                            line = 'EOF'
129
                    else:
Michael W. Hudson's avatar
Michael W. Hudson committed
130 131 132 133 134 135
                        self.stdout.write(self.prompt)
                        self.stdout.flush()
                        line = self.stdin.readline()
                        if not len(line):
                            line = 'EOF'
                        else:
136
                            line = line.rstrip('\r\n')
Michael W. Hudson's avatar
Michael W. Hudson committed
137 138 139 140 141 142 143 144 145 146 147
                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
148

149 150

    def precmd(self, line):
151 152 153 154
        """Hook method executed just before the command line is
        interpreted, but after the input prompt is generated and issued.

        """
155 156 157
        return line

    def postcmd(self, stop, line):
158
        """Hook method executed just after a command dispatch is finished."""
159 160 161
        return stop

    def preloop(self):
162
        """Hook method executed once when the cmdloop() method is called."""
Michael W. Hudson's avatar
Michael W. Hudson committed
163
        pass
164 165

    def postloop(self):
166 167 168 169
        """Hook method executed once when the cmdloop() method is about to
        return.

        """
Michael W. Hudson's avatar
Michael W. Hudson committed
170
        pass
Tim Peters's avatar
Tim Peters committed
171

172
    def parseline(self, line):
Andrew M. Kuchling's avatar
Andrew M. Kuchling committed
173 174 175 176
        """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.
        """
177
        line = line.strip()
178
        if not line:
179
            return None, None, line
180 181 182
        elif line[0] == '?':
            line = 'help ' + line[1:]
        elif line[0] == '!':
183
            if hasattr(self, 'do_shell'):
184
                line = 'shell ' + line[1:]
185
            else:
186
                return None, None, line
187 188
        i, n = 0, len(line)
        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
Tim Peters's avatar
Tim Peters committed
191

192
    def onecmd(self, line):
193 194 195 196 197 198 199 200 201
        """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.

        """
202 203 204 205 206 207
        cmd, arg, line = self.parseline(line)
        if not line:
            return self.emptyline()
        if cmd is None:
            return self.default(line)
        self.lastcmd = line
208 209
        if line == 'EOF' :
            self.lastcmd = ''
210 211 212 213 214 215 216 217 218 219
        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):
220 221 222 223 224 225
        """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.

        """
226 227 228 229
        if self.lastcmd:
            return self.onecmd(self.lastcmd)

    def default(self, line):
230 231 232 233 234 235
        """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.

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

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

        By default, it returns an empty list.

        """
245 246 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
        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
280

281
    def get_names(self):
282 283 284
        # This method used to pull in base class attributes
        # at a time dir() didn't do it yet.
        return dir(self.__class__)
285 286

    def complete_help(self, *args):
287 288 289 290
        commands = set(self.completenames(*args))
        topics = set(a[5:] for a in self.get_names()
                     if a.startswith('help_' + args[0]))
        return list(commands | topics)
291

292
    def do_help(self, arg):
293
        'List available commands with "help" or detailed help with "help cmd".'
294 295 296 297
        if arg:
            # XXX check arg syntax
            try:
                func = getattr(self, 'help_' + arg)
298
            except AttributeError:
299 300 301
                try:
                    doc=getattr(self, 'do_' + arg).__doc__
                    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 308 309
                return
            func()
        else:
310
            names = self.get_names()
311 312 313 314 315 316 317 318 319 320 321 322 323 324 325
            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:]
326
                    if cmd in help:
327 328 329 330 331 332
                        cmds_doc.append(cmd)
                        del help[cmd]
                    elif getattr(self, name).__doc__:
                        cmds_doc.append(cmd)
                    else:
                        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 337 338 339
            self.print_topics(self.undoc_header, cmds_undoc, 15,80)

    def print_topics(self, header, cmds, cmdlen, maxcol):
        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 347 348 349 350 351 352

    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:
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 360
            raise TypeError("list[i] not a string for i in %s"
                            % ", ".join(map(str, nonstrings)))
361 362
        size = len(list)
        if size == 1:
363
            self.stdout.write('%s\n'%str(list[0]))
364 365 366 367 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
            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])
401
            self.stdout.write("%s\n"%str("  ".join(texts)))