Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python readline, tab completion cycling with the Cmd interface

I am using the cmd.Cmd class in Python to offer a simple readline interface to my program.

Self contained example:

from cmd import Cmd

class CommandParser(Cmd):

    def do_x(self, line):
        pass

    def do_xy(self, line):
        pass

    def do_xyz(self, line):
        pass

if __name__ == "__main__":
    parser = CommandParser()
    parser.cmdloop()

Pressing tab twice will show possibilities. Pressing tab again does the same.

My question is, how do I get the options to cycle on the third tab press? In readline terms I think this is called Tab: menu-complete, but I can't see how to apply this to a Cmd instance.

I already tried:

readline.parse_and_bind('Tab: menu-complete')

Both before and after instantiating the parser instance. No luck.

I also tried passing "Tab: menu-complete" to the Cmd constructor. No Luck here either.

Anyone know how it's done?

Cheers!

like image 216
Edd Barrett Avatar asked Oct 31 '22 19:10

Edd Barrett


2 Answers

Unfortunately, it seems as though the only way around it is to monkey-patch the method cmdloop from the cmd.Cmd class, or roll your own.

The right approach is to use "Tab: menu-complete", but that's overriden by the class as shown in line 115: readline.parse_and_bind(self.completekey+": complete"), it is never activated. (For line 115, and the entire cmd package, see this: https://hg.python.org/cpython/file/2.7/Lib/cmd.py). I've shown an edited version of that function below, and how to use it:

import cmd


# note: taken from Python's library: https://hg.python.org/cpython/file/2.7/Lib/cmd.py
def cmdloop(self, intro=None):
    """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.
    """

    self.preloop()
    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+": menu-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)
            else:
                if self.use_rawinput:
                    try:
                        line = raw_input(self.prompt)
                    except EOFError:
                        line = 'EOF'
                else:
                    self.stdout.write(self.prompt)
                    self.stdout.flush()
                    line = self.stdin.readline()
                    if not len(line):
                        line = 'EOF'
                    else:
                        line = line.rstrip('\r\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

# monkey-patch - make sure this is done before any sort of inheritance is used!
cmd.Cmd.cmdloop = cmdloop

# inheritance of the class with the active monkey-patched `cmdloop`
class MyCmd(cmd.Cmd):
    pass

Once you've monkey-patched the class method, (or implemented your own class), it provides the correct behavior (albeit without highlighting and reverse-tabbing, but these can be implemented with other keys as necessary).

like image 193
jrd1 Avatar answered Nov 15 '22 05:11

jrd1


The easiest trick would be to add a space after menu-complete:

parser = CommandParser(completekey="tab: menu-complete ")

The bind expression that is executed

readline.parse_and_bind(self.completekey+": complete")

will then become

readline.parse_and_bind("tab: menu-complete : complete")

Everything after the second space is acutally ignored, so it's the same as tab: menu-complete.

If you don't want to rely on that behaviour of readline parsing (I haven't seen it documented) you could use a subclass of str that refuses to be extended as completekey:

class stubborn_str(str):
    def __add__(self, other):
        return self

parser = CommandParser(completekey=stubborn_str("tab: menu-complete"))

self.completekey+": complete" is now the same as self.completekey.

like image 25
mata Avatar answered Nov 15 '22 04:11

mata