pericmd 029: More on tab completion (1)

The next several blog posts will focus on tab completion.

Let’s get right to it with a simple example. Put the code below to mycomp, chmod +x the file, and put it somewhere in your PATH (e.g. /usr/local/bin or $HOME/bin if your PATH happens to have $HOME/bin as an entry):

#!/usr/bin/env perl

use 5.010;
use strict;
use warnings;

use Perinci::CmdLine::Any;

our %SPEC;
$SPEC{mycomp} = {
    v => 1.1,
    args => {
        int1 => {
            schema => [int => min=>1, max=>30],
        str1 => {
            schema => [str => in=>[qw/foo bar baz qux quux/]],
        str2 => {
            schema => ['str'],
sub mycomp {

    url => '/main/mycomp',

Activate bash completion by executing this command in your shell:

% complete -C mycomp mycomp

If your script happens to live outside PATH, e.g. in /path/to/mycomp, you can instead use:

% complete -C /path/to/mycomp mycomp

but normally your CLI programs will reside in PATH, so the above command is for testing only.

Now to test completion:

% mycomp <tab><tab>
-\?               .gitignore        --json            perl-App-hello/
--config-path     -h                mycomp            --str1
--config-profile  hello             --naked-res       -v
--format          --help            --no-config       --version
.git/             --int1            pause/      

As you can see, by default Perinci::CmdLine gives you a list of known options as well as files and directives in the current directory.

% mycomp -<tab><tab>
-\?               -h                --naked-res       --version
--config-path     --help            --no-config       
--config-profile  --int1            --str1            
--format          --json            -v  

If the current word (the word being completed at the cursor) is “-“, Perinci::CmdLine assumes that you want to complete an option name so it doesn’t give a list of files/dirs. (What if, in the rare case, there is a file beginning with a dash and you want to complete it? You can use ./-.)

If the option name can be completed unambiguously:

% mycomp --i<tab><tab>

then it will be completed directly without showing list of completion candidates (underscore _ shows the location of cursor):

% mycomp --int1 _

Perinci::CmdLine can also complete option values. Now let’s press tab again to complete:

% mycomp --int1 <tab><tab>
1   11  13  15  17  19  20  22  24  26  28  3   4   6   8   
10  12  14  16  18  2   21  23  25  27  29  30  5   7   9   

From the argument schema ([int => min=>1, max=>30]), Perinci::CmdLine can provide a list of numbers from 1 to 30 as completion candidates. Now let’s try another argument:

% mycomp --str1=<tab><tab>
bar   baz   foo   quux  qux   

The schema ([str => in=>[qw/foo bar baz qux quux/]]) also helps Perinci::CmdLine provide a completion list. Now another argument:

% mycomp --str2 <tab><tab>
.git/            hello            mycomp~          perl-App-hello/  
.gitignore       mycomp           pause/           

What happened? Since the schema (['str']) doesn’t provide any hints about possible values, Perinci::CmdLine falls back to completing using files/dirs in the current directory. Of course, you can also do something like:

% mycomp --str2 ../../foo<tab><tab>

to list other directories.

This is all nice and good, but the power of tab completion comes with custom completion: when we are able to provide our own completion to option values (and arguments). Let’s try that by adding a completion routine in our Rinci metadata:

use Complete::Util qw(complete_array_elem);

$SPEC{mycomp} = {
    v => 1.1,
    args => {
        int1 => {
            schema => [int => min=>1, max=>30],
            completion => sub {
                my %args = @_;
                my $word = $args{word};

                # let's provide a list of numbers from 1 to current day of month
                my $mday = (localtime)[3];
                complete_array_elem(word=>$word, array=>[1..$mday]);
        str1 => {
            schema => [str => in=>[qw/foo bar baz qux quux/]],
        str2 => {
            schema => ['str'],

You see a couple of things new here. First is the completion routine which is supplied in the completion property of the argument specification. A completion routine will receive a hash of arguments (the most important argument is word, there are other arguments and we will get to it later). A completion routine is expected to return an array of words or a hash (see Complete for the specification of the “completion answer”). Second is the use of the module Complete::Util and a function from the module called complete_array_elem which will return an array filtered by $word as prefix. The module contains some more utility functions which we will discuss later.

Now let’s test it (assuming today is Feb 27th, 2015):

% mycomp --int1 <tab><tab>
1   11  13  15  17  19  20  22  24  26  3   5   7   9   
10  12  14  16  18  2   21  23  25  27  4   6   8   

Debugging completion

When we write completion code, we might make mistakes. For example, suppose we forget to use Complete::Util qw(complete_array_elem); then when we test it, we might get unexpected result:

% mycomp --int1 <tab><tab>
.git/            hello            mycomp~          perl-App-hello/  
.gitignore       mycomp           pause/   

Why is Perinci::CmdLine showing files/dirs from current directory instead?

To help debug problems when doing custom completion, you can use the testcomp utility (install it via cpanm App::CompleteUtils). To use testcomp, specify the command and arguments and put ^ (caret) to signify where the cursor is supposed to be. So type:

% testcomp mycomp --int1 ^
[testcomp] COMP_LINE=<mycomp --int1 >, COMP_POINT=14
[testcomp] exec(): ["/mnt/home/s1/perl5/perlbrew/perls/perl-5.18.4/bin/perl","-MLog::Any::Adapter=ScreenColoredLevel","mycomp"]
[pericmd] -> run(), @ARGV=[]
[pericmd] Checking env MYCOMP_OPT: <undef>
[pericmd] Running hook_after_get_meta ...
[comp][periscomp] entering Perinci::Sub::Complete::complete_cli_arg(), words=["--int1",""], cword=1, word=<>
[comp][compgl] entering Complete::Getopt::Long::complete_cli_arg(), words=["--int1",""], cword=1, word=<>
[comp][compgl] invoking routine supplied from 'completion' argument to complete option value, option=<--int1>
[comp][periscomp] entering completion routine (that we supply to Complete::Getopt::Long)
[comp][periscomp] completing option value for a known function argument, arg=<int1>, ospec=<int1=i>
[comp][periscomp] invoking routine supplied from 'completion' argument
[comp][periscomp] result from 'completion' routine: <undef>
[comp][periscomp] entering complete_arg_val, arg=<int1>
[comp][periscomp] invoking routine specified in arg spec's 'completion' property
[comp][periscomp] completion died: Undefined subroutine &main::complete_array_elem called at mycomp line 22.
[comp][periscomp] no completion from metadata possible, declining
[comp][periscomp] leaving complete_arg_val, result=<undef>
[comp][periscomp] leaving completion routine (that we supply to Complete::Getopt::Long)
[comp][compgl] adding result from routine: <undef>
[comp][compgl] entering default completion routine
[comp][compgl] completing with file, file=<>
[comp][compgl] leaving default completion routine, result={path_sep => "/",words => [".git/",".gitignore","hello","mycomp","mycomp~","pause/","perl-App-hello/"]}
[comp][compgl] adding result from default completion routine
[comp][compgl] leaving Complete::Getopt::Long::complete_cli_arg(), result={path_sep => "/",words => [".git/",".gitignore","hello","mycomp","mycomp~","pause/","perl-App-hello/"]}
[comp][periscomp] leaving Perinci::Sub::Complete::complete_cli_arg(), result={path_sep => "/",words => [".git/",".gitignore","hello","mycomp","mycomp~","pause/","perl-App-hello/"]}
[pericmd] Running hook_display_result ...
[pericmd] Running hook_after_run ...
[pericmd] exit(0)

From the debug output, you can see the error message and realize that the completion routine dies. You’ll also know that Perinci::CmdLine then falls back to using using files/dirs.


Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s