Communities

Writing
Writing
Codidact Meta
Codidact Meta
The Great Outdoors
The Great Outdoors
Photography & Video
Photography & Video
Scientific Speculation
Scientific Speculation
Cooking
Cooking
Electrical Engineering
Electrical Engineering
Judaism
Judaism
Languages & Linguistics
Languages & Linguistics
Software Development
Software Development
Mathematics
Mathematics
Christianity
Christianity
Code Golf
Code Golf
Music
Music
Physics
Physics
Linux Systems
Linux Systems
Power Users
Power Users
Tabletop RPGs
Tabletop RPGs

Dashboard
Notifications
Mark all as read
Q&A

How do I support tab completion in a python CLI program?

+11
−0

I spend a lot of time writing CLI tools in Python, and I would like to support tab-completion in a style similar to Git. For example, subcommands should be tab-completable, options should expand based on the valid choices, and filenames should complete based on the types of files the program supports.

How is this sort of thing done?

Why does this post require moderator attention?
You might want to add some details to your flag.
Why should this post be closed?

0 comment threads

1 answer

You are accessing this answer with a direct link, so it's being shown above all other answers regardless of its score. You can return to the normal view.

+11
−0

It depends. Do you want to have autocomplete on the shell the program runs in, or do you want the program to intercept the TAB key and do the autocomplete by itself?


Shell autocomplete

If you're running your program in a Linux shell, and want to autocomplete in the shell's command line (such as script.py <TAB>), then it must be configured in the shell itself. In this case you have to provide a custom autocomplete script, such as this.

To provide a simpler version, let's suppose my Python file is script.py and I want to add autocomplete in Bash. I'd create a bash script like this:

#!/bin/bash

_script_options() {
    local cur prev opts
    COMPREPLY=()
    cur="${COMP_WORDS[COMP_CWORD]}"
    prev="${COMP_WORDS[COMP_CWORD-1]}"
    opts="--help -a -b --some-option"

    if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
        COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
        return 0
    fi

    case "${prev}" in
        -a) # list only CSV files
            local files=$(ls *.csv)
            COMPREPLY=( $(compgen -W "${files}" -- ${cur}) )
        ;;
        # other options has no further autocomplete
    esac
}

Then you could copy this script to /etc/bash_completion.d/, and in your .bashrc file just add a line to register it:

complete -F _script_options script.py

Now open a new terminal, and when you type script.py <TAB> it'll use the logic provided by the _script_options function. There's a nice explanation here, but for short:

  • COMPREPLY is the array from which Bash reads the possible completions
  • COMP_WORDS is an array with the words of the current command line, and COMP_CWORD is an index into ${COMP_WORDS} of the word containing the current cursor position
    • hence, cur contains the current word of the command line, while prev contains the previous word
  • opts contains the default list of options for the script

If I type script.py <TAB>, it'll enter the if (because we're in the first word), and it'll show the options in the opts variable (using the command compgen to generate the list from the word list contained in opts).

If I type script.py -a <TAB>, it'll not enter the if, because the current word is the second (an empty one, as I just pressed TAB). As the previous word (contained in prev) is -a, it'll enter the first case option. And in that case, I show only the CSV files in the current directory (the list of file names was obtained using ls command).

You can customize this to contain any logic you want (check again the Git's script to see how complicated it can be). But the basic idea is to use COMP_WORDS and the other variables described here to know what's already typed, and set COMPREPLY with the respective options.

For Windows shell, I don't know how to do it.


Inside the program

If you want the Python program itself to interpret the TAB key and show the autocomplete options, one alternative is to use the cmd module. Quick example:

from cmd import Cmd

class CustomCommand(Cmd):
    def __init__(self):
        super().__init__()
        self.prompt = 'CustomCommand> '
        self.command1_options = [ 'a', 'b', 'some-option', 'someother-option' ]

    def do_command1(self, line):
        """
        This docstring will be displayed if you type "help command1"
        """
        # do whatever the command needs to do ("line" contains the whole command that was typed)
        ...

    def complete_command1(self, text, line, start_index, end_index):
        if text: # if I already started typing the option, filter the existing options
            return [
                option for option in self.command1_options
                if option.startswith(text)
            ]
        else:
            return self.command1_options


my_cmd = CustomCommand()
my_cmd.cmdloop()

If I run the code above, it'll show a prompt like this:

$ python teste.py
CustomCommand> 

The CustomCommand>   prompt is the string I've set in self.prompt. If I type com<TAB>, it'll autocomplete with the command1 command:

CustomCommand> command1

Now if I type command1 <TAB><TAB> (note the space after "command1"), it'll call the complete_command1 method and use the array returned by it to know the autocomplete options. The text argument contains whatever is typed so far (ex: if I type command1 some<TAB><TAB>, inside the complete_command1 method the text argument will have the value some and it'll return a list containing only "some-option" and "someother-option").

After typing the command and pressing ENTER, it'll run the do_command1 method. Basically, you create do_foo methods to execute foo commands, and complete_foo methods to provide the autocomplete options for those commands.

As it's Python code, you can put whatever logic you want inside the methods (ex: use glob or os modules to list only specific files, etc). All the methods receive the line argument containing the whole command that was typed.

Why does this post require moderator attention?
You might want to add some details to your flag.

0 comment threads

Sign up to answer this question »