File Coverage

blib/lib/App/Spec/Completion/Bash.pm
Criterion Covered Total %
statement 15 248 6.0
branch 0 76 0.0
condition 0 22 0.0
subroutine 5 14 35.7
pod 8 8 100.0
total 28 368 7.6


line stmt bran cond sub pod time code
1             # ABSTRACT: Shell Completion generator for bash
2 1     1   838 use strict;
  1         2  
  1         26  
3 1     1   6 use warnings;
  1         2  
  1         40  
4             package App::Spec::Completion::Bash;
5              
6             our $VERSION = '0.012'; # VERSION
7              
8 1     1   5 use Moo;
  1         2  
  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              
16 0           my $appspec_version = App::Spec->VERSION;
17              
18 0           my $functions = [];
19 0           my $completion_outer = $self->completion_commands(
20             commands => $spec->subcommands,
21             options => $spec->options,
22             parameters => $spec->parameters,
23             level => 1,
24             functions => $functions,
25             );
26              
27 0           my $global_options = $spec->options;
28 0           my ($flags_string, $options_string) = $self->flags_options($global_options);
29 0           my $body = <<"EOM";
30             #!bash
31              
32             # Generated with perl module App::Spec v$appspec_version
33              
34             _$appname() \{
35              
36             COMPREPLY=()
37             local program=$appname
38             local cur prev words cword
39             _init_completion -n : || return
40             declare -a FLAGS
41             declare -a OPTIONS
42             declare -a MYWORDS
43              
44             local INDEX=`expr \$cword - 1`
45             MYWORDS=("\$\{words[@]:1:\$cword\}")
46              
47             FLAGS=($flags_string)
48             OPTIONS=($options_string)
49             __${appname}_handle_options_flags
50              
51             $completion_outer
52             \}
53              
54             _${appname}_compreply() \{
55             local prefix=""
56             cur="\$(printf '%q' "\$cur")"
57             IFS=\$'\\n' COMPREPLY=(\$(compgen -P "\$prefix" -W "\$*" -- "\$cur"))
58             __ltrim_colon_completions "\$prefix\$cur"
59              
60             # http://stackoverflow.com/questions/7267185/bash-autocompletion-add-description-for-possible-completions
61             if [[ \$\{#COMPREPLY[*]\} -eq 1 ]]; then # Only one completion
62             COMPREPLY=( "\$\{COMPREPLY[0]%% -- *\}" ) # Remove ' -- ' and everything after
63             COMPREPLY=( "\$\{COMPREPLY[0]%%+( )\}" ) # Remove trailing spaces
64             fi
65             \}
66              
67 0           @{[ join '', @$functions ]}
68             EOM
69 0           my $static_functions = $self->_functions;
70              
71 0           $body .= <<"EOM";
72             $static_functions
73             complete -o default -F _$appname $appname
74             EOM
75              
76 0           return $body;
77             }
78              
79             sub flags_options {
80 0     0 1   my ($self, $options) = @_;
81 0           my @flags;
82             my @opt;
83 0           for my $o (@$options) {
84 0           my $name = $o->name;
85 0           my $aliases = $o->aliases;
86 0           my $summary = $o->summary;
87 0           my @names = ($name, @$aliases);
88 0           ($summary, @names) = $self->escape_singlequote( $summary, @names );
89             @names = map {
90 0 0         length $_ > 1 ? "--$_" : "-$_"
  0            
91             } @names;
92              
93             my @items = map {
94 0           ("'$_'", "'$summary'")
  0            
95             } @names;
96              
97 0 0         if ($o->type eq 'flag') {
98 0           push @flags, @items;
99             }
100             else {
101 0           push @opt, @items;
102             }
103             }
104 0           return ("@flags", "@opt");
105             }
106              
107             sub escape_singlequote {
108 0     0 1   my ($self, @strings) = @_;
109 0           my @result;
110 0           for my $string (@strings) {
111 1     1   667 no warnings 'uninitialized';
  1         2  
  1         181  
112 0           $string =~ s/[']/'"\\\\'"'/g;
113 0           push @result, $string;
114             }
115 0 0         return wantarray ? @result : $result[0];
116             }
117              
118             sub completion_commands {
119 0     0 1   my ($self, %args) = @_;
120 0           my $spec = $self->spec;
121 0           my $appname = $spec->name;
122 0           my $functions = $args{functions};
123 0   0       my $previous = $args{previous} || [];
124 0           my $commands = $args{commands};
125 0           my $options = $args{options};
126 0           my $parameters = $args{parameters};
127 0           my $level = $args{level};
128 0           my $indent = " " x $level;
129              
130             my @commands = map {
131 0           my $name = $_;
132 0           my $summary = $commands->{ $_ }->summary;
133 0           for ($name, $summary) {
134 1     1   8 no warnings 'uninitialized';
  1         2  
  1         1911  
135 0           s/['`]/'"'"'/g;
136 0           s/\$/\\\$/g;
137             }
138 0 0         "'$name'" . (length $summary ? q{$'\t'} . "'$summary'" : '')
139 0           } sort grep { not m/^_/ } keys %$commands;
  0            
140 0           my $cmds = join q{$'\\n'}, @commands;
141              
142 0           my $index = $level - 1;
143 0           my $subc = '';
144 0 0         if (keys %$commands) {
145 0           $subc = <<"EOM";
146             $indent# subcmds
147             ${indent}case \$\{MYWORDS\[$index\]\} in
148             EOM
149             }
150              
151 0           for my $name (sort keys %$commands) {
152 0           my $cmd_spec = $commands->{ $name };
153 0           my ($flags_string, $options_string) = $self->flags_options($cmd_spec->options);
154 0           $subc .= <<"EOM";
155             ${indent} $name)
156             EOM
157 0 0         $subc .= $indent . " FLAGS+=($flags_string)\n" if $flags_string;
158 0 0         $subc .= $indent . " OPTIONS+=($options_string)\n" if $options_string;
159 0           $subc .= <<"EOM";
160             ${indent} __${appname}_handle_options_flags
161             EOM
162 0           my $subcommands = $cmd_spec->subcommands;
163 0           my $parameters = $cmd_spec->parameters;
164 0           my $cmd_options = $cmd_spec->options;
165 0 0 0       if (keys %$subcommands or @$cmd_options or @$parameters) {
      0        
166 0           my $comp = $self->completion_commands(
167             commands => $subcommands,
168             options => [ @$options, @$cmd_options ],
169             parameters => $parameters,
170             level => $level + 1,
171             previous => [@$previous, $name],
172             functions => $functions,
173             );
174 0           $subc .= $comp;
175             }
176             else {
177 0           $subc .= $indent . " __comp_current_options true || return # no subcmds, no params/opts\n";
178             }
179 0           $subc .= <<"EOM";
180             ${indent} ;;
181             EOM
182             }
183              
184 0           my $option_comp;
185 0           my $param_comp = '';
186 0           my $subc_comp = '';
187 0 0         if (@$options) {
188             ($option_comp) = $self->completion_options(
189             options => $options,
190             level => $level,
191             functions => $args{functions},
192 0           previous => $previous,
193             );
194             }
195 0 0         if (@$parameters) {
196 0           $param_comp = $self->completion_parameters(
197             parameters => $parameters,
198             level => $level,
199             previous => $previous,
200             functions => $functions,
201             );
202 0           $param_comp = <<"EOM";
203             $param_comp
204             EOM
205             }
206              
207 0 0         if (keys %$commands) {
208 0           $subc .= <<"EOM";
209             ${indent}esac
210             EOM
211 0           $subc_comp = <<"EOM";
212             ${indent}case \$INDEX in
213              
214             ${indent}$index)
215             ${indent} __comp_current_options || return
216             ${indent} __${appname}_dynamic_comp 'commands' $cmds
217              
218             ${indent};;
219             ${indent}*)
220             $subc
221             ${indent};;
222             ${indent}esac
223             EOM
224 0           return $subc_comp;
225             }
226              
227 0           my $completion = <<"EOM";
228             ${indent}case \$\{MYWORDS[\$INDEX-1]\} in
229             $option_comp
230             ${indent}esac
231             ${indent}case \$INDEX in
232             $param_comp
233             ${indent}*)
234             ${indent} __comp_current_options || return
235             ${indent};;
236             ${indent}esac
237             EOM
238 0           return $completion;
239             }
240              
241             sub completion_parameters {
242 0     0 1   my ($self, %args) = @_;
243 0           my $spec = $self->spec;
244 0           my $appname = $spec->name;
245 0           my $parameters = $args{parameters};
246 0           my $level = $args{level};
247 0           my $indent = " " x $level;
248              
249 0           my $comp = '';
250              
251 0           for my $i (0 .. $#$parameters) {
252 0           my $param = $parameters->[ $i ];
253 0           my $name = $param->name;
254 0           my $num = $level + $i - 1;
255 0           $comp .= $indent . " $num)\n";
256 0           $comp .= $indent . " __comp_current_options || return\n";
257             $comp .= $self->completion_parameter(
258             parameter => $param,
259             level => $level + 1,
260             functions => $args{functions},
261             previous => $args{previous},
262 0           );
263 0           $comp .= $indent . " ;;\n";
264             }
265              
266 0           return $comp;
267             }
268              
269             sub completion_options {
270 0     0 1   my ($self, %args) = @_;
271              
272 0           my $appname = $self->spec->name;
273 0           my $options = $args{options};
274 0           my $level = $args{level};
275 0           my $indent = " " x $level;
276              
277 0           my @comp_options;
278             my @comp_values;
279 0           my $comp_value = '';
280 0           my $maxlength = 0;
281 0           for my $opt (@$options) {
282 0           my $name = $opt->name;
283 0           my $aliases = $opt->aliases;
284 0           my @names = ($name, @$aliases);
285 0           for my $n (@names) {
286 0           my $length = length $n;
287 0 0         $length = $length > 1 ? $length+2 : $length+1;
288 0 0         $maxlength = $length if $length > $maxlength;
289             }
290             }
291 0           for my $i (0 .. $#$options) {
292 0           my $opt = $options->[ $i ];
293 0           my $name = $opt->name;
294 0           my $type = $opt->type;
295 0 0         next if $type eq "flag";
296 0           my $enum = $opt->enum;
297 0           my $summary = $opt->summary;
298 0           $summary =~ s/['`]/'"'"'/g;
299 0           $summary =~ s/\$/\\\$/g;
300 0           my $aliases = $opt->aliases;
301 0           my @names = ($name, @$aliases);
302 0           my @option_strings;
303 0           for my $n (@names) {
304 0 0         my $dash = length $n > 1 ? "--" : "-";
305 0           my $option_string = "$dash$n";
306 0           push @option_strings, $option_string;
307 0           my $length = length $option_string;
308 0           $option_string .= " " x ($maxlength - $length);
309 0 0         my $string = length $summary
310             ? qq{'$option_string -- $summary'}
311             : qq{'$option_string'};
312 0           push @comp_options, $string;
313             }
314              
315 0           $comp_value .= <<"EOM";
316 0           ${indent} @{[ join '|', @option_strings ]})
317             EOM
318 0 0 0       if ($enum) {
    0          
    0          
319 0           my @list = @$enum;
320 0           for (@list) {
321 0           s/['`]/'"'"'/g;
322 0           s/\\/\\\\/g;
323 0           s/ /\\\\\\\\ /g;
324 0           s/\$/\\\$/g;
325 0           $_ = qq{"$_"};
326             }
327 0           $comp_value .= <<"EOM";
328             ${indent} _${appname}_compreply @list
329             ${indent} return
330             EOM
331             }
332             elsif ($type eq "file" or $type eq "dir") {
333             }
334             elsif ($opt->completion) {
335             my $function_name = $self->dynamic_completion(
336             option => $opt,
337             level => $level,
338             previous => $args{previous},
339             functions => $args{functions},
340 0           );
341 0           $comp_value .= <<"EOM";
342             ${indent} $function_name
343             EOM
344             }
345 0           $comp_value .= $indent . " ;;\n";
346             }
347              
348 0           return ($comp_value);
349             }
350              
351             sub dynamic_completion {
352 0     0 1   my ($self, %args) = @_;
353 0           my $functions = $args{functions};
354 0   0       my $previous = $args{previous} || [];
355 0           my $p = $args{option};
356 0           my $level = $args{level};
357 0           my $indent = ' ' x $level;
358 0           my $name = $p->name;
359 0           my $shell_name = $name;
360 0           $name =~ tr/^A-Za-z0-9_:-/_/c;
361 0           $shell_name =~ tr/^A-Za-z0-9_/_/c;
362              
363 0           my $def = $p->completion;
364 0           my ($op, $command, $command_string);
365 0 0 0       if (not ref $def and $def == 1) {
    0          
366 0 0         my $possible_values = $p->values or die "Error for '$name': completion: 1 but 'values' not defined";
367 0 0         $op = $possible_values->{op} or die "Error for '$name': 'values' needs an 'op'";
368             }
369             elsif (ref $def) {
370 0           $op = $def->{op};
371 0           $command = $def->{command};
372 0           $command_string = $def->{command_string};
373             }
374             else {
375 0           die "Error for '$name': invalid value for 'completion'";
376             }
377              
378 0           my $appname = $self->spec->name;
379 0 0         my $function_name = "_${appname}_"
380             . join ("_", @$previous)
381             . "_" . ($p->isa("App::Spec::Option") ? "option" : "param")
382             . "_" . $shell_name . "_completion";
383              
384 0           my $function;
385 0 0 0       if ($op) {
    0          
386 0           $function = <<"EOM";
387             $function_name() \{
388             local __dynamic_completion
389             __dynamic_completion=\$(PERL5_APPSPECRUN_SHELL=bash PERL5_APPSPECRUN_COMPLETION_PARAMETER='$name' \${words[@]})
390             __${appname}_dynamic_comp '$name' "\$__dynamic_completion"
391             \}
392             EOM
393             }
394             elsif ($command or $command_string) {
395              
396 0           my $string = '';
397 0 0         if ($command) {
    0          
398 0           my @args;
399              
400 0           for my $arg (@$command) {
401 0 0         unless (ref $arg) {
402 0           push @args, "'$arg'";
403 0           next;
404             }
405 0 0         if (my $replace = $arg->{replace}) {
406 0 0         if (ref $replace eq 'ARRAY') {
407 0           my @repl = @$replace;
408 0 0         if ($replace->[0] eq 'SHELL_WORDS') {
409 0           my $num = $replace->[1];
410 0           my $index = "\$cword";
411 0 0         if ($num ne 'CURRENT') {
412 0 0         if ($num =~ m/^-/) {
413 0           $index .= $num;
414             }
415             else {
416 0           $index = $num - 1;
417             }
418             }
419 0           my $string = qq{"\$\{words\[$index\]\}"};
420 0           push @args, $string;
421             }
422             }
423             else {
424 0 0         if ($replace eq "SELF") {
425 0           push @args, "\$program";
426             }
427             }
428             }
429             }
430 0           $string = "@args";
431             }
432             elsif (defined $command_string) {
433 0           $string = $command_string;
434             }
435 0           my $varname = "__${name}_completion";
436              
437 0           chomp $string;
438 0           $function = <<"EOM";
439             $function_name() \{
440             local CURRENT_WORD="\${words\[\$cword\]\}"
441             local param_$shell_name="\$($string)"
442             _${appname}_compreply "\$param_$shell_name"
443             \}
444             EOM
445             }
446 0           push @$functions, $function;
447 0           return $function_name;
448             }
449              
450             # sub list_to_alternative {
451             # my ($self, %args) = @_;
452             # my $list = $args{list};
453             # my $maxlength = 0;
454             # for (@$list) {
455             # if (length($_) > $maxlength) {
456             # $maxlength = length $_;
457             # }
458             # }
459             # my @alt = map {
460             # my ($alt_name, $summary);
461             # if (ref $_ eq 'ARRAY') {
462             # ($alt_name, $summary) = @$_;
463             # }
464             # else {
465             # ($alt_name, $summary) = ($_, '');
466             # }
467             # $summary //= '';
468             # $alt_name =~ s/:/\\\\:/g;
469             # $summary =~ s/['`]/'"'"'/g;
470             # $summary =~ s/\$/\\\$/g;
471             # if (length $summary) {
472             # $alt_name .= " " x ($maxlength - length($alt_name));
473             # }
474             # $alt_name;
475             # } @$list;
476             # return join '', map { "$_\n" } @alt;
477             # }
478              
479             sub completion_parameter {
480 0     0 1   my ($self, %args) = @_;
481 0           my $spec = $self->spec;
482 0           my $appname = $spec->name;
483 0           my $param = $args{parameter};
484 0           my $name = $param->name;
485 0           my $level = $args{level};
486 0           my $indent = " " x $level;
487              
488 0           my $comp = '';
489              
490 0           my $type = $param->type;
491 0           my $enum = $param->enum;
492 0 0 0       if ($enum) {
    0          
    0          
493 0           my @list = @$enum;
494 0           for (@list) {
495 0           s/['`]/'"'"'/g;
496 0           s/\\/\\\\/g;
497 0           s/ /\\\\ /g;
498 0           s/\$/\\\$/g;
499 0           $_ = qq{"$_"};
500             }
501 0           $comp = <<"EOM";
502             ${indent} _${appname}_compreply @list
503             EOM
504             }
505             elsif ($type eq "file" or $type eq "dir") {
506             }
507             elsif ($param->completion) {
508             my $function_name = $self->dynamic_completion(
509             option => $param,
510             level => $level,
511             previous => $args{previous},
512             functions => $args{functions},
513 0           );
514 0           $comp .= <<"EOM";
515             ${indent} $function_name
516             EOM
517             }
518 0           return $comp;
519             }
520              
521             sub _functions {
522 0     0     my ($self) = @_;
523 0           my $string = <<'EOM';
524             __APPNAME_dynamic_comp() {
525             local argname="$1"
526             local arg="$2"
527             local name desc cols desclength formatted
528             local comp=()
529             local max=0
530              
531             while read -r line; do
532             name="$line"
533             desc="$line"
534             name="${name%$'\t'*}"
535             if [[ "${#name}" -gt "$max" ]]; then
536             max="${#name}"
537             fi
538             done <<< "$arg"
539              
540             while read -r line; do
541             name="$line"
542             desc="$line"
543             name="${name%$'\t'*}"
544             desc="${desc/*$'\t'}"
545             if [[ -n "$desc" && "$desc" != "$name" ]]; then
546             # TODO portable?
547             cols=`tput cols`
548             [[ -z $cols ]] && cols=80
549             desclength=`expr $cols - 4 - $max`
550             formatted=`printf "%-*s -- %-*s" "$max" "$name" "$desclength" "$desc"`
551             comp+=("$formatted")
552             else
553             comp+=("'$name'")
554             fi
555             done <<< "$arg"
556             _APPNAME_compreply ${comp[@]}
557             }
558              
559             function __APPNAME_handle_options() {
560             local i j
561             declare -a copy
562             local last="${MYWORDS[$INDEX]}"
563             local max=`expr ${#MYWORDS[@]} - 1`
564             for ((i=0; i<$max; i++))
565             do
566             local word="${MYWORDS[$i]}"
567             local found=
568             for ((j=0; j<${#OPTIONS[@]}; j+=2))
569             do
570             local option="${OPTIONS[$j]}"
571             if [[ "$word" == "$option" ]]; then
572             found=1
573             i=`expr $i + 1`
574             break
575             fi
576             done
577             if [[ -n $found && $i -lt $max ]]; then
578             INDEX=`expr $INDEX - 2`
579             else
580             copy+=("$word")
581             fi
582             done
583             MYWORDS=("${copy[@]}" "$last")
584             }
585              
586             function __APPNAME_handle_flags() {
587             local i j
588             declare -a copy
589             local last="${MYWORDS[$INDEX]}"
590             local max=`expr ${#MYWORDS[@]} - 1`
591             for ((i=0; i<$max; i++))
592             do
593             local word="${MYWORDS[$i]}"
594             local found=
595             for ((j=0; j<${#FLAGS[@]}; j+=2))
596             do
597             local flag="${FLAGS[$j]}"
598             if [[ "$word" == "$flag" ]]; then
599             found=1
600             break
601             fi
602             done
603             if [[ -n $found ]]; then
604             INDEX=`expr $INDEX - 1`
605             else
606             copy+=("$word")
607             fi
608             done
609             MYWORDS=("${copy[@]}" "$last")
610             }
611              
612             __APPNAME_handle_options_flags() {
613             __APPNAME_handle_options
614             __APPNAME_handle_flags
615             }
616              
617             __comp_current_options() {
618             local always="$1"
619             if [[ -n $always || ${MYWORDS[$INDEX]} =~ ^- ]]; then
620              
621             local options_spec=''
622             local j=
623              
624             for ((j=0; j<${#FLAGS[@]}; j+=2))
625             do
626             local name="${FLAGS[$j]}"
627             local desc="${FLAGS[$j+1]}"
628             options_spec+="$name"$'\t'"$desc"$'\n'
629             done
630              
631             for ((j=0; j<${#OPTIONS[@]}; j+=2))
632             do
633             local name="${OPTIONS[$j]}"
634             local desc="${OPTIONS[$j+1]}"
635             options_spec+="$name"$'\t'"$desc"$'\n'
636             done
637             __APPNAME_dynamic_comp 'options' "$options_spec"
638              
639             return 1
640             else
641             return 0
642             fi
643             }
644              
645             EOM
646 0           my $appname = $self->spec->name;
647 0           $string =~ s/APPNAME/$appname/g;
648 0           return $string;
649             }
650              
651             1;
652              
653             __DATA__