File Coverage

blib/lib/App/Spec/Completion/Zsh.pm
Criterion Covered Total %
statement 9 217 4.1
branch 0 88 0.0
condition 0 23 0.0
subroutine 3 10 30.0
pod 7 7 100.0
total 19 345 5.5


line stmt bran cond sub pod time code
1             # ABSTRACT: Shell Completion generator for zsh
2 1     1   1010 use strict;
  1         3  
  1         31  
3 1     1   5 use warnings;
  1         6  
  1         45  
4             package App::Spec::Completion::Zsh;
5              
6             our $VERSION = '0.013'; # VERSION
7              
8 1     1   5 use Moo;
  1         4  
  1         4  
9             extends 'App::Spec::Completion';
10              
11             sub generate_completion {
12 0     0 1   my ($self, %args) = @_;
13 0           my $spec = $self->spec;
14 0           my $appname = $spec->name;
15 0           my $functions = [];
16 0           my $appspec_version = App::Spec->VERSION;
17 0           my $completion_outer = $self->completion_commands(
18             commands => $spec->subcommands,
19             options => $spec->options,
20             parameters => $spec->parameters,
21             level => 1,
22             functions => $functions,
23             );
24              
25              
26 0           my $body = <<"EOM";
27             #compdef $appname
28              
29             # Generated with perl module App::Spec v$appspec_version
30              
31             _$appname() {
32             local program=$appname
33             typeset -A opt_args
34             local curcontext="\$curcontext" state line context
35              
36             $completion_outer
37             }
38              
39 0           @{[ join '', @$functions ]}
40             __${appname}_dynamic_comp() {
41             EOM
42 0           $body .= <<'EOM';
43             local argname="$1"
44             local arg="$2"
45             local comp="arg:$argname:(("
46             local line
47             while read -r line; do
48             local name="$line"
49             local desc="$line"
50             name="${name%$'\t'*}"
51             desc="${desc/*$'\t'}"
52             comp="$comp$name"
53             if [[ -n "$desc" && "$name" != "$desc" ]]; then
54             comp="$comp\\:"'"'"$desc"'"'
55             fi
56             comp="$comp "
57             done <<< "$arg"
58              
59             comp="$comp))"
60             _alternative "$comp"
61             }
62             EOM
63              
64 0           return $body;
65             }
66              
67             sub completion_commands {
68 0     0 1   my ($self, %args) = @_;
69 0           my $functions = $args{functions};
70 0           my $spec = $self->spec;
71 0           my $commands = $args{commands};
72 0           my $options = $args{options};
73 0           my $level = $args{level};
74 0   0       my $previous = $args{previous} || [];
75              
76 0           my $indent = ' ' x $level;
77 0           my $indent2 = ' ' x $level . ' ';
78 0 0         my $state = $level > 1 ? "-C" : "";
79 0           my $arguments = $indent . "_arguments -s $state \\\n";
80 0           my $cmd_count = $level;
81 0 0         unless (keys %$commands) {
82 0           $cmd_count--;
83             }
84 0           for my $i (1 .. $cmd_count) {
85 0           $arguments .= $indent2 . "'$i: :->cmd$i' \\\n";
86             }
87              
88             my ($param_args, $param_case) = $self->parameters(
89             parameters => $args{parameters},
90 0           level => $level,
91             count => $level,
92             functions => $functions,
93             previous => $previous,
94             );
95              
96 0 0         if ($param_args) {
97 0           $arguments .= "$param_args";
98             }
99 0 0         if (keys %$commands) {
100 0           $arguments .= $indent2 . "'*: :->args' \\\n";
101             }
102              
103 0 0 0       if (@$options and not keys %$commands) {
104 0           my ($opt) = $self->options(
105             options => $options,
106             level => $level,
107             functions => $functions,
108             previous => $previous,
109             );
110 0           $arguments .= "$opt \\\n";
111             }
112 0           $arguments .= $indent2 . "&& ret=0\n";
113              
114 0           my $cmds = $self->commands_alternative(
115             commands => $commands,
116             level => $level + 1,
117             );
118              
119 0           my $subcmds = '';
120 0 0         if (keys %$commands) {
121 0           $subcmds .= $indent2 . "case \$line[$level] in\n";
122 0           for my $key (sort keys %$commands) {
123 0           my $cmd_spec = $commands->{ $key };
124 0           my $name = $cmd_spec->name;
125 0           $subcmds .= $indent2 . "$name)\n";
126             my $sc = $self->completion_commands(
127             commands => $cmd_spec->subcommands || {},
128 0   0       options => [ @$options, @{ $cmd_spec->options } ],
  0            
129             parameters => $cmd_spec->parameters,
130             level => $level + 1,
131             previous => [@$previous, $name],
132             functions => $functions,
133             );
134 0           $subcmds .= $sc;
135 0           $subcmds .= $indent2 . ";;\n";
136             }
137 0           $subcmds .= $indent2 . "esac\n";
138             }
139              
140 0           my $body = <<"EOM";
141              
142             $indent# ---- Command: @$previous
143             $arguments
144             $param_case
145             EOM
146 0           my $cmd_state = '';
147 0 0         if ($cmds) {
148 0           $cmd_state = <<"EOM";
149             ${indent}cmd$level)
150             ${indent} $cmds
151             ${indent};;
152             EOM
153             }
154              
155 0           my $subcmd_state = '';
156 0 0         if (keys %$commands) {
157 0           $subcmd_state = <<"EOM";
158             ${indent}args)
159             $subcmds
160             ${indent};;
161             EOM
162             }
163              
164 0 0 0       if ($cmd_state or $subcmd_state) {
165 0           $body .= <<"EOM";
166             ${indent}case \$state in
167             EOM
168              
169 0           $body .= <<"EOM";
170             $cmd_state
171             $subcmd_state
172             ${indent}esac
173             EOM
174             }
175              
176 0           return $body;
177             }
178              
179             sub parameters {
180 0     0 1   my ($self, %args) = @_;
181 0           my $functions = $args{functions};
182 0           my $spec = $self->spec;
183 0   0       my $parameters = $args{parameters} || [];
184 0 0         return ('','') unless @$parameters;
185 0           my $level = $args{level};
186 0           my $count = $args{count};
187 0           my $indent = ' ' x $level;
188              
189 0           my $arguments = '';
190 0           my $case = $indent . "case \$state in\n";
191 0           for my $p (@$parameters) {
192 0           my $name = $p->name;
193 0           my $num = $count;
194 0 0         if ($p->multiple) {
195 0           $num = "*";
196             }
197 0           $arguments .= $indent . " '$num: :->$name' \\\n";
198 0           $count++;
199              
200 0           my $completion = '';
201 0 0         if (my $enum = $p->enum) {
    0          
    0          
    0          
    0          
    0          
202 0           my @list = map { "'$_'" } @$enum;
  0            
203 0           $completion = $indent . " compadd -X '$name:' @list";
204             }
205             elsif ($p->type =~ m/^file(name)?\z/) {
206 0           $completion = '_files';
207             }
208             elsif ($p->type =~ m/^dir(name)?\z/) {
209 0           $completion = '_path_files -/';
210             }
211             elsif ($p->type eq 'user') {
212 0           $completion = '_users';
213             }
214             elsif ($p->type eq 'host') {
215 0           $completion = '_hosts';
216             }
217             elsif ($p->completion) {
218             $completion = $self->dynamic_completion(
219             option => $p,
220             level => $level,
221             functions => $functions,
222             previous => $args{previous},
223 0           );
224             }
225 0           $case .= <<"EOM";
226             ${indent}$name)
227             $completion
228             ${indent};;
229             EOM
230             }
231 0           $case .= $indent . "esac\n";
232              
233 0           return ($arguments, $case);
234             }
235              
236             sub dynamic_completion {
237 0     0 1   my ($self, %args) = @_;
238 0           my $functions = $args{functions};
239 0           my $previous = $args{previous};
240 0           my $p = $args{option};
241 0           my $level = $args{level};
242 0           my $indent = ' ' x $level;
243 0           my $name = $p->name;
244 0           my $shell_name = $name;
245 0           $name =~ tr/^A-Za-z0-9_:-/_/c;
246 0           $shell_name =~ tr/^A-Za-z0-9_/_/c;
247              
248 0           my $def = $p->completion;
249 0           my ($op, $command, $command_string);
250 0 0 0       if (not ref $def and $def == 1) {
    0          
251 0 0         my $possible_values = $p->values or die "Error for '$name': completion: 1 but 'values' not defined";
252 0 0         $op = $possible_values->{op} or die "Error for '$name': 'values' needs an 'op'";
253             }
254             elsif (ref $def) {
255 0           $op = $def->{op};
256 0           $command = $def->{command};
257 0           $command_string = $def->{command_string};
258             }
259             else {
260 0           die "Error for '$name': invalid value for 'completion'";
261             }
262              
263 0           my $appname = $self->spec->name;
264 0 0         my $function_name = "_${appname}_"
265             . join ("_", @$previous)
266             . "_" . ($p->isa("App::Spec::Option") ? "option" : "param")
267             . "_" . $shell_name . "_completion";
268              
269 0           my $function;
270 0 0 0       if ($op) {
    0          
271 0           $function = <<"EOM";
272             $function_name() \{
273             local __dynamic_completion
274             __dynamic_completion=\$(PERL5_APPSPECRUN_SHELL=zsh PERL5_APPSPECRUN_COMPLETION_PARAMETER='$name' "\${words[@]}")
275             __${appname}_dynamic_comp '$name' \$__dynamic_completion
276             \}
277             EOM
278             }
279             elsif ($command or $command_string) {
280 0           my $string = '';
281              
282 0 0         if ($command) {
    0          
283 0           my @args;
284 0           for my $arg (@$command) {
285 0 0         unless (ref $arg) {
286 0           push @args, "'$arg'";
287 0           next;
288             }
289 0 0         if (my $replace = $arg->{replace}) {
290 0 0         if (ref $replace eq 'ARRAY') {
291 0           my @repl = @$replace;
292 0 0         if ($replace->[0] eq 'SHELL_WORDS') {
293 0           my $num = $replace->[1];
294 0           my $index = "\$CURRENT";
295 0 0         if ($num ne 'CURRENT') {
296 0 0         if ($num =~ m/^-/) {
297 0           $index .= $num;
298             }
299             else {
300 0           $index = $num;
301             }
302             }
303 0           my $string = qq{"\$words\[$index\]"};
304 0           push @args, $string;
305             }
306             }
307             else {
308 0 0         if ($replace eq "SELF") {
309 0           push @args, "\$program";
310             }
311             }
312             }
313             }
314 0           $string = "@args";
315             }
316             elsif (defined $command_string) {
317 0           $string = $command_string;
318             }
319 0           my $varname = "__${name}_completion";
320              
321 0           $function = <<"EOM";
322             $function_name() \{
323             local __dynamic_completion
324             local CURRENT_WORD="\$words\[CURRENT\]"
325             IFS=\$'\\n' __dynamic_completion=( \$( $string ) )
326             compadd -X "$shell_name:" \$__dynamic_completion
327             \}
328             EOM
329             }
330 0           push @$functions, $function;
331 0           return $function_name;
332             }
333              
334             sub commands_alternative {
335 0     0 1   my ($self, %args) = @_;
336 0           my $spec = $self->spec;
337 0           my $commands = $args{commands};
338 0 0         return '' unless keys %$commands;
339 0           my $level = $args{level};
340 0           my @subcommands;
341 0           for my $key (sort grep { not m/^_/ } keys %$commands) {
  0            
342 0           my $cmd = $commands->{ $key };
343 0           my $name = $cmd->name;
344 0           my $summary = $cmd->summary;
345 0           push @subcommands, [$name, $summary];
346             }
347 0           my $string = $self->list_to_alternative(
348             name => "cmd$level",
349             list => \@subcommands,
350             );
351 0           return "_alternative '$string'";
352             }
353              
354             sub list_to_alternative {
355 0     0 1   my ($self, %args) = @_;
356 0           my $list = $args{list};
357 0           my $name = $args{name};
358             my @alt = map {
359 0           my ($alt_name, $summary);
  0            
360 0 0         if (ref $_ eq 'ARRAY') {
361 0           ($alt_name, $summary) = @$_;
362 0   0       $summary //= '';
363             }
364             else {
365 0           ($alt_name, $summary) = ($_, '');
366             }
367 0           $alt_name =~ s/:/\\\\:/g;
368 0           $summary =~ s/"/\\"/g;
369 0           $summary =~ s/['`]/'"'"'/g;
370 0           $summary =~ s/\$/\\\$/g;
371 0 0         length $summary ? qq{$alt_name\\:"$summary"} : $alt_name
372             } @$list;
373 0           my $string = qq{args:$name:((@alt))};
374             }
375              
376             sub options {
377 0     0 1   my ($self, %args) = @_;
378 0           my $functions = $args{functions};
379 0           my $spec = $self->spec;
380 0           my $options = $args{options};
381 0           my $level = $args{level};
382 0           my $indent = ' ' x $level;
383 0           my @options;
384 0           for my $opt (@$options) {
385 0           my $name = $opt->name;
386 0           my $summary = $opt->summary;
387 0           my $type = $opt->type;
388 0           my $enum = $opt->enum;
389 0           my $aliases = $opt->aliases;
390 0           my $values = '';
391 0 0 0       if ($opt->completion) {
    0          
    0          
    0          
    0          
392             my @names = map {
393 0 0         length > 1 ? "--$_" : "-$_"
  0            
394             } ($name, @$aliases);
395 0           my $comp = $indent . join ('|', @names) . ")\n";
396             my $function_name = $self->dynamic_completion(
397             option => $opt,
398             level => $level,
399             functions => $functions,
400             previous => $args{previous},
401 0           );
402 0           $values = ":$name:$function_name";
403 0           $comp .= $indent . ";;\n";
404             }
405             elsif ($enum) {
406             my @list = map {
407 0           my $item = $_;
  0            
408 0           $item =~ s/:/\\:/g;
409 0           qq{"$item"};
410             } @$enum;
411 0           $values = ":$name:(@list)";
412             }
413             elsif ($type =~ m/^file(name)?\z/) {
414 0           $values = ":$name:_files";
415             }
416             elsif ($type =~ m/^dir(name)?\z/) {
417 0           $values = ":$name:_path_files -/";
418             }
419             elsif (not ref $type and $type ne "flag") {
420 0           $values = ":$name";
421             }
422 0           $summary =~ s/['`]/'"'"'/g;
423 0           $summary =~ s/\$/\\\$/g;
424              
425 0 0         my $multiple = $opt->multiple ? "*" : "";
426             # '(-c --count)'{-c,--count}'[Number of list items to show]:c' \
427             # '(-a --all)'{-a,--all}'[Show all list items]' \
428             my @names = map {
429 0 0         length > 1 ? "--$_" : "-$_"
  0            
430             } ($name, @$aliases);
431 0           for my $name (@names) {
432 0           my $str = "'$multiple$name\[$summary\]$values'";
433 0           push @options, $indent . " $str";
434             }
435             }
436 0           my $string = join " \\\n", @options;
437 0           return $string;
438             }
439              
440              
441             1;
442              
443             __DATA__