File Coverage

blib/lib/App/Spec/Completion/Zsh.pm
Criterion Covered Total %
statement 9 219 4.1
branch 0 90 0.0
condition 0 23 0.0
subroutine 3 10 30.0
pod 7 7 100.0
total 19 349 5.4


line stmt bran cond sub pod time code
1             # ABSTRACT: Shell Completion generator for zsh
2 2     2   148029 use strict;
  2         4  
  2         62  
3 2     2   7 use warnings;
  2         3  
  2         136  
4             package App::Spec::Completion::Zsh;
5              
6             our $VERSION = 'v0.15.0'; # VERSION
7              
8 2     2   438 use Moo;
  2         6896  
  2         9  
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 0         $summary =~ s/\s+/ /g if defined $summary;
346 0           push @subcommands, [$name, $summary];
347             }
348 0           my $string = $self->list_to_alternative(
349             name => "cmd$level",
350             list => \@subcommands,
351             );
352 0           return "_alternative '$string'";
353             }
354              
355             sub list_to_alternative {
356 0     0 1   my ($self, %args) = @_;
357 0           my $list = $args{list};
358 0           my $name = $args{name};
359             my @alt = map {
360 0           my ($alt_name, $summary);
  0            
361 0 0         if (ref $_ eq 'ARRAY') {
362 0           ($alt_name, $summary) = @$_;
363 0   0       $summary //= '';
364             }
365             else {
366 0           ($alt_name, $summary) = ($_, '');
367             }
368 0           $alt_name =~ s/:/\\\\:/g;
369 0           $summary =~ s/"/\\"/g;
370 0           $summary =~ s/['`]/'"'"'/g;
371 0           $summary =~ s/\$/\\\$/g;
372 0 0         length $summary ? qq{$alt_name\\:"$summary"} : $alt_name
373             } @$list;
374 0           my $string = qq{args:$name:((@alt))};
375             }
376              
377             sub options {
378 0     0 1   my ($self, %args) = @_;
379 0           my $functions = $args{functions};
380 0           my $spec = $self->spec;
381 0           my $options = $args{options};
382 0           my $level = $args{level};
383 0           my $indent = ' ' x $level;
384 0           my @options;
385 0           for my $opt (@$options) {
386 0           my $name = $opt->name;
387 0           my $summary = $opt->summary;
388 0           my $type = $opt->type;
389 0           my $enum = $opt->enum;
390 0           my $aliases = $opt->aliases;
391 0           my $values = '';
392 0 0 0       if ($opt->completion) {
    0          
    0          
    0          
    0          
393             my @names = map {
394 0 0         length > 1 ? "--$_" : "-$_"
  0            
395             } ($name, @$aliases);
396 0           my $comp = $indent . join ('|', @names) . ")\n";
397             my $function_name = $self->dynamic_completion(
398             option => $opt,
399             level => $level,
400             functions => $functions,
401             previous => $args{previous},
402 0           );
403 0           $values = ":$name:$function_name";
404 0           $comp .= $indent . ";;\n";
405             }
406             elsif ($enum) {
407             my @list = map {
408 0           my $item = $_;
  0            
409 0           $item =~ s/:/\\:/g;
410 0           qq{"$item"};
411             } @$enum;
412 0           $values = ":$name:(@list)";
413             }
414             elsif ($type =~ m/^file(name)?\z/) {
415 0           $values = ":$name:_files";
416             }
417             elsif ($type =~ m/^dir(name)?\z/) {
418 0           $values = ":$name:_path_files -/";
419             }
420             elsif (not ref $type and $type ne "flag") {
421 0           $values = ":$name";
422             }
423 0           $summary =~ s/\s+/ /g;
424 0           $summary =~ s/['`]/'"'"'/g;
425 0           $summary =~ s/\$/\\\$/g;
426              
427 0 0         my $multiple = $opt->multiple ? "*" : "";
428             # '(-c --count)'{-c,--count}'[Number of list items to show]:c' \
429             # '(-a --all)'{-a,--all}'[Show all list items]' \
430             my @names = map {
431 0 0         length > 1 ? "--$_" : "-$_"
  0            
432             } ($name, @$aliases);
433 0           for my $name (@names) {
434 0           my $str = "'$multiple$name\[$summary\]$values'";
435 0           push @options, $indent . " $str";
436             }
437             }
438 0           my $string = join " \\\n", @options;
439 0           return $string;
440             }
441              
442              
443             1;
444              
445             __DATA__