Welcome to Software Development on Codidact!
Will you help us build our independent community of developers helping developers? We're small and trying to grow. We welcome questions about all aspects of software development, from design to code to QA and more. Got questions? Got answers? Got code you'd like someone to review? Please join us.
How do I support tab completion in a python CLI program?
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?
1 answer
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, andCOMP_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, whileprev
contains the previous word
- hence,
-
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.
0 comment threads