File Coverage

blib/lib/App/Spec/Completion/Zsh.pm
Criterion Covered Total %
statement 9 215 4.1
branch 0 84 0.0
condition 0 26 0.0
subroutine 3 10 30.0
pod 7 7 100.0
total 19 342 5.5


line stmt bran cond sub pod time code
1             # ABSTRACT: Shell Completion generator for zsh
2 1     1   746 use strict;
  1         15  
  1         24  
3 1     1   5 use warnings;
  1         1  
  1         37  
4             package App::Spec::Completion::Zsh;
5              
6             our $VERSION = '0.012'; # VERSION
7              
8 1     1   4 use Moo;
  1         3  
  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          
202 0           my @list = map { "'$_'" } @$enum;
  0            
203 0           $completion = $indent . " compadd -X '$name:' @list";
204             }
205             elsif ($p->type eq 'file') {
206 0           $completion = '_files';
207             }
208             elsif ($p->type eq 'user') {
209 0           $completion = '_users';
210             }
211             elsif ($p->type eq 'host') {
212 0           $completion = '_hosts';
213             }
214             elsif ($p->completion) {
215             $completion = $self->dynamic_completion(
216             option => $p,
217             level => $level,
218             functions => $functions,
219             previous => $args{previous},
220 0           );
221             }
222 0           $case .= <<"EOM";
223             ${indent}$name)
224             $completion
225             ${indent};;
226             EOM
227             }
228 0           $case .= $indent . "esac\n";
229              
230 0           return ($arguments, $case);
231             }
232              
233             sub dynamic_completion {
234 0     0 1   my ($self, %args) = @_;
235 0           my $functions = $args{functions};
236 0           my $previous = $args{previous};
237 0           my $p = $args{option};
238 0           my $level = $args{level};
239 0           my $indent = ' ' x $level;
240 0           my $name = $p->name;
241 0           my $shell_name = $name;
242 0           $name =~ tr/^A-Za-z0-9_:-/_/c;
243 0           $shell_name =~ tr/^A-Za-z0-9_/_/c;
244              
245 0           my $def = $p->completion;
246 0           my ($op, $command, $command_string);
247 0 0 0       if (not ref $def and $def == 1) {
    0          
248 0 0         my $possible_values = $p->values or die "Error for '$name': completion: 1 but 'values' not defined";
249 0 0         $op = $possible_values->{op} or die "Error for '$name': 'values' needs an 'op'";
250             }
251             elsif (ref $def) {
252 0           $op = $def->{op};
253 0           $command = $def->{command};
254 0           $command_string = $def->{command_string};
255             }
256             else {
257 0           die "Error for '$name': invalid value for 'completion'";
258             }
259              
260 0           my $appname = $self->spec->name;
261 0 0         my $function_name = "_${appname}_"
262             . join ("_", @$previous)
263             . "_" . ($p->isa("App::Spec::Option") ? "option" : "param")
264             . "_" . $shell_name . "_completion";
265              
266 0           my $function;
267 0 0 0       if ($op) {
    0          
268 0           $function = <<"EOM";
269             $function_name() \{
270             local __dynamic_completion
271             __dynamic_completion=\$(PERL5_APPSPECRUN_SHELL=zsh PERL5_APPSPECRUN_COMPLETION_PARAMETER='$name' "\${words[@]}")
272             __${appname}_dynamic_comp '$name' \$__dynamic_completion
273             \}
274             EOM
275             }
276             elsif ($command or $command_string) {
277 0           my $string = '';
278              
279 0 0         if ($command) {
    0          
280 0           my @args;
281 0           for my $arg (@$command) {
282 0 0         unless (ref $arg) {
283 0           push @args, "'$arg'";
284 0           next;
285             }
286 0 0         if (my $replace = $arg->{replace}) {
287 0 0         if (ref $replace eq 'ARRAY') {
288 0           my @repl = @$replace;
289 0 0         if ($replace->[0] eq 'SHELL_WORDS') {
290 0           my $num = $replace->[1];
291 0           my $index = "\$CURRENT";
292 0 0         if ($num ne 'CURRENT') {
293 0 0         if ($num =~ m/^-/) {
294 0           $index .= $num;
295             }
296             else {
297 0           $index = $num;
298             }
299             }
300 0           my $string = qq{"\$words\[$index\]"};
301 0           push @args, $string;
302             }
303             }
304             else {
305 0 0         if ($replace eq "SELF") {
306 0           push @args, "\$program";
307             }
308             }
309             }
310             }
311 0           $string = "@args";
312             }
313             elsif (defined $command_string) {
314 0           $string = $command_string;
315             }
316 0           my $varname = "__${name}_completion";
317              
318 0           $function = <<"EOM";
319             $function_name() \{
320             local __dynamic_completion
321             local CURRENT_WORD="\$words\[CURRENT\]"
322             IFS=\$'\\n' __dynamic_completion=( \$( $string ) )
323             compadd -X "$shell_name:" \$__dynamic_completion
324             \}
325             EOM
326             }
327 0           push @$functions, $function;
328 0           return $function_name;
329             }
330              
331             sub commands_alternative {
332 0     0 1   my ($self, %args) = @_;
333 0           my $spec = $self->spec;
334 0           my $commands = $args{commands};
335 0 0         return '' unless keys %$commands;
336 0           my $level = $args{level};
337 0           my @subcommands;
338 0           for my $key (sort grep { not m/^_/ } keys %$commands) {
  0            
339 0           my $cmd = $commands->{ $key };
340 0           my $name = $cmd->name;
341 0           my $summary = $cmd->summary;
342 0           push @subcommands, [$name, $summary];
343             }
344 0           my $string = $self->list_to_alternative(
345             name => "cmd$level",
346             list => \@subcommands,
347             );
348 0           return "_alternative '$string'";
349             }
350              
351             sub list_to_alternative {
352 0     0 1   my ($self, %args) = @_;
353 0           my $list = $args{list};
354 0           my $name = $args{name};
355             my @alt = map {
356 0           my ($alt_name, $summary);
  0            
357 0 0         if (ref $_ eq 'ARRAY') {
358 0           ($alt_name, $summary) = @$_;
359 0   0       $summary //= '';
360             }
361             else {
362 0           ($alt_name, $summary) = ($_, '');
363             }
364 0           $alt_name =~ s/:/\\\\:/g;
365 0           $summary =~ s/"/\\"/g;
366 0           $summary =~ s/['`]/'"'"'/g;
367 0           $summary =~ s/\$/\\\$/g;
368 0 0         length $summary ? qq{$alt_name\\:"$summary"} : $alt_name
369             } @$list;
370 0           my $string = qq{args:$name:((@alt))};
371             }
372              
373             sub options {
374 0     0 1   my ($self, %args) = @_;
375 0           my $functions = $args{functions};
376 0           my $spec = $self->spec;
377 0           my $options = $args{options};
378 0           my $level = $args{level};
379 0           my $indent = ' ' x $level;
380 0           my @options;
381 0           for my $opt (@$options) {
382 0           my $name = $opt->name;
383 0           my $summary = $opt->summary;
384 0           my $type = $opt->type;
385 0           my $enum = $opt->enum;
386 0           my $aliases = $opt->aliases;
387 0           my $values = '';
388 0 0 0       if ($opt->completion) {
    0 0        
    0          
    0          
389             my @names = map {
390 0 0         length > 1 ? "--$_" : "-$_"
  0            
391             } ($name, @$aliases);
392 0           my $comp = $indent . join ('|', @names) . ")\n";
393             my $function_name = $self->dynamic_completion(
394             option => $opt,
395             level => $level,
396             functions => $functions,
397             previous => $args{previous},
398 0           );
399 0           $values = ":$name:$function_name";
400 0           $comp .= $indent . ";;\n";
401             }
402             elsif ($enum) {
403             my @list = map {
404 0           my $item = $_;
  0            
405 0           $item =~ s/:/\\:/g;
406 0           qq{"$item"};
407             } @$enum;
408 0           $values = ":$name:(@list)";
409             }
410             elsif ($type eq "file" or $type eq "dir") {
411 0           $values = ":$name:_files";
412             }
413             elsif (not ref $type and $type ne "flag") {
414 0           $values = ":$name";
415             }
416 0           $summary =~ s/['`]/'"'"'/g;
417 0           $summary =~ s/\$/\\\$/g;
418              
419 0 0         my $multiple = $opt->multiple ? "*" : "";
420             # '(-c --count)'{-c,--count}'[Number of list items to show]:c' \
421             # '(-a --all)'{-a,--all}'[Show all list items]' \
422             my @names = map {
423 0 0         length > 1 ? "--$_" : "-$_"
  0            
424             } ($name, @$aliases);
425 0           for my $name (@names) {
426 0           my $str = "'$multiple$name\[$summary\]$values'";
427 0           push @options, $indent . " $str";
428             }
429             }
430 0           my $string = join " \\\n", @options;
431 0           return $string;
432             }
433              
434              
435             1;
436              
437             __DATA__