File Coverage

blib/lib/App/Spec/Completion/Bash.pm
Criterion Covered Total %
statement 15 252 5.9
branch 0 80 0.0
condition 0 16 0.0
subroutine 5 14 35.7
pod 8 8 100.0
total 28 370 7.5


line stmt bran cond sub pod time code
1             # ABSTRACT: Shell Completion generator for bash
2 1     1   1028 use strict;
  1         2  
  1         30  
3 1     1   5 use warnings;
  1         2  
  1         46  
4             package App::Spec::Completion::Bash;
5              
6             our $VERSION = '0.013'; # VERSION
7              
8 1     1   6 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   832 no warnings 'uninitialized';
  1         2  
  1         229  
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   9 no warnings 'uninitialized';
  1         2  
  1         2648  
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         if ($enum) {
    0          
    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 =~ m/^file(name)?\z/) {
333 0           $comp_value .= <<"EOM";
334             ${indent} compopt -o filenames
335             ${indent} return
336             EOM
337             }
338             elsif ($type =~ m/^dir(name)?\z/) {
339 0           $comp_value .= <<"EOM";
340             ${indent} compopt -o dirnames
341             ${indent} return
342             EOM
343             }
344             elsif ($opt->completion) {
345             my $function_name = $self->dynamic_completion(
346             option => $opt,
347             level => $level,
348             previous => $args{previous},
349             functions => $args{functions},
350 0           );
351 0           $comp_value .= <<"EOM";
352             ${indent} $function_name
353             EOM
354             }
355 0           $comp_value .= $indent . " ;;\n";
356             }
357              
358 0           return ($comp_value);
359             }
360              
361             sub dynamic_completion {
362 0     0 1   my ($self, %args) = @_;
363 0           my $functions = $args{functions};
364 0   0       my $previous = $args{previous} || [];
365 0           my $p = $args{option};
366 0           my $level = $args{level};
367 0           my $indent = ' ' x $level;
368 0           my $name = $p->name;
369 0           my $shell_name = $name;
370 0           $name =~ tr/^A-Za-z0-9_:-/_/c;
371 0           $shell_name =~ tr/^A-Za-z0-9_/_/c;
372              
373 0           my $def = $p->completion;
374 0           my ($op, $command, $command_string);
375 0 0 0       if (not ref $def and $def == 1) {
    0          
376 0 0         my $possible_values = $p->values or die "Error for '$name': completion: 1 but 'values' not defined";
377 0 0         $op = $possible_values->{op} or die "Error for '$name': 'values' needs an 'op'";
378             }
379             elsif (ref $def) {
380 0           $op = $def->{op};
381 0           $command = $def->{command};
382 0           $command_string = $def->{command_string};
383             }
384             else {
385 0           die "Error for '$name': invalid value for 'completion'";
386             }
387              
388 0           my $appname = $self->spec->name;
389 0 0         my $function_name = "_${appname}_"
390             . join ("_", @$previous)
391             . "_" . ($p->isa("App::Spec::Option") ? "option" : "param")
392             . "_" . $shell_name . "_completion";
393              
394 0           my $function;
395 0 0 0       if ($op) {
    0          
396 0           $function = <<"EOM";
397             $function_name() \{
398             local __dynamic_completion
399             __dynamic_completion=\$(PERL5_APPSPECRUN_SHELL=bash PERL5_APPSPECRUN_COMPLETION_PARAMETER='$name' \${words[@]})
400             __${appname}_dynamic_comp '$name' "\$__dynamic_completion"
401             \}
402             EOM
403             }
404             elsif ($command or $command_string) {
405              
406 0           my $string = '';
407 0 0         if ($command) {
    0          
408 0           my @args;
409              
410 0           for my $arg (@$command) {
411 0 0         unless (ref $arg) {
412 0           push @args, "'$arg'";
413 0           next;
414             }
415 0 0         if (my $replace = $arg->{replace}) {
416 0 0         if (ref $replace eq 'ARRAY') {
417 0           my @repl = @$replace;
418 0 0         if ($replace->[0] eq 'SHELL_WORDS') {
419 0           my $num = $replace->[1];
420 0           my $index = "\$cword";
421 0 0         if ($num ne 'CURRENT') {
422 0 0         if ($num =~ m/^-/) {
423 0           $index .= $num;
424             }
425             else {
426 0           $index = $num - 1;
427             }
428             }
429 0           my $string = qq{"\$\{words\[$index\]\}"};
430 0           push @args, $string;
431             }
432             }
433             else {
434 0 0         if ($replace eq "SELF") {
435 0           push @args, "\$program";
436             }
437             }
438             }
439             }
440 0           $string = "@args";
441             }
442             elsif (defined $command_string) {
443 0           $string = $command_string;
444             }
445 0           my $varname = "__${name}_completion";
446              
447 0           chomp $string;
448 0           $function = <<"EOM";
449             $function_name() \{
450             local CURRENT_WORD="\${words\[\$cword\]\}"
451             local param_$shell_name="\$($string)"
452             _${appname}_compreply "\$param_$shell_name"
453             \}
454             EOM
455             }
456 0           push @$functions, $function;
457 0           return $function_name;
458             }
459              
460             # sub list_to_alternative {
461             # my ($self, %args) = @_;
462             # my $list = $args{list};
463             # my $maxlength = 0;
464             # for (@$list) {
465             # if (length($_) > $maxlength) {
466             # $maxlength = length $_;
467             # }
468             # }
469             # my @alt = map {
470             # my ($alt_name, $summary);
471             # if (ref $_ eq 'ARRAY') {
472             # ($alt_name, $summary) = @$_;
473             # }
474             # else {
475             # ($alt_name, $summary) = ($_, '');
476             # }
477             # $summary //= '';
478             # $alt_name =~ s/:/\\\\:/g;
479             # $summary =~ s/['`]/'"'"'/g;
480             # $summary =~ s/\$/\\\$/g;
481             # if (length $summary) {
482             # $alt_name .= " " x ($maxlength - length($alt_name));
483             # }
484             # $alt_name;
485             # } @$list;
486             # return join '', map { "$_\n" } @alt;
487             # }
488              
489             sub completion_parameter {
490 0     0 1   my ($self, %args) = @_;
491 0           my $spec = $self->spec;
492 0           my $appname = $spec->name;
493 0           my $param = $args{parameter};
494 0           my $name = $param->name;
495 0           my $level = $args{level};
496 0           my $indent = " " x $level;
497              
498 0           my $comp = '';
499              
500 0           my $type = $param->type;
501 0           my $enum = $param->enum;
502 0 0         if ($enum) {
    0          
    0          
    0          
503 0           my @list = @$enum;
504 0           for (@list) {
505 0           s/['`]/'"'"'/g;
506 0           s/\\/\\\\/g;
507 0           s/ /\\\\ /g;
508 0           s/\$/\\\$/g;
509 0           $_ = qq{"$_"};
510             }
511 0           $comp = <<"EOM";
512             ${indent} _${appname}_compreply @list
513             EOM
514             }
515             elsif ($type =~ m/^file(name)?\z/) {
516 0           $comp = <<"EOM";
517             ${indent} compopt -o filenames
518             EOM
519             }
520             elsif ($type =~ m/^dir(name)?\z/) {
521 0           $comp = <<"EOM";
522             ${indent} compopt -o dirnames
523             EOM
524             }
525             elsif ($param->completion) {
526             my $function_name = $self->dynamic_completion(
527             option => $param,
528             level => $level,
529             previous => $args{previous},
530             functions => $args{functions},
531 0           );
532 0           $comp .= <<"EOM";
533             ${indent} $function_name
534             EOM
535             }
536 0           return $comp;
537             }
538              
539             sub _functions {
540 0     0     my ($self) = @_;
541 0           my $string = <<'EOM';
542             __APPNAME_dynamic_comp() {
543             local argname="$1"
544             local arg="$2"
545             local name desc cols desclength formatted
546             local comp=()
547             local max=0
548              
549             while read -r line; do
550             name="$line"
551             desc="$line"
552             name="${name%$'\t'*}"
553             if [[ "${#name}" -gt "$max" ]]; then
554             max="${#name}"
555             fi
556             done <<< "$arg"
557              
558             while read -r line; do
559             name="$line"
560             desc="$line"
561             name="${name%$'\t'*}"
562             desc="${desc/*$'\t'}"
563             if [[ -n "$desc" && "$desc" != "$name" ]]; then
564             # TODO portable?
565             cols=`tput cols`
566             [[ -z $cols ]] && cols=80
567             desclength=`expr $cols - 4 - $max`
568             formatted=`printf "%-*s -- %-*s" "$max" "$name" "$desclength" "$desc"`
569             comp+=("$formatted")
570             else
571             comp+=("'$name'")
572             fi
573             done <<< "$arg"
574             _APPNAME_compreply ${comp[@]}
575             }
576              
577             function __APPNAME_handle_options() {
578             local i j
579             declare -a copy
580             local last="${MYWORDS[$INDEX]}"
581             local max=`expr ${#MYWORDS[@]} - 1`
582             for ((i=0; i<$max; i++))
583             do
584             local word="${MYWORDS[$i]}"
585             local found=
586             for ((j=0; j<${#OPTIONS[@]}; j+=2))
587             do
588             local option="${OPTIONS[$j]}"
589             if [[ "$word" == "$option" ]]; then
590             found=1
591             i=`expr $i + 1`
592             break
593             fi
594             done
595             if [[ -n $found && $i -lt $max ]]; then
596             INDEX=`expr $INDEX - 2`
597             else
598             copy+=("$word")
599             fi
600             done
601             MYWORDS=("${copy[@]}" "$last")
602             }
603              
604             function __APPNAME_handle_flags() {
605             local i j
606             declare -a copy
607             local last="${MYWORDS[$INDEX]}"
608             local max=`expr ${#MYWORDS[@]} - 1`
609             for ((i=0; i<$max; i++))
610             do
611             local word="${MYWORDS[$i]}"
612             local found=
613             for ((j=0; j<${#FLAGS[@]}; j+=2))
614             do
615             local flag="${FLAGS[$j]}"
616             if [[ "$word" == "$flag" ]]; then
617             found=1
618             break
619             fi
620             done
621             if [[ -n $found ]]; then
622             INDEX=`expr $INDEX - 1`
623             else
624             copy+=("$word")
625             fi
626             done
627             MYWORDS=("${copy[@]}" "$last")
628             }
629              
630             __APPNAME_handle_options_flags() {
631             __APPNAME_handle_options
632             __APPNAME_handle_flags
633             }
634              
635             __comp_current_options() {
636             local always="$1"
637             if [[ -n $always || ${MYWORDS[$INDEX]} =~ ^- ]]; then
638              
639             local options_spec=''
640             local j=
641              
642             for ((j=0; j<${#FLAGS[@]}; j+=2))
643             do
644             local name="${FLAGS[$j]}"
645             local desc="${FLAGS[$j+1]}"
646             options_spec+="$name"$'\t'"$desc"$'\n'
647             done
648              
649             for ((j=0; j<${#OPTIONS[@]}; j+=2))
650             do
651             local name="${OPTIONS[$j]}"
652             local desc="${OPTIONS[$j+1]}"
653             options_spec+="$name"$'\t'"$desc"$'\n'
654             done
655             __APPNAME_dynamic_comp 'options' "$options_spec"
656              
657             return 1
658             else
659             return 0
660             fi
661             }
662              
663             EOM
664 0           my $appname = $self->spec->name;
665 0           $string =~ s/APPNAME/$appname/g;
666 0           return $string;
667             }
668              
669             1;
670              
671             __DATA__