File Coverage

blib/lib/Complete/Bash.pm
Criterion Covered Total %
statement 171 238 71.8
branch 100 166 60.2
condition 54 89 60.6
subroutine 19 21 90.4
pod 4 4 100.0
total 348 518 67.1


line stmt bran cond sub pod time code
1             package Complete::Bash;
2              
3 4     4   71509 use 5.010001;
  4         43  
4 4     4   36 use strict;
  4         8  
  4         92  
5 4     4   19 use warnings;
  4         8  
  4         127  
6 4     4   6911 use Log::ger;
  4         239  
  4         18  
7              
8 4     4   972 use Exporter 'import';
  4         10  
  4         836  
9              
10             our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
11             our $DATE = '2022-09-08'; # DATE
12             our $DIST = 'Complete-Bash'; # DIST
13             our $VERSION = '0.337'; # VERSION
14              
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   13 my ($user, $slash) = @_;
31 4         7 my @ent;
32 4 50       9 if (length $user) {
33 0         0 @ent = getpwnam($user);
34             } else {
35 4         423 @ent = getpwuid($>);
36 4         15 $user = $ent[0];
37             }
38 4 50       36 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   30 no warnings 'uninitialized';
  4         9  
  4         775  
44              
45 138     138   282 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         378 $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     114 ($is_cur_word ? "\$$5" : $ENV{$5})
    100          
    100          
    100          
56             !egx;
57 138         291 $word;
58             }
59              
60             sub _add_double_quoted {
61 4     4   55 no warnings 'uninitialized';
  4         10  
  4         1819  
62              
63 23     23   49 my ($word, $is_cur_word) = @_;
64              
65 23         73 $word =~ s!\\(.) | # 1) escaped char
66             \$(\w+) # 2) variable name
67             !
68             $1 ? $1 :
69 3 50       15 ($is_cur_word ? "\$$2" : $ENV{$2})
    100          
70             !egx;
71 23         50 $word;
72             }
73              
74             sub _add_single_quoted {
75 20     20   37 my $word = shift;
76 20         36 $word =~ s/\\(.)/$1/g;
77 20         69 $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 27212 my ($line, $marker) = @_;
114 91   100     428 $marker //= '^';
115              
116 91         184 my $point = index($line, $marker);
117 91 100       199 die "BUG: No marker '$marker' in line <$line>" unless $point >= 0;
118 90         450 $line =~ s/\Q$marker\E//;
119 90         440 ($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   30 no warnings 'uninitialized';
  4         11  
  4         11016  
262 87     87 1 178 my ($line, $point, $opts) = @_;
263              
264 87   33     154 $line //= $ENV{COMP_LINE};
265 87   0     173 $point //= $ENV{COMP_POINT} // 0;
      33        
266              
267 87 50       159 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       178 if $ENV{COMPLETE_BASH_TRACE};
272              
273 87         123 my @words;
274             my $cword;
275 87         115 my $pos = 0;
276 87         121 my $pos_min_ws = 0;
277 87         102 my $after_ws = 1; # XXX what does this variable mean?
278 87         188 my $chunk;
279             my $add_blank;
280 87         0 my $is_cur_word;
281 87         596 $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         419 $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     862 if ($2 || $5 || defined($8)) {
    100 100        
293             # double-quoted/single-quoted/unquoted chunk
294              
295 181 100       341 if (not(defined $cword)) {
296 157 100       373 $pos_min_ws = $pos - length($2 ? $4 : $5 ? $7 : $9);
    100          
297             #say "D:pos_min_ws=$pos_min_ws";
298 157 100       319 if ($point <= $pos_min_ws) {
    100          
299 72 100       132 $cword = @words - ($after_ws ? 0 : 1);
300             } elsif ($point < $pos) {
301 2 50       6 $cword = @words + 1 - ($after_ws ? 0 : 1);
302 2         3 $add_blank = 1;
303             }
304             }
305              
306 181 100       294 if ($after_ws) {
307 158   100     388 $is_cur_word = defined($cword) && $cword==@words;
308             } else {
309 23   100     66 $is_cur_word = defined($cword) && $cword==@words-1;
310             }
311             #say "D:is_cur_word=$is_cur_word";
312 181 100       524 $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     456 if ($opts && $opts->{truncate_current_word} &&
      66        
      100        
317             $is_cur_word && $pos > $point) {
318 9         40 $chunk = substr(
319             $chunk, 0, length($chunk)-($pos_min_ws-$point));
320             #say "D:truncating current word to <$chunk>";
321             }
322 181 100       278 if ($after_ws) {
323 158         290 push @words, $chunk;
324             } else {
325 23         41 $words[-1] .= $chunk;
326             }
327 181 100       285 if ($add_blank) {
328 2         4 push @words, '';
329 2         3 $add_blank = 0;
330             }
331 181 100       1048 $after_ws = ($2 ? $4 : $5 ? $7 : $9) ? 1:0;
    100          
    100          
332              
333             } elsif ($10) {
334             # non-whitespace word-breaking characters
335 10         22 push @words, $10;
336 10         40 $after_ws = 1;
337             } else {
338             # whitespace
339 1         7 $after_ws = 1;
340             }
341             !egx;
342              
343 87   66     233 $cword //= @words;
344 87   100     204 $words[$cword] //= '';
345              
346             log_trace "[compbash] parse_cmdline(): result: words=%s, cword=%s", \@words, $cword
347 87 50       157 if $ENV{COMPLETE_BASH_TRACE};
348              
349 87         669 [\@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 4532 my ($words, $cword) = @_;
379 3         20 my $new_words = [];
380 3         6 my $i = -1;
381 3         10 while (++$i < @$words) {
382 15         23 my $w = $words->[$i];
383 15 100       77 if ($w =~ /\A[\@=:]+\z/) {
384 5 100 66     22 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         4 push @$new_words, $w;
389             }
390 5 50       15 if ($i+1 < @$words) {
391 5         7 $i++;
392 5         9 $new_words->[-1] .= $words->[$i];
393 5 100 100     23 $cword-- if $cword >= $i || $cword >= @$new_words;
394             }
395             } else {
396 10         25 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         22 [$new_words, $cword];
402             }
403              
404             sub _terminal_width {
405             # XXX need to cache?
406 15 50   15   19 if (eval { require Term::Size; 1 }) {
  15         78  
  15         37  
407 15         132 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             sub _terminal_height {
415             # XXX need to cache?
416 0 0   0   0 if (eval { require Term::Size; 1 }) {
  0         0  
  0         0  
417 0         0 my (undef, $lines) = Term::Size::chars(*STDOUT{IO});
418 0   0     0 $lines // 25;
419             } else {
420 0   0     0 $ENV{LINES} // 25;
421             }
422             }
423              
424             # given terminal width & number of columns, calculate column width
425             sub _column_width {
426 14     14   23 my ($terminal_width, $num_columns) = @_;
427 14 50 33     48 if (defined $num_columns && $num_columns > 0) {
428 0         0 int( ($terminal_width - ($num_columns-1)*2) / $num_columns ) - 1;
429             } else {
430 14         26 undef;
431             }
432             }
433              
434             # given terminal width & column width, calculate number of columns
435             sub _num_columns {
436 0     0   0 my ($terminal_width, $column_width) = @_;
437 0         0 my $n = int( ($terminal_width+2) / ($column_width+2) );
438 0 0       0 $n >= 1 ? $n : 1;
439             }
440              
441             $SPEC{format_completion} = {
442             v => 1.1,
443             summary => 'Format completion for output (for shell)',
444             description => <<'_',
445              
446             Bash accepts completion reply in the form of one entry per line to STDOUT. Some
447             characters will need to be escaped. This function helps you do the formatting,
448             with some options.
449              
450             This function accepts completion answer structure as described in the `Complete`
451             POD. Aside from `words`, this function also recognizes these keys:
452              
453             _
454             args_as => 'array',
455             args => {
456             completion => {
457             summary => 'Completion answer structure',
458             description => <<'_',
459              
460             Either an array or hash. See function description for more details.
461              
462             _
463             schema=>['any*' => of => ['hash*', 'array*']],
464             req=>1,
465             pos=>0,
466             },
467             opts => {
468             summary => 'Specify options',
469             schema=>'hash*',
470             pos=>1,
471             description => <<'_',
472              
473             Known options:
474              
475             * as
476              
477             Either `string` (the default) or `array` (to return array of lines instead of
478             the lines joined together). Returning array is useful if you are doing
479             completion inside `Term::ReadLine`, for example, where the library expects an
480             array.
481              
482             * esc_mode
483              
484             Escaping mode for entries. Either `default` (most nonalphanumeric characters
485             will be escaped), `shellvar` (like `default`, but dollar sign `$` will also be
486             escaped, convenient when completing environment variables for example),
487             `filename` (currently equals to `default`), `option` (currently equals to
488             `default`), or `none` (no escaping will be done).
489              
490             * word
491              
492             A workaround. String. For now, see source code for more details.
493              
494             * show_summaries
495              
496             Whether to show item's summaries. Boolean, default is from
497             COMPLETE_BASH_SHOW_SUMMARIES environment variable or 1.
498              
499             An answer item contain summary, which is a short description about the item,
500             e.g.:
501              
502             [{word=>"-a" , summary=>"Show hidden files"},
503             {word=>"-l" , summary=>"Show details"},
504             {word=>"--sort", summary=>"Specify sort order"}],
505              
506             When summaries are not shown, user will just be seeing something like:
507              
508             -a
509             -l
510             --sort
511              
512             But when summaries are shown, user will see:
513              
514             -a -- Show hidden files
515             -l -- Show details
516             --sort -- Specify sort order
517              
518             which is quite helpful.
519              
520             * workaround_with_wordbreaks
521              
522             Boolean. Default is true. See source code for more details.
523              
524             _
525              
526             },
527             },
528             result => {
529             summary => 'Formatted string (or array, if `as` is set to `array`)',
530             schema => ['any*' => of => ['str*', 'array*']],
531             },
532             result_naked => 1,
533             };
534             sub format_completion {
535 15     15 1 23337 my ($hcomp, $opts) = @_;
536              
537 15   100     81 $opts //= {};
538              
539 15 100       41 $hcomp = {words=>$hcomp} unless ref($hcomp) eq 'HASH';
540 15         25 my $words = $hcomp->{words};
541 15   100     49 my $as = $opts->{as} // 'string';
542             # 'escmode' key is deprecated (Complete 0.11-) and will be removed later
543             my $esc_mode = $opts->{esc_mode} // $ENV{COMPLETE_BASH_DEFAULT_ESC_MODE} //
544 15   66     60 'default';
      50        
545 15         23 my $path_sep = $hcomp->{path_sep};
546              
547             # we keep the original words (before formatted with summaries) when we want
548             # to use fzf instead of passing to bash directly
549 15         39 my @words;
550             my @summaries;
551 15         0 my @res;
552 15         0 my $has_summary;
553              
554             my $code_return_message = sub {
555             # display a message instead of list of words. we send " " (ASCII space)
556             # which bash does not display, so we can display a line of message while
557             # the user does not get the message as the completion. I've also tried
558             # \000 to \037 instead of space (\040) but nothing works better.
559 1     1   3 my $msg = shift;
560 1 50       4 if ($msg =~ /\A /) {
561 0         0 $msg =~ s/\A +//;
562 0 0       0 $msg = " (empty message)" unless length $msg;
563             }
564 1         4 return (sprintf("%-"._terminal_width()."s", $msg), " ");
565 15         58 };
566              
567             FORMAT_MESSAGE:
568             # display a message instead of list of words. we send " " (ASCII space)
569             # which bash does not display, so we can display a line of message while the
570             # user does not get the message as the completion. I've also tried \000 to
571             # \037 instead of space (\040) but nothing works better.
572 15 100       37 if (defined $hcomp->{message}) {
573 1         5 @res = $code_return_message->($hcomp->{message});
574 1         5 goto RETURN_RES;
575             }
576              
577             WORKAROUND_PREVENT_BASH_FROM_INSERTING_SPACE:
578             {
579 14 100       17 last unless @$words == 1;
  14         32  
580 9 100       23 if (defined $path_sep) {
581 4         42 my $re = qr/\Q$path_sep\E\z/;
582 4         8 my $word;
583 4 100       14 if (ref $words->[0] eq 'HASH') {
584             $words = [$words->[0], {word=>"$words->[0]{word} "}] if
585 1 50       10 $words->[0]{word} =~ $re;
586             } else {
587 3 100       21 $words = [$words->[0], "$words->[0] "]
588             if $words->[0] =~ $re;
589             }
590 4         15 last;
591             }
592              
593 5 50 66     27 if ($hcomp->{is_partial} ||
      66        
594             ref $words->[0] eq 'HASH' && $words->[0]{is_partial}) {
595 2 100       6 if (ref $words->[0] eq 'HASH') {
596 1         7 $words = [$words->[0], {word=>"$words->[0]{word} "}];
597             } else {
598 1         4 $words = [$words->[0], "$words->[0] "];
599             }
600 2         5 last;
601             }
602             }
603              
604             WORKAROUND_WITH_WORDBREAKS:
605             # this is a workaround. since bash breaks words using characters in
606             # $COMP_WORDBREAKS, which by default is "'@><=;|&(: this presents a problem
607             # we often encounter: if we want to provide with a list of strings
608             # containing say ':', most often Perl modules/packages, if user types e.g.
609             # "Text::AN" and we provide completion ["Text::ANSI"] then bash will change
610             # the word at cursor to become "Text::Text::ANSI" since it sees the current
611             # word as "AN" and not "Text::AN". the workaround is to chop /^Text::/ from
612             # completion answers. btw, we actually chop /^text::/i to handle
613             # case-insensitive matching, although this does not have the ability to
614             # replace the current word (e.g. if we type 'text::an' then bash can only
615             # replace the current word 'an' with 'ANSI).
616             {
617 14 50 50     16 last unless $opts->{workaround_with_wordbreaks} // 1;
  14         47  
618 14 50       29 last unless defined $opts->{word};
619              
620 0 0       0 if ($opts->{word} =~ s/(.+[\@><=;|&\(:])//) {
621 0         0 my $prefix = $1;
622 0         0 for (@$words) {
623 0 0       0 if (ref($_) eq 'HASH') {
624 0         0 $_->{word} =~ s/\A\Q$prefix\E//i;
625             } else {
626 0         0 s/\A\Q$prefix\E//i;
627             }
628             }
629             }
630             }
631              
632             ESCAPE_WORDS:
633 14         28 for my $entry (@$words) {
634 26 100       50 my $word = ref($entry) eq 'HASH' ? $entry->{word} : $entry;
635 26 100 50     76 my $summary = (ref($entry) eq 'HASH' ? $entry->{summary} : undef) // '';
636 26 100       64 if ($esc_mode eq 'shellvar') {
    100          
637             # escape $ also
638 1         10 $word =~ s!([^A-Za-z0-9,+._/:~-])!\\$1!g;
639             } elsif ($esc_mode eq 'none') {
640             # no escaping
641             } else {
642             # default
643 24         80 $word =~ s!([^A-Za-z0-9,+._/:\$~-])!\\$1!g;
644             }
645 26         47 push @words, $word;
646 26         40 push @summaries, $summary;
647 26 50       63 $has_summary = 1 if length $summary;
648             }
649              
650 14   50     40 my $summary_align = $ENV{COMPLETE_BASH_SUMMARY_ALIGN} // 'left';
651 14   50     32 my $max_columns = $ENV{COMPLETE_BASH_MAX_COLUMNS} // 0;
652 14         31 my $terminal_width = _terminal_width();
653 14         28 my $column_width = _column_width($terminal_width, $max_columns);
654              
655             #warn "terminal_width=$terminal_width, column_width=".($column_width // 'undef')."\n";
656              
657             FORMAT_SUMMARIES: {
658 14         18 @res = @words;
  14         34  
659 14 100       32 last if @words <= 1;
660 10 50       22 last unless $has_summary;
661             last unless $opts->{show_summaries} //
662 0 0 0     0 $ENV{COMPLETE_BASH_SHOW_SUMMARIES} // 1;
      0        
663 0         0 my $max_entry_width = 8;
664 0         0 my $max_summ_width = 0;
665 0         0 for (0..$#words) {
666 0 0       0 $max_entry_width = length $words[$_]
667             if $max_entry_width < length $words[$_];
668 0 0       0 $max_summ_width = length $summaries[$_]
669             if $max_summ_width < length $summaries[$_];
670             }
671             #warn "max_entry_width=$max_entry_width, max_summ_width=$max_summ_width\n";
672 0 0       0 if ($summary_align eq 'right') {
673             # if we are aligning summary to the right, we want to fill column
674             # width width
675 0 0       0 if ($max_columns <= 0) {
676 0         0 $max_columns = _num_columns(
677             $terminal_width, ($max_entry_width + 2 + $max_summ_width));
678             }
679 0         0 $column_width = _column_width($terminal_width, $max_columns);
680 0         0 my $new_max_summ_width = $column_width - 2 - $max_entry_width;
681 0 0       0 $max_summ_width = $new_max_summ_width
682             if $max_summ_width < $new_max_summ_width;
683             #warn "max_columns=$max_columns, column_width=$column_width, max_summ_width=$max_summ_width\n";
684             }
685              
686 0         0 for (0..$#words) {
687 0         0 my $summary = $summaries[$_];
688 0 0       0 if (length $summary) {
689 0 0       0 $res[$_] = sprintf(
690             "%-${max_entry_width}s |%".
691             ($summary_align eq 'right' ? $max_summ_width : '')."s",
692             $words[$_], $summary);
693             }
694             }
695             } # FORMAT_SUMMARIES
696              
697             MAX_COLUMNS: {
698 14 50       19 last unless $max_columns > 0;
  14         29  
699 0         0 my $max_entry_width = 0;
700 0         0 for (@res) {
701 0 0       0 $max_entry_width = length if $max_entry_width < length;
702             }
703 0 0       0 last if $max_entry_width >= $column_width;
704 0         0 for (@res) {
705 0 0       0 $_ .= " " x ($column_width - length) if $column_width > length;
706             }
707             }
708              
709             PASS_TO_FZF: {
710 14 50       15 last if $ENV{INSIDE_EMACS};
  14         33  
711 14 50       27 last unless $ENV{COMPLETE_BASH_FZF};
712 0   0     0 my $items = $ENV{COMPLETE_BASH_FZF_ITEMS} // 100;
713 0 0       0 if ($items == -1) {
714 0         0 $items = _terminal_height();
715             }
716 0 0       0 last unless @words >= $items;
717              
718 0         0 require File::Which;
719 0 0       0 unless (File::Which::which("fzf")) {
720             #@res = $code_return_message->("Cannot find fzf to filter ".
721             # scalar(@words)." items");
722 0         0 goto RETURN_RES;
723             }
724              
725 0         0 require IPC::Open2;
726 0         0 local *CHLD_OUT;
727 0         0 local *CHLD_IN;
728             my $pid = IPC::Open2::open2(
729             \*CHLD_OUT, \*CHLD_IN, "fzf", "-m", "-d:", "--with-nth=2..")
730 0 0       0 or do {
731 0         0 @res = $code_return_message->("Cannot open fzf to filter ".
732             scalar(@words)." items");
733 0         0 goto RETURN_RES;
734             };
735              
736 0         0 print CHLD_IN map { "$_:$res[$_]\n" } 0..$#res;
  0         0  
737 0         0 close CHLD_IN;
738              
739 0         0 my @res_words;
740 0         0 while () {
741 0 0       0 my ($index) = /\A([0-9]+)\:/ or next;
742 0         0 push @res_words, $words[$index];
743             }
744 0 0       0 if (@res_words) {
745 0         0 @res = join(" ", @res_words);
746             } else {
747 0         0 @res = ();
748             }
749 0         0 waitpid($pid, 0);
750             }
751              
752             RETURN_RES:
753             #use Data::Dump; warn Data::Dump::dump(\@res);
754 15 100       28 if ($as eq 'array') {
755 1         11 return \@res;
756             } else {
757 14         26 return join("", map {($_, "\n")} @res);
  26         188  
758             }
759             }
760              
761             1;
762             # ABSTRACT: Completion routines for bash shell
763              
764             __END__