File Coverage

blib/lib/App/Spec/Completion/Bash.pm
Criterion Covered Total %
statement 15 256 5.8
branch 0 84 0.0
condition 0 16 0.0
subroutine 5 14 35.7
pod 8 8 100.0
total 28 378 7.4


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