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 {
[200];
}
Perinci::CmdLine::Any->new(
url => '/main/mycomp',
)->run;
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 ...
.git/
.gitignore
hello
mycomp
mycomp~
pause/
perl-App-hello/
[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.