13

I have a Cmd console set up to auto-complete card names for a Magic: the Gathering collection management system.

It uses the text parameter to query the database for cards, and uses the results to auto-complete/suggest cards.

However, these cards names have multiple words, and Cmd runs auto-completion from the last space to the end of the line.

For example:

mtgdb> add Mage<tab><tab>
Mage Slayer (Alara Reborn)     Magefire Wings (Alara Reborn)
mtgdb> add Mage S<tab><tab>
Sages of the Anima (Alara Reborn)
Sanctum Plowbeast (Alara Reborn)
Sangrite Backlash (Alara Reborn)
Sanity Gnawers (Alara Reborn)
Sen Triplets (Alara Reborn)
[...]
mtgdb> add Mage Sl<tab>
mtgdb> add Mage Slave of Bolas (Alara Reborn)

I tried manually grabbing what I wanted from the line parameter, which gets the results I want from the database, but this fails to overwrite the first word:

mtgdb> add Mage Sl<tab>
mtgdb> add Mage Mage Slayer (Alara Reborn)

In the end, I need the auto-completer to work like this:

mtgdb> add Mage Sl<tab>
mtgdb> add Mage Slayer (Alara Reborn)

Aside from the manual parsing attempt above, I also tried replacing spaces with plus signs, and discovered that Cmd is perfectly happy splitting on those as well. Replacing spaces with underscores works, but there is one card in Unhinged which is named _____, so I have to go through acrobatics to demunge the strings since I can't just line.replace("_", " ").

Here's some runnable test code:

import cmd

commands = [
    "foo",
    "foo bar blah",
    "bar",
    "bar baz blah",
    "baz",
    "baz foo blah"]

class Console(cmd.Cmd):
    intro = "Test console for" + \
            "http://stackoverflow.com/questions/4001708/\n" + \
            "Type \"cmd<space><tab><tab>\" to test " + \
            "auto-completion with spaces in commands\nwith " + \
            "similar beginings."

    def do_cmd(self, line):
        print(line)

    def complete_cmd(self, text, line, start_index, end_index):
        if text:
            return [command for command in commands
                    if command.startswith(text)]
        else:
            return commands

if __name__ == "__main__":
    command = Console()
    command.cmdloop()
Erik Youngren
  • 783
  • 1
  • 6
  • 15
  • 1
    There's some good information here: http://stackoverflow.com/questions/187621/how-to-make-a-python-command-line-program-autocomplete-arbitrary-things-not-int – Josh Oct 22 '10 at 23:51
  • can you provide runnable test code? That seems possible to fix – nosklo Oct 23 '10 at 02:22
  • Yeah, I saw that one, too, @offsound, it's where I got the idea to use Cmd. I'll whip up some test code. – Erik Youngren Oct 23 '10 at 05:01

3 Answers3

16

It shouldn't need to be overly complicated. Something like the following:

import cmd

completions = [
    'Mage Slayer (Alara Reborn)',
    'Magefire Wings (Alara Reborn)',
    'Sages of the Anima (Alara Reborn)',
    'Sanctum Plowbeast (Alara Reborn)',
    'Sangrite Backlash (Alara Reborn)',
    'Sanity Gnawers (Alara Reborn)',
    'Sen Triplets (Alara Reborn)'
]

class mycmd(cmd.Cmd):
    def __init__(self):
        cmd.Cmd.__init__(self)

    def do_quit(self, s):
        return True

    def do_add(self, s):
        pass

    def complete_add(self, text, line, begidx, endidx):
        mline = line.partition(' ')[2]
        offs = len(mline) - len(text)
        return [s[offs:] for s in completions if s.startswith(mline)]

if __name__ == '__main__':
    mycmd().cmdloop()
D Krueger
  • 2,446
  • 15
  • 12
  • 1
    I have use the same "trick" to allowed path completion. I swap the last line for return [fp[offs:] for fp in glob.glob(mline+'*')] – Julien Grenier Nov 05 '13 at 21:18
  • If you toss in a few .lower() calls, you can make your command completion case insensitive as well. `return [s[offs:] for s in completions if s.lower().startswith(mline.lower())]` – James Tomasino Apr 15 '15 at 16:11
0

I did override of the cmdloop function, and it was pretty straightforward. I didn't have to change anything else. Just copy the cmdloop function from the module (find code by doing import cmd, cmd.__file__), and add the two lines for changing delimiters:

    try:
       import readline
       self.old_completer = readline.get_completer()
       readline.set_completer(self.complete)
       readline.parse_and_bind(self.completekey+": complete")
       # do not use - as delimiter
       old_delims = readline.get_completer_delims() # <-
       readline.set_completer_delims(old_delims.replace('-', '')) # <-
    except ImportError:
        pass

That did it for me. In your case you may want to remove whichever delimiter is causing the issues.

Jay Medina
  • 544
  • 5
  • 12
0

You could do readline.set_completer_delims('').

However, your complete_* functions won't be called anymore; you will have to override Cmd.complete or Cmd.completenames. Look at the sourcecode of the cmd module for details.

adw
  • 4,901
  • 1
  • 25
  • 18
  • Blowing up the `complete_*` functions isn't something I want to do, but altering how they are dispatched is perfect. Not just `Cmd.complete`, but `Cmd.parseline` as well. I'll post what I change in the question after I figure it out. Thanks! – Erik Youngren Oct 23 '10 at 09:30
  • 1
    actually, this does it: `readline.set_completer_delims(' \t\n...@#$%^&*()-=+[{]}\\|;:\'",<>?')` – wesen Jan 30 '13 at 19:22