One final rant about programmable completion in bash

Still hacking around bash tab completion. Released some utilities like pmpath, podpath, pmless, pmedit (you can find them in App-PMUtils distribution). To use these utilities:

% cpanm App::PMUtils
% complete -C pmpath pmpath ; # this line can be added to your .bashrc
% pmpath [TAB]
% pmpath tex[TAB]
% pmpath text/ansi[TAB]
% complete -C pmless pmless; # do the same for the other utils
(and so on)

You'll notice that I use slash instead of double colon as it should be. And here goes the rant.

Here's the gist of how completion in bash works. For each command, you'll need to issue a 'complete' internal bash command to tell bash how to complete argument for that command. For example:

% complete -W "checkout clean commit log status" examplecmd

After this command is issued, if you type:

% examplecmd [TAB]

or:

% examplecmd c[TAB]

bash will display the completion alternatives which are taken from the word list specified in -W. bash can accept the list from other sources, most notably from bash function (-F) or from an external command (-C). If you see the above, my utilities call themselves to do the completion (e.g. complete -C pmpath pmpath).

A bash function will receive its input from 2 variables: COMP_WORDS, which is a bash array containing the command line chopped up into words much like (but not exactly like) @ARGV in Perl. And COMP_CWORD, which is an integer referring to the index of the array and indicating the position of cursor when the Tab key is pressed.

An external command will receive its input from 2 environment variables too, but since arrays are thought to be rather cumbersome to pass, this time bash sets COMP_LINE (a string, the raw command line) and COMP_POINT (an integer pointing to the character position in COMP_LINE where the cursor is).

(By the way, other shells do not use the same way. If I'm not mistaken, zsh can accept completion source from external program too, but it will pass the input as command line arguments. And AFAIK fish does not provide a mechanism to call external program: you have to specify everything through its 'complete' internal command).

Anyway, back to bash: in COMP_WORDS case, bash chops the command line string using some criteria (which really comes from the readline library) which is the COMP_WORDBREAKS setting. The default value for this variable are: space, tab, newline, double quote, single quote, at sign, greater-than sign, less-than sign, equal sign, semicolon, pipe, ampersand, open paren, and colon.

In shell, those characters make sense. For example:

% ls -l >>[TAB]
% ps ax|[TAB]
% export PATH=/sbin:/bin:[TAB]
% export PATH=/sbin:/bin:/home/a[TAB]

But if we want to complete Perl-related stuffs, like module names in my command-line utilities, things are not so good:

% pmpath Text::[TAB]

bash will assume word as '' (empty string) since it begins right after word separator character which is the colon. By the way, this annoyance also happens if you want to complete file names which contain colon, or equal sign, or at sign, or the other aforementioned word-breaking character. To avoid this, you'll have to escape the character:

% pmpath Text\:\:[TAB]

but it's not very fun to type, is it.

And it's also annoying because if we use an external program (the -C option of 'complete'), we receive the raw command line string and are actually free to chop up the command line however we want.

In my completion library, I split the command line using just space and tab. Of course, after taking backslash and quotes into account. So, for example:

pmpath Text::ANSI

will be split into:

["pmpath", "Text::ANSI"]

and this:

cmd "first argument" second\ argument

will be split into:

["cmd", "first argument", "second argument"]

When the completion library sees Text::ANSI it will try to find all modules (and module prefixes, so user can drill down using tab completion) that begin with that string, e.g. ["Text::ANSI::", "Text::ANSITable", "Text::ANSITable::"]. But if it returns those, as mentioned previously, bash will replace the input buffer with:

% pmpath Text::Text::ANSI

because it replaces the last word (ANSI, not Text::ANSI "as it should") with the common prefix of the completion reply (Text::ANSI).

Annoying right? That's why in my utilities, I currently use / instead of double colon.

(The completion library could try to be smart and return this list of completion instead: ["ANSI::", "ANSITable", "ANSITable::"]. But I want to be able to do case insensitive searching so that bash can replace all-lowercase typed word into the proper CamelCased module name. And that's not possible if we still use colon as bash will already move to the next word).

Ah well, slash is not too bad.

5 Comments

How about:

pmpath 'Text::A[TAB]

It's maybe a little annoying to quote it, but it should work.

Hi,

yesterday I found 'Complete::Util' on metacpan. I loked at the docs and felt that there is missing something when you don't know the internals of bash completion.

Now I found your blog entry. I think you should take this entry and put it in an examples section to your ditribution's pod.

I will give this module a try as I just need to implement this kind of functionality.

Best regards
McA

what about 'Ansi::

( single quote? )

Leave a comment

About Steven Haryanto

user-pic A programmer (mostly Perl 5 nowadays). My CPAN ID: SHARYANTO. I'm sedusedan on perlmonks. My twitter is stevenharyanto (but I don't tweet much). Follow me on github: sharyanto.