File Coverage

blib/lib/Complete/Bash.pm
Criterion Covered Total %
statement 167 226 73.8
branch 99 160 61.8
condition 54 85 63.5
subroutine 18 19 94.7
pod 4 4 100.0
total 342 494 69.2


line stmt bran cond sub pod time code
1             package Complete::Bash;
2              
3             our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
4             our $DATE = '2020-04-16'; # DATE
5             our $DIST = 'Complete-Bash'; # DIST
6             our $VERSION = '0.335'; # VERSION
7              
8 4     4   58245 use 5.010001;
  4         31  
9 4     4   18 use strict;
  4         6  
  4         66  
10 4     4   25 use warnings;
  4         6  
  4         115  
11 4     4   5904 use Log::ger;
  4         204  
  4         16  
12              
13             require Exporter;
14             our @ISA = qw(Exporter);
15             our @EXPORT_OK = qw(
16             point
17             parse_cmdline
18             join_wordbreak_words
19             format_completion
20             );
21              
22             our %SPEC;
23              
24             $SPEC{':package'} = {
25             v => 1.1,
26             summary => 'Completion routines for bash shell',
27             };
28              
29             sub _expand_tilde {
30 4     4   12 my ($user, $slash) = @_;
31 4         5 my @ent;
32 4 50       6 if (length $user) {
33 0         0 @ent = getpwnam($user);
34             } else {
35 4         295 @ent = getpwuid($>);
36 4         12 $user = $ent[0];
37             }
38 4 50       30 return $ent[7] . $slash if @ent;
39 0         0 "~$user$slash"; # return as-is when failed
40             }
41              
42             sub _add_unquoted {
43 4     4   1233 no warnings 'uninitialized';
  4         6  
  4         655  
44              
45 138     138   236 my ($word, $is_cur_word, $after_ws) = @_;
46              
47             #say "D:add_unquoted word=$word is_cur_word=$is_cur_word after_ws=$after_ws";
48              
49 138         305 $word =~ s!^(~)(\w*)(/|\z) | # 1) tilde 2) username 3) optional slash
50             \\(.) | # 4) escaped char
51             \$(\w+) # 5) variable name
52             !
53             $1 ? (not($after_ws) || $is_cur_word ? "$1$2$3" : _expand_tilde($2, $3)) :
54             $4 ? $4 :
55 20 100 100     100 ($is_cur_word ? "\$$5" : $ENV{$5})
    100          
    100          
    100          
56             !egx;
57 138         238 $word;
58             }
59              
60             sub _add_double_quoted {
61 4     4   26 no warnings 'uninitialized';
  4         13  
  4         1420  
62              
63 23     23   46 my ($word, $is_cur_word) = @_;
64              
65 23         59 $word =~ s!\\(.) | # 1) escaped char
66             \$(\w+) # 2) variable name
67             !
68             $1 ? $1 :
69 3 50       13 ($is_cur_word ? "\$$2" : $ENV{$2})
    100          
70             !egx;
71 23         37 $word;
72             }
73              
74             sub _add_single_quoted {
75 20     20   35 my $word = shift;
76 20         30 $word =~ s/\\(.)/$1/g;
77 20         26 $word;
78             }
79              
80             $SPEC{point} = {
81             v => 1.1,
82             summary => 'Return line with point marked by a marker',
83             description => <<'_',
84              
85             This is a utility function useful for testing/debugging. `parse_cmdline()`
86             expects a command-line and a cursor position (`$line`, `$point`). This routine
87             expects `$line` with a marker character (by default it's the caret, `^`) and
88             return (`$line`, `$point`) to feed to `parse_cmdline()`.
89              
90             Example:
91              
92             point("^foo") # => ("foo", 0)
93             point("fo^o") # => ("foo", 2)
94              
95             _
96             args_as => 'array',
97             args => {
98             cmdline => {
99             summary => 'Command-line which contains a marker character',
100             schema => 'str*',
101             pos => 0,
102             },
103             marker => {
104             summary => 'Marker character',
105             schema => ['str*', len=>1],
106             default => '^',
107             pos => 1,
108             },
109             },
110             result_naked => 1,
111             };
112             sub point {
113 91     91 1 22996 my ($line, $marker) = @_;
114 91   100     342 $marker //= '^';
115              
116 91         176 my $point = index($line, $marker);
117 91 100       164 die "BUG: No marker '$marker' in line <$line>" unless $point >= 0;
118 90         379 $line =~ s/\Q$marker\E//;
119 90         308 ($line, $point);
120             }
121              
122             $SPEC{parse_cmdline} = {
123             v => 1.1,
124             summary => 'Parse shell command-line for processing by completion routines',
125             description => <<'_',
126              
127             This function basically converts `COMP_LINE` (str) and `COMP_POINT` (int) into
128             something like (but not exactly the same as) `COMP_WORDS` (array) and
129             `COMP_CWORD` (int) that bash supplies to shell functions.
130              
131             The differences with bash are (these differences are mostly for parsing
132             convenience for programs that use this routine; this comparison is made against
133             bash versions 4.2-4.3):
134              
135             1) quotes and backslashes are stripped (bash's `COMP_WORDS` contains all the
136             quotes and backslashes);
137              
138             2) quoted phrase that contains spaces, or phrase that contains escaped spaces is
139             parsed as a single word. For example:
140              
141             command "First argument" Second\ argument
142              
143             bash would split it as (represented as Perl):
144              
145             ["command", "\"First", "argument\"", "Second\\", "argument"]
146              
147             which is not very convenient. We parse it into:
148              
149             ["command", "First argument", "Second argument"]
150              
151             3) variables are substituted with their values from environment variables except
152             for the current word (`COMP_WORDS[COMP_CWORD]`) (bash does not perform
153             variable substitution for `COMP_WORDS`). However, note that special shell
154             variables that are not environment variables like `$0`, `$_`, `$IFS` will not
155             be replaced correctly because bash does not export those variables for us.
156              
157             4) tildes (`~`) are expanded with user's home directory except for the current
158             word (bash does not perform tilde expansion for `COMP_WORDS`);
159              
160             Caveats:
161              
162             * Like bash, we group non-whitespace word-breaking characters into its own word.
163             By default `COMP_WORDBREAKS` is:
164              
165             "'@><=;|&(:
166              
167             So if raw command-line is:
168              
169             command --foo=bar http://example.com:80 mail@example.org Foo::Bar
170              
171             then the parse result will be:
172              
173             ["command", "--foo", "=", "bar", "http", ":", "//example.com", ":", "80", "Foo", "::", "Bar"]
174              
175             which is annoying sometimes. But we follow bash here so we can more easily
176             accept input from a joined `COMP_WORDS` if we write completion bash functions,
177             e.g. (in the example, `foo` is a Perl script):
178              
179             _foo ()
180             {
181             local words=(${COMP_CWORDS[@]})
182             # add things to words, etc
183             local point=... # calculate the new point
184             COMPREPLY=( `COMP_LINE="foo ${words[@]}" COMP_POINT=$point foo` )
185             }
186              
187             To avoid these word-breaking characters to be split/grouped, we can escape
188             them with backslash or quote them, e.g.:
189              
190             command "http://example.com:80" Foo\:\:Bar
191              
192             which bash will parse as:
193              
194             ["command", "\"http://example.com:80\"", "Foo\\:\\:Bar"]
195              
196             and we parse as:
197              
198             ["command", "http://example.com:80", "Foo::Bar"]
199              
200             * Due to the way bash parses the command line (see above), the two below are
201             equivalent:
202              
203             % cmd --foo=bar
204             % cmd --foo = bar
205              
206             Because they both expand to `['--foo', '=', 'bar']`. But obviously
207             does not regard the two as equivalent.
208              
209             _
210             args_as => 'array',
211             args => {
212             cmdline => {
213             summary => 'Command-line, defaults to COMP_LINE environment',
214             schema => 'str*',
215             pos => 0,
216             },
217             point => {
218             summary => 'Point/position to complete in command-line, '.
219             'defaults to COMP_POINT',
220             schema => 'int*',
221             pos => 1,
222             },
223             opts => {
224             summary => 'Options',
225             schema => 'hash*',
226             description => <<'_',
227              
228             Optional. Known options:
229              
230             * `truncate_current_word` (bool). If set to 1, will truncate current word to the
231             position of cursor, for example (`^` marks the position of cursor):
232             `--vers^oo` to `--vers` instead of `--versoo`. This is more convenient when
233             doing tab completion.
234              
235             _
236             schema => 'hash*',
237             pos => 2,
238             },
239             },
240             result => {
241             schema => ['array*', len=>2],
242             description => <<'_',
243              
244             Return a 2-element array: `[$words, $cword]`. `$words` is array of str,
245             equivalent to `COMP_WORDS` provided by bash to shell functions. `$cword` is an
246             integer, roughly equivalent to `COMP_CWORD` provided by bash to shell functions.
247             The word to be completed is at `$words->[$cword]`.
248              
249             Note that COMP_LINE includes the command name. If you want the command-line
250             arguments only (like in `@ARGV`), you need to strip the first element from
251             `$words` and reduce `$cword` by 1.
252              
253              
254             _
255             },
256             result_naked => 1,
257             links => [
258             ],
259             };
260             sub parse_cmdline {
261 4     4   26 no warnings 'uninitialized';
  4         8  
  4         8616  
262 87     87 1 144 my ($line, $point, $opts) = @_;
263              
264 87   33     133 $line //= $ENV{COMP_LINE};
265 87   0     143 $point //= $ENV{COMP_POINT} // 0;
      33        
266              
267 87 50       129 die "$0: COMP_LINE not set, make sure this script is run under ".
268             "bash completion (e.g. through complete -C)\n" unless defined $line;
269              
270             log_trace "[compbash] parse_cmdline(): input: line=<$line> point=<$point>"
271 87 50       153 if $ENV{COMPLETE_BASH_TRACE};
272              
273 87         99 my @words;
274             my $cword;
275 87         92 my $pos = 0;
276 87         91 my $pos_min_ws = 0;
277 87         94 my $after_ws = 1; # XXX what does this variable mean?
278 87         153 my $chunk;
279             my $add_blank;
280 87         0 my $is_cur_word;
281 87         499 $line =~ s!( # 1) everything
282             (")((?: \\\\|\\"|[^"])*)(?:"|\z)(\s*) | # 2) open " 3) content 4) space after
283             (')((?: \\\\|\\'|[^'])*)(?:'|\z)(\s*) | # 5) open ' 6) content 7) space after
284             ((?: \\\\|\\"|\\'|\\=|\\\s|[^"'@><=|&\(:\s])+)(\s*) | # 8) unquoted word 9) space after
285             ([\@><=|&\(:]+) | # 10) non-whitespace word-breaking characters
286             \s+
287             )!
288 192         320 $pos += length($1);
289             #say "D: \$1=<$1> \$2=<$3> \$3=<$3> \$4=<$4> \$5=<$5> \$6=<$6> \$7=<$7> \$8=<$8> \$9=<$9> \$10=<$10>";
290             #say "D:<$1> pos=$pos, point=$point, cword=$cword, after_ws=$after_ws";
291              
292 192 100 100     728 if ($2 || $5 || defined($8)) {
    100 100        
293             # double-quoted/single-quoted/unquoted chunk
294              
295 181 100       265 if (not(defined $cword)) {
296 157 100       318 $pos_min_ws = $pos - length($2 ? $4 : $5 ? $7 : $9);
    100          
297             #say "D:pos_min_ws=$pos_min_ws";
298 157 100       238 if ($point <= $pos_min_ws) {
    100          
299 72 100       111 $cword = @words - ($after_ws ? 0 : 1);
300             } elsif ($point < $pos) {
301 2 50       4 $cword = @words + 1 - ($after_ws ? 0 : 1);
302 2         3 $add_blank = 1;
303             }
304             }
305              
306 181 100       225 if ($after_ws) {
307 158   100     318 $is_cur_word = defined($cword) && $cword==@words;
308             } else {
309 23   100     54 $is_cur_word = defined($cword) && $cword==@words-1;
310             }
311             #say "D:is_cur_word=$is_cur_word";
312 181 100       436 $chunk =
    100          
313             $2 ? _add_double_quoted($3, $is_cur_word) :
314             $5 ? _add_single_quoted($6) :
315             _add_unquoted($8, $is_cur_word, $after_ws);
316 181 100 66     343 if ($opts && $opts->{truncate_current_word} &&
      66        
      100        
317             $is_cur_word && $pos > $point) {
318 9         43 $chunk = substr(
319             $chunk, 0, length($chunk)-($pos_min_ws-$point));
320             #say "D:truncating current word to <$chunk>";
321             }
322 181 100       232 if ($after_ws) {
323 158         239 push @words, $chunk;
324             } else {
325 23         28 $words[-1] .= $chunk;
326             }
327 181 100       265 if ($add_blank) {
328 2         3 push @words, '';
329 2         2 $add_blank = 0;
330             }
331 181 100       790 $after_ws = ($2 ? $4 : $5 ? $7 : $9) ? 1:0;
    100          
    100          
332              
333             } elsif ($10) {
334             # non-whitespace word-breaking characters
335 10         14 push @words, $10;
336 10         31 $after_ws = 1;
337             } else {
338             # whitespace
339 1         5 $after_ws = 1;
340             }
341             !egx;
342              
343 87   66     196 $cword //= @words;
344 87   100     195 $words[$cword] //= '';
345              
346             log_trace "[compbash] parse_cmdline(): result: words=%s, cword=%s", \@words, $cword
347 87 50       137 if $ENV{COMPLETE_BASH_TRACE};
348              
349 87         505 [\@words, $cword];
350             }
351              
352             $SPEC{join_wordbreak_words} = {
353             v => 1.1,
354             summary => 'Post-process parse_cmdline() result by joining some words',
355             description => <<'_',
356              
357             `parse_cmdline()`, like bash, splits some characters that are considered as
358             word-breaking characters:
359              
360             "'@><=;|&(:
361              
362             So if command-line is:
363              
364             command --module=Data::Dump bob@example.org
365              
366             then they will be parsed as:
367              
368             ["command", "--module", "=", "Data", "::", "Dump", "bob", '@', "example.org"]
369              
370             Normally in Perl applications, we want `:`, `@` to be part of word. So this
371             routine will convert the above into:
372              
373             ["command", "--module=Data::Dump", 'bob@example.org']
374              
375             _
376             };
377             sub join_wordbreak_words {
378 3     3 1 3745 my ($words, $cword) = @_;
379 3         5 my $new_words = [];
380 3         18 my $i = -1;
381 3         9 while (++$i < @$words) {
382 15         18 my $w = $words->[$i];
383 15 100       36 if ($w =~ /\A[\@=:]+\z/) {
384 5 100 66     19 if (@$new_words and $#$new_words != $cword) {
385 3         6 $new_words->[-1] .= $w;
386 3 50 66     11 $cword-- if $cword >= $i || $cword >= @$new_words;
387             } else {
388 2         5 push @$new_words, $w;
389             }
390 5 50       11 if ($i+1 < @$words) {
391 5         5 $i++;
392 5         8 $new_words->[-1] .= $words->[$i];
393 5 100 100     17 $cword-- if $cword >= $i || $cword >= @$new_words;
394             }
395             } else {
396 10         22 push @$new_words, $w;
397             }
398             }
399             log_trace "[compbash] join_wordbreak_words(): result: words=%s, cword=%d", $new_words, $cword
400 3 50       9 if $ENV{COMPLETE_BASH_TRACE};
401 3         21 [$new_words, $cword];
402             }
403              
404             sub _terminal_width {
405             # XXX need to cache?
406 15 50   15   17 if (eval { require Term::Size; 1 }) {
  15         484  
  15         521  
407 15         113 my ($cols, undef) = Term::Size::chars(*STDOUT{IO});
408 15   50     76 $cols // 80;
409             } else {
410 0   0     0 $ENV{COLUMNS} // 80;
411             }
412             }
413              
414             # given terminal width & number of columns, calculate column width
415             sub _column_width {
416 14     14   18 my ($terminal_width, $num_columns) = @_;
417 14 50 33     41 if (defined $num_columns && $num_columns > 0) {
418 0         0 int( ($terminal_width - ($num_columns-1)*2) / $num_columns ) - 1;
419             } else {
420 14         18 undef;
421             }
422             }
423              
424             # given terminal width & column width, calculate number of columns
425             sub _num_columns {
426 0     0   0 my ($terminal_width, $column_width) = @_;
427 0         0 my $n = int( ($terminal_width+2) / ($column_width+2) );
428 0 0       0 $n >= 1 ? $n : 1;
429             }
430              
431             $SPEC{format_completion} = {
432             v => 1.1,
433             summary => 'Format completion for output (for shell)',
434             description => <<'_',
435              
436             Bash accepts completion reply in the form of one entry per line to STDOUT. Some
437             characters will need to be escaped. This function helps you do the formatting,
438             with some options.
439              
440             This function accepts completion answer structure as described in the `Complete`
441             POD. Aside from `words`, this function also recognizes these keys:
442              
443             _
444             args_as => 'array',
445             args => {
446             completion => {
447             summary => 'Completion answer structure',
448             description => <<'_',
449              
450             Either an array or hash. See function description for more details.
451              
452             _
453             schema=>['any*' => of => ['hash*', 'array*']],
454             req=>1,
455             pos=>0,
456             },
457             opts => {
458             summary => 'Specify options',
459             schema=>'hash*',
460             pos=>1,
461             description => <<'_',
462              
463             Known options:
464              
465             * as
466              
467             Either `string` (the default) or `array` (to return array of lines instead of
468             the lines joined together). Returning array is useful if you are doing
469             completion inside `Term::ReadLine`, for example, where the library expects an
470             array.
471              
472             * esc_mode
473              
474             Escaping mode for entries. Either `default` (most nonalphanumeric characters
475             will be escaped), `shellvar` (like `default`, but dollar sign `$` will also be
476             escaped, convenient when completing environment variables for example),
477             `filename` (currently equals to `default`), `option` (currently equals to
478             `default`), or `none` (no escaping will be done).
479              
480             * word
481              
482             A workaround. String. For now, see source code for more details.
483              
484             * show_summaries
485              
486             Whether to show item's summaries. Boolean, default is from
487             COMPLETE_BASH_SHOW_SUMMARIES environment variable or 1.
488              
489             An answer item contain summary, which is a short description about the item,
490             e.g.:
491              
492             [{word=>"-a" , summary=>"Show hidden files"},
493             {word=>"-l" , summary=>"Show details"},
494             {word=>"--sort", summary=>"Specify sort order"}],
495              
496             When summaries are not shown, user will just be seeing something like:
497              
498             -a
499             -l
500             --sort
501              
502             But when summaries are shown, user will see:
503              
504             -a -- Show hidden files
505             -l -- Show details
506             --sort -- Specify sort order
507              
508             which is quite helpful.
509              
510             * workaround_with_wordbreaks
511              
512             Boolean. Default is true. See source code for more details.
513              
514             _
515              
516             },
517             },
518             result => {
519             summary => 'Formatted string (or array, if `as` is set to `array`)',
520             schema => ['any*' => of => ['str*', 'array*']],
521             },
522             result_naked => 1,
523             };
524             sub format_completion {
525 15     15 1 19664 my ($hcomp, $opts) = @_;
526              
527 15   100     59 $opts //= {};
528              
529 15 100       47 $hcomp = {words=>$hcomp} unless ref($hcomp) eq 'HASH';
530 15         21 my $words = $hcomp->{words};
531 15   100     39 my $as = $opts->{as} // 'string';
532             # 'escmode' key is deprecated (Complete 0.11-) and will be removed later
533             my $esc_mode = $opts->{esc_mode} // $ENV{COMPLETE_BASH_DEFAULT_ESC_MODE} //
534 15   66     49 'default';
      50        
535 15         20 my $path_sep = $hcomp->{path_sep};
536              
537             # we keep the original words (before formatted with summaries) when we want
538             # to use fzf instead of passing to bash directly
539 15         32 my @words;
540             my @summaries;
541 15         0 my @res;
542 15         0 my $has_summary;
543              
544             my $code_return_message = sub {
545             # display a message instead of list of words. we send " " (ASCII space)
546             # which bash does not display, so we can display a line of message while
547             # the user does not get the message as the completion. I've also tried
548             # \000 to \037 instead of space (\040) but nothing works better.
549 1     1   1 my $msg = shift;
550 1 50       4 if ($msg =~ /\A /) {
551 0         0 $msg =~ s/\A +//;
552 0 0       0 $msg = " (empty message)" unless length $msg;
553             }
554 1         2 return (sprintf("%-"._terminal_width()."s", $msg), " ");
555 15         61 };
556              
557             FORMAT_MESSAGE:
558             # display a message instead of list of words. we send " " (ASCII space)
559             # which bash does not display, so we can display a line of message while the
560             # user does not get the message as the completion. I've also tried \000 to
561             # \037 instead of space (\040) but nothing works better.
562 15 100       32 if (defined $hcomp->{message}) {
563 1         3 @res = $code_return_message->($hcomp->{message});
564 1         6 goto RETURN_RES;
565             }
566              
567             WORKAROUND_PREVENT_BASH_FROM_INSERTING_SPACE:
568             {
569 14 100       16 last unless @$words == 1;
  14         26  
570 9 100       19 if (defined $path_sep) {
571 4         38 my $re = qr/\Q$path_sep\E\z/;
572 4         7 my $word;
573 4 100       10 if (ref $words->[0] eq 'HASH') {
574             $words = [$words->[0], {word=>"$words->[0]{word} "}] if
575 1 50       9 $words->[0]{word} =~ $re;
576             } else {
577 3 100       20 $words = [$words->[0], "$words->[0] "]
578             if $words->[0] =~ $re;
579             }
580 4         11 last;
581             }
582              
583 5 50 66     19 if ($hcomp->{is_partial} ||
      66        
584             ref $words->[0] eq 'HASH' && $words->[0]{is_partial}) {
585 2 100       6 if (ref $words->[0] eq 'HASH') {
586 1         4 $words = [$words->[0], {word=>"$words->[0]{word} "}];
587             } else {
588 1         4 $words = [$words->[0], "$words->[0] "];
589             }
590 2         4 last;
591             }
592             }
593              
594             WORKAROUND_WITH_WORDBREAKS:
595             # this is a workaround. since bash breaks words using characters in
596             # $COMP_WORDBREAKS, which by default is "'@><=;|&(: this presents a problem
597             # we often encounter: if we want to provide with a list of strings
598             # containing say ':', most often Perl modules/packages, if user types e.g.
599             # "Text::AN" and we provide completion ["Text::ANSI"] then bash will change
600             # the word at cursor to become "Text::Text::ANSI" since it sees the current
601             # word as "AN" and not "Text::AN". the workaround is to chop /^Text::/ from
602             # completion answers. btw, we actually chop /^text::/i to handle
603             # case-insensitive matching, although this does not have the ability to
604             # replace the current word (e.g. if we type 'text::an' then bash can only
605             # replace the current word 'an' with 'ANSI).
606             {
607 14 50 50     18 last unless $opts->{workaround_with_wordbreaks} // 1;
  14         39  
608 14 50       26 last unless defined $opts->{word};
609              
610 0 0       0 if ($opts->{word} =~ s/(.+[\@><=;|&\(:])//) {
611 0         0 my $prefix = $1;
612 0         0 for (@$words) {
613 0 0       0 if (ref($_) eq 'HASH') {
614 0         0 $_->{word} =~ s/\A\Q$prefix\E//i;
615             } else {
616 0         0 s/\A\Q$prefix\E//i;
617             }
618             }
619             }
620             }
621              
622             ESCAPE_WORDS:
623 14         21 for my $entry (@$words) {
624 26 100       43 my $word = ref($entry) eq 'HASH' ? $entry->{word} : $entry;
625 26 100 50     59 my $summary = (ref($entry) eq 'HASH' ? $entry->{summary} : undef) // '';
626 26 100       45 if ($esc_mode eq 'shellvar') {
    100          
627             # escape $ also
628 1         9 $word =~ s!([^A-Za-z0-9,+._/:~-])!\\$1!g;
629             } elsif ($esc_mode eq 'none') {
630             # no escaping
631             } else {
632             # default
633 24         69 $word =~ s!([^A-Za-z0-9,+._/:\$~-])!\\$1!g;
634             }
635 26         38 push @words, $word;
636 26         30 push @summaries, $summary;
637 26 50       55 $has_summary = 1 if length $summary;
638             }
639              
640 14   50     37 my $summary_align = $ENV{COMPLETE_BASH_SUMMARY_ALIGN} // 'left';
641 14   50     31 my $max_columns = $ENV{COMPLETE_BASH_MAX_COLUMNS} // 0;
642 14         25 my $terminal_width = _terminal_width();
643 14         28 my $column_width = _column_width($terminal_width, $max_columns);
644              
645             #warn "terminal_width=$terminal_width, column_width=".($column_width // 'undef')."\n";
646              
647             FORMAT_SUMMARIES: {
648 14         29 @res = @words;
  14         33  
649 14 100       25 last if @words <= 1;
650 10 50       20 last unless $has_summary;
651             last unless $opts->{show_summaries} //
652 0 0 0     0 $ENV{COMPLETE_BASH_SHOW_SUMMARIES} // 1;
      0        
653 0         0 my $max_entry_width = 8;
654 0         0 my $max_summ_width = 0;
655 0         0 for (0..$#words) {
656 0 0       0 $max_entry_width = length $words[$_]
657             if $max_entry_width < length $words[$_];
658 0 0       0 $max_summ_width = length $summaries[$_]
659             if $max_summ_width < length $summaries[$_];
660             }
661             #warn "max_entry_width=$max_entry_width, max_summ_width=$max_summ_width\n";
662 0 0       0 if ($summary_align eq 'right') {
663             # if we are aligning summary to the right, we want to fill column
664             # width width
665 0 0       0 if ($max_columns <= 0) {
666 0         0 $max_columns = _num_columns(
667             $terminal_width, ($max_entry_width + 2 + $max_summ_width));
668             }
669 0         0 $column_width = _column_width($terminal_width, $max_columns);
670 0         0 my $new_max_summ_width = $column_width - 2 - $max_entry_width;
671 0 0       0 $max_summ_width = $new_max_summ_width
672             if $max_summ_width < $new_max_summ_width;
673             #warn "max_columns=$max_columns, column_width=$column_width, max_summ_width=$max_summ_width\n";
674             }
675              
676 0         0 for (0..$#words) {
677 0         0 my $summary = $summaries[$_];
678 0 0       0 if (length $summary) {
679 0 0       0 $res[$_] = sprintf(
680             "%-${max_entry_width}s |%".
681             ($summary_align eq 'right' ? $max_summ_width : '')."s",
682             $words[$_], $summary);
683             }
684             }
685             } # FORMAT_SUMMARIES
686              
687             MAX_COLUMNS: {
688 14 50       15 last unless $max_columns > 0;
  14         23  
689 0         0 my $max_entry_width = 0;
690 0         0 for (@res) {
691 0 0       0 $max_entry_width = length if $max_entry_width < length;
692             }
693 0 0       0 last if $max_entry_width >= $column_width;
694 0         0 for (@res) {
695 0 0       0 $_ .= " " x ($column_width - length) if $column_width > length;
696             }
697             }
698              
699             PASS_TO_FZF: {
700 14 50       16 last unless $ENV{COMPLETE_BASH_FZF};
  14         27  
701 0   0     0 my $items = $ENV{COMPLETE_BASH_FZF_ITEMS} // 100;
702 0 0       0 last unless @words >= $items;
703              
704 0         0 require File::Which;
705 0 0       0 unless (File::Which::which("fzf")) {
706             #@res = $code_return_message->("Cannot find fzf to filter ".
707             # scalar(@words)." items");
708 0         0 goto RETURN_RES;
709             }
710              
711 0         0 require IPC::Open2;
712 0         0 local *CHLD_OUT;
713 0         0 local *CHLD_IN;
714             my $pid = IPC::Open2::open2(
715             \*CHLD_OUT, \*CHLD_IN, "fzf", "-m", "-d:", "--with-nth=2..")
716 0 0       0 or do {
717 0         0 @res = $code_return_message->("Cannot open fzf to filter ".
718             scalar(@words)." items");
719 0         0 goto RETURN_RES;
720             };
721              
722 0         0 print CHLD_IN map { "$_:$res[$_]\n" } 0..$#res;
  0         0  
723 0         0 close CHLD_IN;
724              
725 0         0 my @res_words;
726 0         0 while () {
727 0 0       0 my ($index) = /\A([0-9]+)\:/ or next;
728 0         0 push @res_words, $words[$index];
729             }
730 0 0       0 if (@res_words) {
731 0         0 @res = join(" ", @res_words);
732             } else {
733 0         0 @res = ();
734             }
735 0         0 waitpid($pid, 0);
736             }
737              
738             RETURN_RES:
739             #use Data::Dump; warn Data::Dump::dump(\@res);
740 15 100       27 if ($as eq 'array') {
741 1         10 return \@res;
742             } else {
743 14         23 return join("", map {($_, "\n")} @res);
  26         157  
744             }
745             }
746              
747             1;
748             # ABSTRACT: Completion routines for bash shell
749              
750             __END__