File Coverage

blib/lib/Complete/Bash.pm
Criterion Covered Total %
statement 168 227 74.0
branch 100 162 61.7
condition 54 85 63.5
subroutine 18 19 94.7
pod 4 4 100.0
total 344 497 69.2


line stmt bran cond sub pod time code
1             package Complete::Bash;
2              
3             our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
4             our $DATE = '2021-07-24'; # DATE
5             our $DIST = 'Complete-Bash'; # DIST
6             our $VERSION = '0.336'; # VERSION
7              
8 4     4   79889 use 5.010001;
  4         76  
9 4     4   23 use strict;
  4         8  
  4         116  
10 4     4   22 use warnings;
  4         5  
  4         146  
11 4     4   8230 use Log::ger;
  4         317  
  4         23  
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   14 my ($user, $slash) = @_;
31 4         8 my @ent;
32 4 50       11 if (length $user) {
33 0         0 @ent = getpwnam($user);
34             } else {
35 4         452 @ent = getpwuid($>);
36 4         17 $user = $ent[0];
37             }
38 4 50       38 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   1685 no warnings 'uninitialized';
  4         10  
  4         1042  
44              
45 138     138   315 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         416 $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     148 ($is_cur_word ? "\$$5" : $ENV{$5})
    100          
    100          
    100          
56             !egx;
57 138         310 $word;
58             }
59              
60             sub _add_double_quoted {
61 4     4   34 no warnings 'uninitialized';
  4         16  
  4         1865  
62              
63 23     23   58 my ($word, $is_cur_word) = @_;
64              
65 23         81 $word =~ s!\\(.) | # 1) escaped char
66             \$(\w+) # 2) variable name
67             !
68             $1 ? $1 :
69 3 50       19 ($is_cur_word ? "\$$2" : $ENV{$2})
    100          
70             !egx;
71 23         51 $word;
72             }
73              
74             sub _add_single_quoted {
75 20     20   47 my $word = shift;
76 20         43 $word =~ s/\\(.)/$1/g;
77 20         45 $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 33110 my ($line, $marker) = @_;
114 91   100     494 $marker //= '^';
115              
116 91         235 my $point = index($line, $marker);
117 91 100       255 die "BUG: No marker '$marker' in line <$line>" unless $point >= 0;
118 90         577 $line =~ s/\Q$marker\E//;
119 90         431 ($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   34 no warnings 'uninitialized';
  4         9  
  4         11428  
262 87     87 1 227 my ($line, $point, $opts) = @_;
263              
264 87   33     187 $line //= $ENV{COMP_LINE};
265 87   0     181 $point //= $ENV{COMP_POINT} // 0;
      33        
266              
267 87 50       183 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       214 if $ENV{COMPLETE_BASH_TRACE};
272              
273 87         153 my @words;
274             my $cword;
275 87         145 my $pos = 0;
276 87         128 my $pos_min_ws = 0;
277 87         126 my $after_ws = 1; # XXX what does this variable mean?
278 87         219 my $chunk;
279             my $add_blank;
280 87         0 my $is_cur_word;
281 87         785 $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         473 $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     1017 if ($2 || $5 || defined($8)) {
    100 100        
293             # double-quoted/single-quoted/unquoted chunk
294              
295 181 100       367 if (not(defined $cword)) {
296 157 100       441 $pos_min_ws = $pos - length($2 ? $4 : $5 ? $7 : $9);
    100          
297             #say "D:pos_min_ws=$pos_min_ws";
298 157 100       364 if ($point <= $pos_min_ws) {
    100          
299 72 100       173 $cword = @words - ($after_ws ? 0 : 1);
300             } elsif ($point < $pos) {
301 2 50       7 $cword = @words + 1 - ($after_ws ? 0 : 1);
302 2         4 $add_blank = 1;
303             }
304             }
305              
306 181 100       310 if ($after_ws) {
307 158   100     460 $is_cur_word = defined($cword) && $cword==@words;
308             } else {
309 23   100     73 $is_cur_word = defined($cword) && $cword==@words-1;
310             }
311             #say "D:is_cur_word=$is_cur_word";
312 181 100       584 $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     446 if ($opts && $opts->{truncate_current_word} &&
      66        
      100        
317             $is_cur_word && $pos > $point) {
318 9         63 $chunk = substr(
319             $chunk, 0, length($chunk)-($pos_min_ws-$point));
320             #say "D:truncating current word to <$chunk>";
321             }
322 181 100       311 if ($after_ws) {
323 158         314 push @words, $chunk;
324             } else {
325 23         45 $words[-1] .= $chunk;
326             }
327 181 100       339 if ($add_blank) {
328 2         4 push @words, '';
329 2         4 $add_blank = 0;
330             }
331 181 100       1107 $after_ws = ($2 ? $4 : $5 ? $7 : $9) ? 1:0;
    100          
    100          
332              
333             } elsif ($10) {
334             # non-whitespace word-breaking characters
335 10         24 push @words, $10;
336 10         42 $after_ws = 1;
337             } else {
338             # whitespace
339 1         11 $after_ws = 1;
340             }
341             !egx;
342              
343 87   66     250 $cword //= @words;
344 87   100     217 $words[$cword] //= '';
345              
346             log_trace "[compbash] parse_cmdline(): result: words=%s, cword=%s", \@words, $cword
347 87 50       189 if $ENV{COMPLETE_BASH_TRACE};
348              
349 87         735 [\@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 4514 my ($words, $cword) = @_;
379 3         5 my $new_words = [];
380 3         20 my $i = -1;
381 3         10 while (++$i < @$words) {
382 15         21 my $w = $words->[$i];
383 15 100       43 if ($w =~ /\A[\@=:]+\z/) {
384 5 100 66     26 if (@$new_words and $#$new_words != $cword) {
385 3         6 $new_words->[-1] .= $w;
386 3 50 66     12 $cword-- if $cword >= $i || $cword >= @$new_words;
387             } else {
388 2         5 push @$new_words, $w;
389             }
390 5 50       13 if ($i+1 < @$words) {
391 5         8 $i++;
392 5         9 $new_words->[-1] .= $words->[$i];
393 5 100 100     21 $cword-- if $cword >= $i || $cword >= @$new_words;
394             }
395             } else {
396 10         26 push @$new_words, $w;
397             }
398             }
399             log_trace "[compbash] join_wordbreak_words(): result: words=%s, cword=%d", $new_words, $cword
400 3 50       8 if $ENV{COMPLETE_BASH_TRACE};
401 3         23 [$new_words, $cword];
402             }
403              
404             sub _terminal_width {
405             # XXX need to cache?
406 15 50   15   26 if (eval { require Term::Size; 1 }) {
  15         576  
  15         623  
407 15         148 my ($cols, undef) = Term::Size::chars(*STDOUT{IO});
408 15   50     89 $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   27 my ($terminal_width, $num_columns) = @_;
417 14 50 33     48 if (defined $num_columns && $num_columns > 0) {
418 0         0 int( ($terminal_width - ($num_columns-1)*2) / $num_columns ) - 1;
419             } else {
420 14         26 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 24145 my ($hcomp, $opts) = @_;
526              
527 15   100     73 $opts //= {};
528              
529 15 100       58 $hcomp = {words=>$hcomp} unless ref($hcomp) eq 'HASH';
530 15         28 my $words = $hcomp->{words};
531 15   100     48 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     58 'default';
      50        
535 15         24 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         40 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   2 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         3 return (sprintf("%-"._terminal_width()."s", $msg), " ");
555 15         62 };
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       41 if (defined $hcomp->{message}) {
563 1         5 @res = $code_return_message->($hcomp->{message});
564 1         7 goto RETURN_RES;
565             }
566              
567             WORKAROUND_PREVENT_BASH_FROM_INSERTING_SPACE:
568             {
569 14 100       20 last unless @$words == 1;
  14         31  
570 9 100       20 if (defined $path_sep) {
571 4         45 my $re = qr/\Q$path_sep\E\z/;
572 4         8 my $word;
573 4 100       12 if (ref $words->[0] eq 'HASH') {
574             $words = [$words->[0], {word=>"$words->[0]{word} "}] if
575 1 50       11 $words->[0]{word} =~ $re;
576             } else {
577 3 100       26 $words = [$words->[0], "$words->[0] "]
578             if $words->[0] =~ $re;
579             }
580 4         14 last;
581             }
582              
583 5 50 66     28 if ($hcomp->{is_partial} ||
      66        
584             ref $words->[0] eq 'HASH' && $words->[0]{is_partial}) {
585 2 100       7 if (ref $words->[0] eq 'HASH') {
586 1         6 $words = [$words->[0], {word=>"$words->[0]{word} "}];
587             } else {
588 1         5 $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     20 last unless $opts->{workaround_with_wordbreaks} // 1;
  14         50  
608 14 50       33 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         27 for my $entry (@$words) {
624 26 100       51 my $word = ref($entry) eq 'HASH' ? $entry->{word} : $entry;
625 26 100 50     77 my $summary = (ref($entry) eq 'HASH' ? $entry->{summary} : undef) // '';
626 26 100       50 if ($esc_mode eq 'shellvar') {
    100          
627             # escape $ also
628 1         10 $word =~ s!([^A-Za-z0-9,+._/:~-])!\\$1!g;
629             } elsif ($esc_mode eq 'none') {
630             # no escaping
631             } else {
632             # default
633 24         88 $word =~ s!([^A-Za-z0-9,+._/:\$~-])!\\$1!g;
634             }
635 26         46 push @words, $word;
636 26         38 push @summaries, $summary;
637 26 50       77 $has_summary = 1 if length $summary;
638             }
639              
640 14   50     47 my $summary_align = $ENV{COMPLETE_BASH_SUMMARY_ALIGN} // 'left';
641 14   50     34 my $max_columns = $ENV{COMPLETE_BASH_MAX_COLUMNS} // 0;
642 14         31 my $terminal_width = _terminal_width();
643 14         32 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         21 @res = @words;
  14         35  
649 14 100       34 last if @words <= 1;
650 10 50       36 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       19 last unless $max_columns > 0;
  14         29  
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       20 last if $ENV{INSIDE_EMACS};
  14         32  
701 14 50       29 last unless $ENV{COMPLETE_BASH_FZF};
702 0   0     0 my $items = $ENV{COMPLETE_BASH_FZF_ITEMS} // 100;
703 0 0       0 last unless @words >= $items;
704              
705 0         0 require File::Which;
706 0 0       0 unless (File::Which::which("fzf")) {
707             #@res = $code_return_message->("Cannot find fzf to filter ".
708             # scalar(@words)." items");
709 0         0 goto RETURN_RES;
710             }
711              
712 0         0 require IPC::Open2;
713 0         0 local *CHLD_OUT;
714 0         0 local *CHLD_IN;
715             my $pid = IPC::Open2::open2(
716             \*CHLD_OUT, \*CHLD_IN, "fzf", "-m", "-d:", "--with-nth=2..")
717 0 0       0 or do {
718 0         0 @res = $code_return_message->("Cannot open fzf to filter ".
719             scalar(@words)." items");
720 0         0 goto RETURN_RES;
721             };
722              
723 0         0 print CHLD_IN map { "$_:$res[$_]\n" } 0..$#res;
  0         0  
724 0         0 close CHLD_IN;
725              
726 0         0 my @res_words;
727 0         0 while () {
728 0 0       0 my ($index) = /\A([0-9]+)\:/ or next;
729 0         0 push @res_words, $words[$index];
730             }
731 0 0       0 if (@res_words) {
732 0         0 @res = join(" ", @res_words);
733             } else {
734 0         0 @res = ();
735             }
736 0         0 waitpid($pid, 0);
737             }
738              
739             RETURN_RES:
740             #use Data::Dump; warn Data::Dump::dump(\@res);
741 15 100       25 if ($as eq 'array') {
742 1         12 return \@res;
743             } else {
744 14         27 return join("", map {($_, "\n")} @res);
  26         224  
745             }
746             }
747              
748             1;
749             # ABSTRACT: Completion routines for bash shell
750              
751             __END__