123

I am aware of how to setup autocompletion of python objects in the python interpreter (on unix).

  • Google shows many hits for explanations on how to do this.
  • Unfortunately, there are so many references to that it is difficult to find what I need to do, which is slightly different.

I need to know how to enable, tab/auto completion of arbitrary items in a command-line program written in python.

My specific use case is a command-line python program that needs to send emails. I want to be able to autocomplete email addresses (I have the addresses on disk) when the user types part of it (and optionally presses the TAB key).

I do not need it to work on windows or mac, just linux.

Paul D. Eden
  • 19,939
  • 18
  • 59
  • 63
  • 1
    This [blog](http://blog.e-shell.org/221) should do the tricks with config the .pythonrc file. – LF00 Jun 10 '17 at 01:53

9 Answers9

83

Use Python's readline bindings. For example,

import readline

def completer(text, state):
    options = [i for i in commands if i.startswith(text)]
    if state < len(options):
        return options[state]
    else:
        return None

readline.parse_and_bind("tab: complete")
readline.set_completer(completer)

The official module docs aren't much more detailed, see the readline docs for more info.

sneeu
  • 2,602
  • 3
  • 24
  • 27
ephemient
  • 198,619
  • 38
  • 280
  • 391
  • 2
    note tough that if you write your command line with the cmd module that there are better ways to do it. – Florian Bösch Oct 09 '08 at 15:17
  • Note that readline doesn't work on Windows. pyreadline seems to be a drop in replacement though, but it doesn't work with linux. – Samuel Dec 23 '20 at 15:33
  • 2
    You could just put a conditional at the start of your python file that checks the os and imports either `readline` or `pyreadline`. – Paul Rooney Feb 15 '21 at 02:27
  • @Samuel With `pyreadline` you use the same code snippet and just replace `readline` with `pyreadline`? Doesn't seem to work – yem May 24 '22 at 06:22
  • On Windows 11: `AttributeError: module 'pyreadline' has no attribute 'parse_and_bind'` – yem May 24 '22 at 06:29
  • Try readline = pyreadline.Readline(); readline.parse_and_bind("tab: complete");. – Samuel May 25 '22 at 13:28
65

Follow the cmd documentation and you'll be fine

import cmd

addresses = [
    'here@blubb.com',
    'foo@bar.com',
    'whatever@wherever.org',
]

class MyCmd(cmd.Cmd):
    def do_send(self, line):
        pass

    def complete_send(self, text, line, start_index, end_index):
        if text:
            return [
                address for address in addresses
                if address.startswith(text)
            ]
        else:
            return addresses


if __name__ == '__main__':
    my_cmd = MyCmd()
    my_cmd.cmdloop()

Output for tab -> tab -> send -> tab -> tab -> f -> tab

(Cmd)
help  send
(Cmd) send
foo@bar.com            here@blubb.com         whatever@wherever.org
(Cmd) send foo@bar.com
(Cmd)
Florian Bösch
  • 27,420
  • 11
  • 48
  • 53
  • Is there any way to control how readline columnizes it's output? So lets say i'd want it to columnize with two spaces between each item. – Fnord Nov 24 '13 at 15:55
  • 2
    When I run this code, tabs are simply printed into the command line. In fact this is true regardless of if I use cmd or straight readline. – Hack Saw Sep 16 '16 at 04:01
  • I see a lot of readline references in the cmd documentation. Does this work in a Windows shell? – Samuel Dec 23 '20 at 15:35
  • 1
    @Hack Saw, I hit this problem on MacOS: the bundled `readline` isn't the same as what's on Linux, so I switched to Linux (a Raspberry Pi) and it worked immediately. There's something called `gnureadline` which might be worth looking at. – Gordon Fogus Feb 05 '22 at 05:55
49

Since you say "NOT interpreter" in your question, I guess you don't want answers involving python readline and suchlike. (edit: in hindsight, that's obviously not the case. Ho hum. I think this info is interesting anyway, so I'll leave it here.)

I think you might be after this.

It's about adding shell-level completion to arbitrary commands, extending bash's own tab-completion.

In a nutshell, you'll create a file containing a shell-function that will generate possible completions, save it into /etc/bash_completion.d/ and register it with the command complete. Here's a snippet from the linked page:

_foo() 
{
    local cur prev opts
    COMPREPLY=()
    cur="${COMP_WORDS[COMP_CWORD]}"
    prev="${COMP_WORDS[COMP_CWORD-1]}"
    opts="--help --verbose --version"

    if [[ ${cur} == -* ]] ; then
        COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
        return 0
    fi
}
complete -F _foo foo

In this case, the typing foo --[TAB] will give you the values in the variable opts, i.e. --help, --verbose and --version. For your purposes, you'll essentially want to customise the values that are put into opts.

Do have a look at the example on the linked page, it's all pretty straightforward.

mac
  • 42,153
  • 26
  • 121
  • 131
Owen
  • 891
  • 6
  • 6
39

I am surprised that nobody has mentioned argcomplete, here is an example from the docs:

from argcomplete.completers import ChoicesCompleter

parser.add_argument("--protocol", choices=('http', 'https', 'ssh', 'rsync', 'wss'))
parser.add_argument("--proto").completer=ChoicesCompleter(('http', 'https', 'ssh', 'rsync', 'wss'))
qed
  • 22,298
  • 21
  • 125
  • 196
16

Here is a full-working version of the code that was very supplied by ephemient here (thank you).

import readline

addrs = ['angela@domain.com', 'michael@domain.com', 'david@test.com']

def completer(text, state):
    options = [x for x in addrs if x.startswith(text)]
    try:
        return options[state]
    except IndexError:
        return None

readline.set_completer(completer)
readline.parse_and_bind("tab: complete")

while 1:
    a = raw_input("> ")
    print "You entered", a
Community
  • 1
  • 1
Paul D. Eden
  • 19,939
  • 18
  • 59
  • 63
  • 1
    Note that readline doesn't work on Windows, at least not in Python 2.7.18. pyreadline seems to be a mostly drop in replacement though, except you have to replace raw_input() with readline.readline(), – Samuel Dec 23 '20 at 15:38
13
# ~/.pythonrc
import rlcompleter, readline
readline.parse_and_bind('tab:complete')

# ~/.bashrc
export PYTHONSTARTUP=~/.pythonrc
user178047
  • 1,264
  • 15
  • 20
12

You can try using the Python Prompt Toolkit, a library for building interactive command line applications in Python.

The library makes it easy to add interactive autocomplete functionality, allowing the user to use the Tab key to visually cycle through the available choices. The library is cross-platform (Linux, OS X, FreeBSD, OpenBSD, Windows). Example:

pgcli - Python Prompt Toolkit

(Image source: pcgli)

Flux
  • 9,805
  • 5
  • 46
  • 92
1

The posted answers work fine but I have open sourced an autocomplete library that I wrote at work. We have been using it for a while in production and it is fast, stable and easy to use. It even has a demo mode so you can quickly test what you would get as you type words.

To install it, simply run: pip install fast-autocomplete

Here is an example:

>>> from fast_autocomplete import AutoComplete
>>> words = {'book': {}, 'burrito': {}, 'pizza': {}, 'pasta':{}}
>>> autocomplete = AutoComplete(words=words)
>>> autocomplete.search(word='b', max_cost=3, size=3)
[['book'], ['burrito']]
>>> autocomplete.search(word='bu', max_cost=3, size=3)
[['burrito']]
>>> autocomplete.search(word='barrito', max_cost=3, size=3)  # mis-spelling
[['burrito']]

Checkout: https://github.com/seperman/fast-autocomplete for the source code.

And here is an explanation of how it works: http://zepworks.com/posts/you-autocomplete-me/

It deals with mis-spellings and optionally sorting by the weight of the word. (let's say burrito is more important than book, then you give burrito a higher "count" and it will show up first before book in the results.

Words is a dictionary and each word can have a context. For example the "count", how to display the word, some other context around the word etc. In this example words didn't have any context.

Seperman
  • 4,254
  • 1
  • 28
  • 27
1

This works well.

#!/usr/bin/python3

import readline
readline.parse_and_bind("tab: complete")

def complete(text,state):
    volcab = ['dog','cat','rabbit','bird','slug','snail']
    results = [x for x in volcab if x.startswith(text)] + [None]
    return results[state]

readline.set_completer(complete)

line = input('prompt> ')
WaXxX333
  • 388
  • 1
  • 2
  • 11