Bash completion can be awkward to program in bash. The docs aren't great, and the language esoteric. It would be nice to be able to use your favorite language to do the heavy lifting. This article shows how to achieve this in Python and provides an example of how you can call in to your own application to get the completion data.

The Bash Part

I will start with the bash part of the script. This can be copied replacing just the name of your completion program and the commands to complete. It focuses on getting the important information out of bash completion and into your code.

 1 _complete () {
 2     local cur
 3 
 4     cur=${COMP_WORDS[COMP_CWORD]}
 5     words=`./comp_blog.py "$COMP_CWORD" ${COMP_WORDS[*]}`
 6 
 7     COMPREPLY=( $(compgen -W "${words}" -- ${cur}) )
 8 }
 9 
10 blog(){
11     echo "blog $*"
12 }
13 
14 complete -F _complete blog

The above script is boilerplate code that gets the right values out of the completion variables and sends them through to your script. The arguments passed in are the all of the arguments currently on the commandline. cur is used by the compgen function to determine what the current word in the argument list is.

The Python Part

Time for the Python script. This will just implement a multi-level completion where previous terms dictate what future terms look like.

 1 #!/usr/bin/python
 2 import sys
 3 
 4 options = {
 5         'add': ['--name', '--date', '--content'],
 6         'delete': ['--name', '--force']
 7 }
 8 
 9 commands = options.keys()
10 
11 index = int(sys.argv[1]) # First argument is the index.
12 all = sys.argv[2:]       # All others are the args.
13 
14 # If the list is shorter than the index of the current word
15 # then the current word is empty. Otherwise it is the last
16 # element in the list.
17 cur = "" if len(all) <= index else all[index]
18 prev = all[index - 1] # Item before the current.
19 all = all[1:] # Strip off the command name from the beginning.
20 
21 if prev not in all:
22     # prev must be the command itself. Return subcommands.
23     print " ".join(commands)
24 else:
25     # Find the subcommands already present.
26     command_list = [word for word in all if word in commands]
27     if command_list:
28         # Send back options of latest subcommand.
29         opts = options[command_list[-1]]
30         print " ".join([opt for opt in opts if opt not in all])
31     else:
32         # No completions to be had.
33         print ""

Note that the cur variable isn't used but is provided in order to illustrate how to get the same value as in the bash script above. The all variable contains all arguments, stripping out the name of the script (argv[0]) and the name of the command being completed (i.e. blog in this example.) This is a fairly simple multi-level script that has a list of options for the subcommands provided.

More complex scripts could be created - with much more ease and readability than if they had been written in bash. I have written a small command framework in Ruby which uses the Trollop option parser to parse the command line. Using the parser method in Trollop, it is fairly easy to create a program which defines subcommands, where those subcommands provide their own option parser. The Trollop parsers can be queried for the purposes of auto-completion. This may make up a future post.



Comments

comments powered by Disqus