File Coverage

blib/lib/Complete/Util.pm
Criterion Covered Total %
statement 281 330 85.1
branch 171 240 71.2
condition 51 79 64.5
subroutine 15 19 78.9
pod 9 10 90.0
total 527 678 77.7


line stmt bran cond sub pod time code
1             package Complete::Util;
2              
3 7     7   484551 use 5.010001;
  7         91  
4 7     7   37 use strict;
  7         16  
  7         210  
5 7     7   39 use warnings;
  7         14  
  7         233  
6 7     7   12521 use Log::ger;
  7         372  
  7         38  
7              
8 7     7   4986 use Complete::Common qw(:all);
  7         2902  
  7         1146  
9 7     7   51 use Exporter qw(import);
  7         32  
  7         31150  
10              
11             our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
12             our $DATE = '2023-01-17'; # DATE
13             our $DIST = 'Complete-Util'; # DIST
14             our $VERSION = '0.614'; # VERSION
15              
16             our @EXPORT_OK = qw(
17             hashify_answer
18             arrayify_answer
19             combine_answers
20             modify_answer
21             ununiquify_answer
22             answer_has_entries
23             answer_num_entries
24             complete_array_elem
25             complete_hash_key
26             complete_comma_sep
27             );
28              
29             our %SPEC;
30              
31             our $COMPLETE_UTIL_TRACE = $ENV{COMPLETE_UTIL_TRACE} // 0;
32              
33             our %arg0_answer = (
34             answer => {
35             summary => 'Completion answer structure',
36             schema => ['any*' => of => ['array*','hash*']],
37             req => 1,
38             pos => 0,
39             },
40             );
41              
42             $SPEC{':package'} = {
43             v => 1.1,
44             summary => 'General completion routine',
45             description => <<'_',
46              
47             This package provides some generic completion routines that follow the
48             convention. (If you are looking for bash/shell tab completion
49             routines, take a look at the See Also section.) The main routine is
50             `complete_array_elem` which tries to complete a word using choices from elements
51             of supplied array. For example:
52              
53             complete_array_elem(word => "a", array => ["apple", "apricot", "banana"]);
54              
55             The routine will first try a simple substring prefix matching. If that fails,
56             will try some other methods like word-mode, character-mode, or fuzzy matching.
57             These methods can be disabled using settings.
58              
59             There are other utility routines e.g. for converting completion answer structure
60             from hash to array/array to hash, combine or modify answer, etc. These routines
61             are usually used by the other more specific or higher-level completion modules.
62              
63             _
64             };
65              
66             $SPEC{hashify_answer} = {
67             v => 1.1,
68             summary => 'Make sure we return completion answer in hash form',
69             description => <<'_',
70              
71             This function accepts a hash or an array. If it receives an array, will convert
72             the array into `{words=>$ary}' first to make sure the completion answer is in
73             hash form.
74              
75             Then will add keys from `meta` to the hash.
76              
77             _
78             args => {
79             %arg0_answer,
80             meta => {
81             summary => 'Metadata (extra keys) for the hash',
82             schema => 'hash*',
83             pos => 1,
84             },
85             },
86             args_as => 'array',
87             result_naked => 1,
88             result => {
89             schema => 'hash*',
90             },
91             };
92             sub hashify_answer {
93 0     0 1 0 my $ans = shift;
94 0 0       0 return unless defined $ans;
95 0 0       0 if (ref($ans) ne 'HASH') {
96 0         0 $ans = {words=>$ans};
97             }
98 0 0       0 if (@_) {
99 0         0 my $meta = shift;
100 0         0 for (keys %$meta) {
101 0         0 $ans->{$_} = $meta->{$_};
102             }
103             }
104 0         0 $ans;
105             }
106              
107             $SPEC{arrayify_answer} = {
108             v => 1.1,
109             summary => 'Make sure we return completion answer in array form',
110             description => <<'_',
111              
112             This is the reverse of `hashify_answer`. It accepts a hash or an array. If it
113             receives a hash, will return its `words` key.
114              
115             _
116             args => {
117             %arg0_answer,
118             },
119             args_as => 'array',
120             result_naked => 1,
121             result => {
122             schema => 'array*',
123             },
124             };
125             sub arrayify_answer {
126 0     0 1 0 my $ans = shift;
127 0 0       0 return unless defined $ans;
128 0 0       0 if (ref($ans) eq 'HASH') {
129 0         0 $ans = $ans->{words};
130             }
131 0         0 $ans;
132             }
133              
134             $SPEC{answer_num_entries} = {
135             v => 1.1,
136             summary => 'Get the number of entries in an answer',
137             description => <<'_',
138              
139             It is equivalent to:
140              
141             ref $answer eq 'ARRAY' ? (@$answer // 0) : (@{$answer->{words}} // 0);
142              
143             _
144             args => {
145             %arg0_answer,
146             },
147             args_as => 'array',
148             result_naked => 1,
149             result => {
150             schema => 'int*',
151             },
152             };
153             sub answer_num_entries {
154 7     7 1 106 my $ans = shift;
155 7 50       19 return unless defined $ans;
156 7 100 100     35 return ref($ans) eq 'HASH' ? (@{$ans->{words} // []} // 0) : (@$ans // 0);
  4   50     29  
      50        
157             }
158              
159             $SPEC{answer_has_entries} = {
160             v => 1.1,
161             summary => 'Check if answer has entries',
162             description => <<'_',
163              
164             It is equivalent to:
165              
166             ref $answer eq 'ARRAY' ? (@$answer ? 1:0) : (@{$answer->{words}} ? 1:0);
167              
168             _
169             args => {
170             %arg0_answer,
171             },
172             args_as => 'array',
173             result_naked => 1,
174             result => {
175             schema => 'int*',
176             },
177             };
178             sub answer_has_entries {
179 5     5 1 97 my $ans = shift;
180 5 50       12 return unless defined $ans;
181 5 100 100     24 return ref($ans) eq 'HASH' ? (@{$ans->{words} // []} ? 1:0) : (@$ans ? 1:0);
  3 100       23  
    100          
182             }
183              
184             sub __min(@) { ## no critic: Subroutines::ProhibitSubroutinePrototypes
185 6     6   8 my $m = $_[0];
186 6         11 for (@_) {
187 12 100       26 $m = $_ if $_ < $m;
188             }
189 6         14 $m;
190             }
191              
192             our $code_editdist;
193             our $editdist_flex;
194              
195             # straight copy of Wikipedia's "Levenshtein Distance"
196             sub __editdist {
197 0     0   0 my @a = split //, shift;
198 0         0 my @b = split //, shift;
199              
200             # There is an extra row and column in the matrix. This is the distance from
201             # the empty string to a substring of the target.
202 0         0 my @d;
203 0         0 $d[$_][0] = $_ for 0 .. @a;
204 0         0 $d[0][$_] = $_ for 0 .. @b;
205              
206 0         0 for my $i (1 .. @a) {
207 0         0 for my $j (1 .. @b) {
208 0 0       0 $d[$i][$j] = (
209             $a[$i-1] eq $b[$j-1]
210             ? $d[$i-1][$j-1]
211             : 1 + __min(
212             $d[$i-1][$j],
213             $d[$i][$j-1],
214             $d[$i-1][$j-1]
215             )
216             );
217             }
218             }
219              
220 0         0 $d[@a][@b];
221             }
222              
223             my %complete_array_elem_args = (
224             %arg_word,
225             array => {
226             schema => ['array*'=>{of=>'str*'}],
227             req => 1,
228             pos => 1,
229             slurpy => 1,
230             },
231             summaries => {
232             schema => ['array*'=>{of=>'str*'}],
233             },
234             exclude => {
235             schema => ['array*'],
236             },
237             replace_map => {
238             schema => ['hash*', each_value=>['array*', of=>'str*']],
239             description => <<'_',
240              
241             You can supply correction entries in this option. An example is when array if
242             `['mount','unmount']` and `umount` is a popular "typo" for `unmount`. When
243             someone already types `um` it cannot be completed into anything (even the
244             current fuzzy mode will return *both* so it cannot complete immediately).
245              
246             One solution is to add replace_map `{'unmount'=>['umount']}`. This way, `umount`
247             will be regarded the same as `unmount` and when user types `um` it can be
248             completed unambiguously into `unmount`.
249              
250             _
251             tags => ['experimental'],
252             },
253             );
254              
255             $SPEC{complete_array_elem} = {
256             v => 1.1,
257             summary => 'Complete from array',
258             description => <<'_',
259              
260             Try to find completion from an array of strings. Will attempt several methods,
261             from the cheapest and most discriminating to the most expensive and least
262             discriminating.
263              
264             First method is normal/exact string prefix matching (either case-sensitive or
265             insensitive depending on the `$Complete::Common::OPT_CI` variable or the
266             `COMPLETE_OPT_CI` environment variable). If at least one match is found, return
267             result. Else, proceed to the next method.
268              
269             Word-mode matching (can be disabled by setting
270             `$Complete::Common::OPT_WORD_MODE` or `COMPLETE_OPT_WORD_MODE` environment
271             varialbe to false). Word-mode matching is described in . If
272             at least one match is found, return result. Else, proceed to the next method.
273              
274             Prefix char-mode matching (can be disabled by settings
275             `$Complete::Common::OPT_CHAR_MODE` or `COMPLETE_OPT_CHAR_MODE` environment
276             variable to false). Prefix char-mode matching is just like char-mode matching
277             (see next paragraph) except the first character must match. If at least one
278             match is found, return result. Else, proceed to the next method.
279              
280             Char-mode matching (can be disabled by settings
281             `$Complete::Common::OPT_CHAR_MODE` or `COMPLETE_OPT_CHAR_MODE` environment
282             variable to false). Char-mode matching is described in . If
283             at least one match is found, return result. Else, proceed to the next method.
284              
285             Fuzzy matching (can be disabled by setting `$Complete::Common::OPT_FUZZY` or
286             `COMPLETE_OPT_FUZZY` to false). Fuzzy matching is described in
287             . If at least one match is found, return result. Else,
288             return empty string.
289              
290             Will sort the resulting completion list, so you don't have to presort the array.
291              
292             _
293             args => {
294             %complete_array_elem_args,
295             },
296             result_naked => 1,
297             result => {
298             schema => 'array',
299             },
300             };
301             sub complete_array_elem {
302 48     48 1 30309 my %args = @_;
303              
304 48 50       129 my $array0 = $args{array} or die "Please specify array";
305 48         78 my $summaries = $args{summaries};
306 48   50     98 my $word = $args{word} // "";
307              
308 48         70 my $ci = $Complete::Common::OPT_CI;
309 48         72 my $map_case = $Complete::Common::OPT_MAP_CASE;
310 48         62 my $word_mode = $Complete::Common::OPT_WORD_MODE;
311 48         66 my $char_mode = $Complete::Common::OPT_CHAR_MODE;
312 48         72 my $fuzzy = $Complete::Common::OPT_FUZZY;
313              
314 48 50       98 log_trace("[computil] entering complete_array_elem(), word=<%s>", $word)
315             if $COMPLETE_UTIL_TRACE;
316              
317 48         58 my $res;
318              
319 48 100       102 unless (@$array0) {
320 1         2 $res = []; goto RETURN_RES;
  1         7  
321             }
322              
323             # normalize
324 47 100       88 my $wordn = $ci ? uc($word) : $word; $wordn =~ s/_/-/g if $map_case;
  47 100       93  
325              
326 47         64 my $excluden;
327 47 100       121 if ($args{exclude}) {
328 3         6 $excluden = {};
329 3         5 for my $el (@{$args{exclude}}) {
  3         8  
330 7 100       14 my $eln = $ci ? uc($el) : $el; $eln =~ s/_/-/g if $map_case;
  7 100       15  
331 7   50     27 $excluden->{$eln} //= 1;
332             }
333             }
334              
335 47         74 my $rmapn;
336             my $rev_rmapn; # to replace back to the original words back in the result
337 47 100       104 if (my $rmap = $args{replace_map}) {
338 4         7 $rmapn = {};
339 4         7 $rev_rmapn = {};
340 4         15 for my $k (keys %$rmap) {
341 4 100       11 my $kn = $ci ? uc($k) : $k; $kn =~ s/_/-/g if $map_case;
  4 50       8  
342 4         6 my @vn;
343 4         9 for my $v (@{ $rmap->{$k} }) {
  4         10  
344 4 100       8 my $vn = $ci ? uc($v) : $v; $vn =~ s/_/-/g if $map_case;
  4 50       9  
345 4         9 push @vn, $vn;
346 4   33     19 $rev_rmapn->{$vn} //= $k;
347             }
348 4         14 $rmapn->{$kn} = \@vn;
349             }
350             }
351              
352 47         169 my @words; # the answer
353             my @wordsumms; # summaries for each item in @words
354 47         0 my @array ; # original array + rmap entries
355 47         0 my @arrayn; # case- & map-case-normalized form of $array + rmap entries
356 47         0 my @arraysumms; # summaries for each item in @array (or @arrayn)
357              
358             # normal string prefix matching. we also fill @array & @arrayn here (which
359             # will be used again in word-mode, fuzzy, and char-mode matching) so we
360             # don't have to calculate again.
361 47 50       81 log_trace("[computil] Trying normal string-prefix matching ...") if $COMPLETE_UTIL_TRACE;
362 47         71 for my $i (0..$#{$array0}) {
  47         125  
363 183         264 my $el = $array0->[$i];
364 183 100       276 my $eln = $ci ? uc($el) : $el; $eln =~ s/_/-/g if $map_case;
  183 100       307  
365 183 100 100     312 next if $excluden && $excluden->{$eln};
366 178         245 push @array , $el;
367 178         230 push @arrayn, $eln;
368 178 100       295 push @arraysumms, $summaries->[$i] if $summaries;
369 178 100       364 if (0==index($eln, $wordn)) {
370 65         89 push @words, $el;
371 65 100       111 push @wordsumms, $summaries->[$i] if $summaries;
372             }
373 178 100 100     369 if ($rmapn && $rmapn->{$eln}) {
374 4         18 for my $vn (@{ $rmapn->{$eln} }) {
  4         12  
375 4         11 push @array , $el;
376 4         5 push @arrayn, $vn;
377             # we add the normalized form, because we'll just revert it back
378             # to the original word in the final result
379 4 100       28 if (0==index($vn, $wordn)) {
380 3         6 push @words, $vn;
381 3 50       9 push @wordsumms, $summaries->[$i] if $summaries;
382             }
383             }
384             }
385             }
386 47 50 66     162 log_trace("[computil] Result from normal string-prefix matching: %s", \@words) if @words && $COMPLETE_UTIL_TRACE;
387              
388             # word-mode matching
389             {
390 47 100 66     72 last unless $word_mode && !@words;
  47         116  
391 2         16 my @split_wordn = $wordn =~ /(\w+)/g;
392 2 100       11 unshift @split_wordn, '' if $wordn =~ /\A\W/;
393 2 50       9 last unless @split_wordn > 1;
394 2         5 my $re = '\A';
395 2         4 for my $i (0..$#split_wordn) {
396 4 100       11 $re .= '(?:\W+\w+)*\W+' if $i;
397 4         12 $re .= quotemeta($split_wordn[$i]).'\w*';
398             }
399 2         129 $re = qr/$re/;
400 2 50       9 log_trace("[computil] Trying word-mode matching (re=%s) ...", $re) if $COMPLETE_UTIL_TRACE;
401              
402 2         8 for my $i (0..$#array) {
403 8         11 my $match;
404             {
405 8 100       12 if ($arrayn[$i] =~ $re) {
  8         48  
406 4         10 $match++;
407 4         7 last;
408             }
409             # try splitting CamelCase into Camel-Case
410 4         8 my $tmp = $array[$i];
411 4 50       16 if ($tmp =~ s/([a-z0-9_])([A-Z])/$1-$2/g) {
412 0 0       0 $tmp = uc($tmp) if $ci; $tmp =~ s/_/-/g if $map_case; # normalize again
  0 0       0  
413 0 0       0 if ($tmp =~ $re) {
414 0         0 $match++;
415 0         0 last;
416             }
417             }
418             }
419 8 100       17 next unless $match;
420 4         7 push @words, $array[$i];
421 4 50       9 push @wordsumms, $arraysumms[$i] if $summaries;
422             }
423 2 50 33     16 log_trace("[computil] Result from word-mode matching: %s", \@words) if @words && $COMPLETE_UTIL_TRACE;
424             }
425              
426             # prefix char-mode matching
427 47 50 100     136 if ($char_mode && !@words && length($wordn) && length($wordn) <= 7) {
      66        
      66        
428 4         16 my $re = join(".*", map {quotemeta} split(//, $wordn));
  7         24  
429 4         56 $re = qr/\A$re/;
430 4 50       13 log_trace("[computil] Trying prefix char-mode matching (re=%s) ...", $re) if $COMPLETE_UTIL_TRACE;
431 4         12 for my $i (0..$#array) {
432 16 100       67 if ($arrayn[$i] =~ $re) {
433 1         4 push @words, $array[$i];
434 1 50       4 push @wordsumms, $arraysumms[$i] if $summaries;
435             }
436             }
437 4 50 66     19 log_trace("[computil] Result from prefix char-mode matching: %s", \@words) if @words && $COMPLETE_UTIL_TRACE;
438             }
439              
440             # char-mode matching
441 47 50 100     128 if ($char_mode && !@words && length($wordn) && length($wordn) <= 7) {
      66        
      66        
442 3         9 my $re = join(".*", map {quotemeta} split(//, $wordn));
  5         16  
443 3         31 $re = qr/$re/;
444 3 50       11 log_trace("[computil] Trying char-mode matching (re=%s) ...", $re) if $COMPLETE_UTIL_TRACE;
445 3         9 for my $i (0..$#array) {
446 13 100       52 if ($arrayn[$i] =~ $re) {
447 2         9 push @words, $array[$i];
448 2 50       7 push @wordsumms, $arraysumms[$i] if $summaries;
449             }
450             }
451 3 50 66     21 log_trace("[computil] Result from char-mode matching: %s", \@words) if @words && $COMPLETE_UTIL_TRACE;
452             }
453              
454             # fuzzy matching
455 47 100 66     103 if ($fuzzy && !@words) {
456 1 50       4 log_trace("[computil] Trying fuzzy matching ...") if $COMPLETE_UTIL_TRACE;
457 1   33     5 $code_editdist //= do {
458 1   50     6 my $env = $ENV{COMPLETE_UTIL_LEVENSHTEIN} // '';
459 1 50       8 if ($env eq 'xs') {
    50          
    50          
    50          
460 0         0 require Text::Levenshtein::XS;
461 0         0 $editdist_flex = 0;
462 0         0 \&Text::Levenshtein::XS::distance;
463             } elsif ($env eq 'flexible') {
464 0         0 require Text::Levenshtein::Flexible;
465 0         0 $editdist_flex = 1;
466 0         0 \&Text::Levenshtein::Flexible::levenshtein_l;
467             } elsif ($env eq 'pp') {
468 0         0 $editdist_flex = 0;
469 0         0 \&__editdist;
470 1         722 } elsif (eval { require Text::Levenshtein::Flexible; 1 }) {
  1         980  
471 1         3 $editdist_flex = 1;
472 1         5 \&Text::Levenshtein::Flexible::levenshtein_l;
473             } else {
474 0         0 $editdist_flex = 0;
475 0         0 \&__editdist;
476             }
477             };
478              
479 1         3 my $factor = 1.3;
480 1         3 my $x = -1;
481 1         2 my $y = 1;
482              
483             # note: we cannot use Text::Levenshtein::Flexible::levenshtein_l_all()
484             # because we perform distance calculation on the normalized array but we
485             # want to get the original array elements
486              
487 1         1 my %editdists;
488             ELEM:
489 1         7 for my $i (0..$#array) {
490 3         6 my $eln = $arrayn[$i];
491              
492 3         8 for my $l (length($wordn)-$y .. length($wordn)+$y) {
493 3 50       8 next if $l <= 0;
494 3         6 my $chopped = substr($eln, 0, $l);
495 3         8 my $maxd = __min(
496             __min(length($chopped), length($word))/$factor,
497             $fuzzy,
498             );
499 3         5 my $d;
500 3 50       8 unless (defined $editdists{$chopped}) {
501 3 50       6 if ($editdist_flex) {
502 3         17 $d = $code_editdist->($wordn, $chopped, $maxd);
503 3 100       9 next ELEM unless defined $d;
504             } else {
505 0         0 $d = $code_editdist->($wordn, $chopped);
506             }
507 1         4 $editdists{$chopped} = $d;
508             } else {
509 0         0 $d = $editdists{$chopped};
510             }
511             #say "D: d($word,$chopped)=$d (maxd=$maxd)";
512 1 50       2 next unless $d <= $maxd;
513 1         18 push @words, $array[$i];
514 1 50       3 push @wordsumms, $arraysumms[$i] if $summaries;
515 1         3 next ELEM;
516             }
517             }
518 1 50 33     15 log_trace("[computil] Result from fuzzy matching: %s", \@words) if @words && $COMPLETE_UTIL_TRACE;
519             }
520              
521             # replace back the words from replace_map
522 47 100 66     94 if ($rmapn && @words) {
523 4         7 my @wordsn;
524 4         7 for my $el (@words) {
525 4 100       11 my $eln = $ci ? uc($el) : $el; $eln =~ s/_/-/g if $map_case;
  4 50       6  
526 4         9 push @wordsn, $eln;
527             }
528 4         9 for my $i (0..$#words) {
529 4 100       12 if (my $w = $rev_rmapn->{$wordsn[$i]}) {
530 3         7 $words[$i] = $w;
531             }
532             }
533             }
534              
535             # sort results and insert summaries
536             $res = [
537             map {
538 76 100       220 $summaries ?
539             {word=>$words[$_], summary=>$wordsumms[$_]} :
540             $words[$_]
541             }
542             sort {
543 47 100       166 $ci ?
  50         141  
544             lc($words[$a]) cmp lc($words[$b]) :
545             $words[$a] cmp $words[$b] }
546             0 .. $#words
547             ];
548              
549 48 50       108 RETURN_RES:
550             log_trace("[computil] leaving complete_array_elem(), res=%s", $res)
551             if $COMPLETE_UTIL_TRACE;
552 48         220 $res;
553             }
554              
555             $SPEC{complete_hash_key} = {
556             v => 1.1,
557             summary => 'Complete from hash keys',
558             args => {
559             %arg_word,
560             hash => { schema=>['hash*'=>{}], req=>1 },
561             summaries => { schema=>['hash*'=>{}] },
562             summaries_from_hash_values => { schema=>'true*' },
563             },
564             result_naked => 1,
565             result => {
566             schema => 'array',
567             },
568             args_rels => {
569             choose_one => ['summaries', 'summaries_from_hash_values'],
570             },
571             };
572             sub complete_hash_key {
573 4     4 1 2637 my %args = @_;
574 4 50       14 my $hash = $args{hash} or die "Please specify hash";
575 4   50     11 my $word = $args{word} // "";
576 4         10 my $summaries = $args{summaries};
577 4         5 my $summaries_from_hash_values = $args{summaries_from_hash_values};
578              
579 4         16 my @keys = keys %$hash;
580 4         8 my @summaries;
581             my $has_summary;
582 4 100       12 if ($summaries) {
    100          
583 1         2 $has_summary++;
584 1         3 for (@keys) { push @summaries, $summaries->{$_} }
  5         10  
585             } elsif ($summaries_from_hash_values) {
586 1         2 $has_summary++;
587 1         3 for (@keys) { push @summaries, $hash->{$_} }
  5         8  
588             }
589              
590             complete_array_elem(
591 4         13 word=>$word, array=>\@keys,
592             (summaries=>\@summaries) x !!$has_summary,
593             );
594             }
595              
596             my %complete_comma_sep_args = (
597             %complete_array_elem_args,
598             sep => {
599             schema => 'str*',
600             default => ',',
601             },
602             uniq => {
603             summary => 'Whether list should contain unique elements',
604             description => <<'_',
605              
606             When this option is set to true, if the formed list in the current word already
607             contains an element, the element will not be offered again as completion answer.
608             For example, if `elems` is `[1,2,3,4]` and `word` is `2,3,` then without `uniq`
609             set to true the completion answer is:
610              
611             2,3,1
612             2,3,2
613             2,3,3
614             2,3,4
615              
616             but with `uniq` set to true, the completion answer becomes:
617              
618             2,3,1
619             2,3,4
620              
621             See also the `remaining` option for a more general mechanism of offering fewer
622             elements.
623              
624             _
625             schema => ['bool*', is=>1],
626             },
627             remaining => {
628             schema => ['code*'],
629             summary => 'What elements should remain for completion',
630             description => <<'_',
631              
632             This is a more general mechanism if the `uniq` option does not suffice. Suppose
633             you are offering completion for sorting fields. The elements are field names as
634             well as field names prefixed with dash (`-`) to mean sorting with a reverse
635             order. So for example `elems` is `["name","-name","age","-age"]`. When current
636             word is `name`, it doesn't make sense to offer `name` nor `-name` again as the
637             next sorting field. So we can set `remaining` to this code:
638              
639             sub {
640             my ($seen_elems, $elems) = @_;
641              
642             my %seen;
643             for (@$seen_elems) {
644             (my $nodash = $_) =~ s/^-//;
645             $seen{$nodash}++;
646             }
647              
648             my @remaining;
649             for (@$elems) {
650             (my $nodash = $_) =~ s/^-//;
651             push @remaining, $_ unless $seen{$nodash};
652             }
653              
654             \@remaining;
655             }
656              
657             As you can see above, the code is given `$seen_elems` and `$elems` as arguments
658             and is expected to return remaining elements to offer.
659              
660             _
661             tags => ['hidden-cli'],
662             },
663             );
664             $complete_comma_sep_args{elems} = delete $complete_comma_sep_args{array};
665              
666             $SPEC{complete_comma_sep} = {
667             v => 1.1,
668             summary => 'Complete a comma-separated list string',
669             args => {
670             %complete_comma_sep_args,
671             },
672             result_naked => 1,
673             result => {
674             schema => 'array',
675             },
676             };
677             sub complete_comma_sep {
678 21     21 1 18306 my %args = @_;
679 21   50     68 my $word = delete $args{word} // "";
680 21   50     85 my $sep = delete $args{sep} // ',';
681 21 50       58 my $elems = delete $args{elems} or die "Please specify elems";
682 21         35 my $summaries = delete $args{summaries};
683 21         31 my $uniq = delete $args{uniq};
684 21         29 my $remaining = delete $args{remaining};
685              
686 21         34 my $ci = $Complete::Common::OPT_CI;
687              
688 21         28 my %summaries_for; # key=elem val=summary
689             GEN_SUMMARIES_HASH:
690             {
691 21 100       30 last unless $summaries;
  21         46  
692 2         3 for my $i (0 .. $#{$elems}) {
  2         6  
693 7         13 my $elem0 = $elems->[$i];
694 7         9 my $summary = $summaries->[$i];
695 7 50       13 my $elem = $ci ? lc($elem0) : $elem0;
696 7 50       14 if (exists $summaries_for{$elem}) {
697 0         0 log_warn "Non-unique value '$elem', using only the first summary for it";
698 0         0 next;
699             }
700 7         17 $summaries_for{$elem} = $summary;
701             }
702             } # GEN_SUMMARIES_HASH
703              
704 21         96 my @mentioned_elems = split /\Q$sep\E/, $word, -1;
705 21 100       64 my $cae_word = @mentioned_elems ? pop(@mentioned_elems) : ''; # cae=complete_array_elem
706              
707 21         33 my $remaining_elems;
708 21 100       47 if ($remaining) {
    100          
709 4         11 $remaining_elems = $remaining->(\@mentioned_elems, $elems);
710             } elsif ($uniq) {
711 6         9 my %mem;
712 6         9 $remaining_elems = [];
713 6         13 for (@mentioned_elems) {
714 10 50       16 if ($ci) { $mem{lc $_}++ } else { $mem{$_}++ }
  0         0  
  10         22  
715             }
716 6         11 for (@$elems) {
717 18 50       46 push @$remaining_elems, $_ unless ($ci ? $mem{lc $_} : $mem{$_});
    100          
718             }
719             } else {
720 11         18 $remaining_elems = $elems;
721             }
722              
723             my $cae_res = complete_array_elem(
724             %args,
725             word => $cae_word,
726             array => $remaining_elems,
727 21 50       217 ($summaries ? (summaries=>[map {$summaries_for{ $ci ? lc($_):$_ }} @$remaining_elems]) : ()),
  5 100       17  
728             );
729              
730 21         53 my $prefix = join($sep, @mentioned_elems);
731 21 100       51 $prefix .= $sep if @mentioned_elems;
732 21 100       35 $cae_res = [map { ref $_ eq 'HASH' ? { %$_, word=>"$prefix$_->{word}" } : "$prefix$_" } @$cae_res];
  36         131  
733              
734             # add trailing comma for convenience, where appropriate
735             {
736 21 100       32 last unless @$cae_res == 1;
  21         75  
737 5 100       16 last if @$remaining_elems <= 1;
738 4 50       64 $cae_res = [{word=>$cae_res->[0]}] unless ref $cae_res->[0] eq 'HASH';
739 4 50       24 $cae_res = [{word=>"$cae_res->[0]{word}$sep", (defined $cae_res->[0]{summary} ? (summary=>$cae_res->[0]{summary}) : ()), is_partial=>1}];
740             }
741 21         72 $cae_res;
742             }
743              
744             $SPEC{combine_answers} = {
745             v => 1.1,
746             summary => 'Given two or more answers, combine them into one',
747             description => <<'_',
748              
749             This function is useful if you want to provide a completion answer that is
750             gathered from multiple sources. For example, say you are providing completion
751             for the Perl tool , which accepts a filename (a tarball like
752             `*.tar.gz`), a directory, or a module name. You can do something like this:
753              
754             combine_answers(
755             complete_file(word=>$word),
756             complete_module(word=>$word),
757             );
758              
759             But if a completion answer has a metadata `final` set to true, then that answer
760             is used as the final answer without any combining with the other answers.
761              
762             _
763             args => {
764             answers => {
765             schema => [
766             'array*' => {
767             of => ['any*', of=>['hash*','array*']], # XXX answer_t
768             min_len => 1,
769             },
770             ],
771             req => 1,
772             pos => 0,
773             greedy => 1,
774             },
775             },
776             args_as => 'array',
777             result_naked => 1,
778             result => {
779             schema => 'hash*',
780             description => <<'_',
781              
782             Return a combined completion answer. Words from each input answer will be
783             combined, order preserved and duplicates removed. The other keys from each
784             answer will be merged.
785              
786             _
787             },
788             };
789             sub combine_answers {
790 10     10 1 28169 require List::Util;
791              
792 10 100       32 return unless @_;
793 9 100       33 return $_[0] if @_ < 2;
794              
795 8         45 my $final = {words=>[]};
796 8         16 my $encounter_hash;
797             my $add_words = sub {
798 17     17   24 my $words = shift;
799 17         30 for my $entry (@$words) {
800 29         98 push @{ $final->{words} }, $entry
801             unless List::Util::first(
802             sub {
803             (ref($entry) ? $entry->{word} : $entry)
804             eq
805 75 100       171 (ref($_) ? $_->{word} : $_)
    100          
806 34 100       76 }, @{ $final->{words} }
  34         85  
807             );
808             }
809 8         36 };
810              
811             ANSWER:
812 8         17 for my $ans (@_) {
813 20 100       58 if (ref($ans) eq 'ARRAY') {
    50          
814 9         16 $add_words->($ans);
815             } elsif (ref($ans) eq 'HASH') {
816 11         14 $encounter_hash++;
817              
818 11 100       25 if ($ans->{final}) {
819 3         8 $final = $ans;
820 3         6 last ANSWER;
821             }
822              
823 8   50     28 $add_words->($ans->{words} // []);
824 8         26 for (keys %$ans) {
825 17 100       29 if ($_ eq 'words') {
    100          
826 8         16 next;
827             } elsif ($_ eq 'static') {
828 6 100       18 if (exists $final->{$_}) {
829 4   66     36 $final->{$_} &&= $ans->{$_};
830             } else {
831 2         9 $final->{$_} = $ans->{$_};
832             }
833             } else {
834 3         8 $final->{$_} = $ans->{$_};
835             }
836             }
837             }
838             }
839              
840 8 100       49 $encounter_hash ? $final : $final->{words};
841             }
842              
843             $SPEC{modify_answer} = {
844             v => 1.1,
845             summary => 'Modify answer (add prefix/suffix, etc)',
846             args => {
847             answer => {
848             schema => ['any*', of=>['hash*','array*']], # XXX answer_t
849             req => 1,
850             pos => 0,
851             },
852             suffix => {
853             schema => 'str*',
854             },
855             prefix => {
856             schema => 'str*',
857             },
858             },
859             result_naked => 1,
860             result => {
861             schema => 'undef',
862             },
863             };
864             sub modify_answer {
865 3     3 1 106 my %args = @_;
866              
867 3         7 my $answer = $args{answer};
868 3 100       17 my $words = ref($answer) eq 'HASH' ? $answer->{words} : $answer;
869              
870 3 50       9 if (defined(my $prefix = $args{prefix})) {
871 3         7 for (@$words) {
872 9 100       18 if (ref $_ eq 'HASH') {
873 1         4 $_->{word} = "$prefix$_->{word}";
874             } else {
875 8         14 $_ = "$prefix$_";
876             }
877             }
878             }
879 3 50       8 if (defined(my $suffix = $args{suffix})) {
880 3         5 for (@$words) {
881 9 100       18 if (ref $_ eq 'HASH') {
882 1         3 $_->{word} = "$_->{word}$suffix";
883             } else {
884 8         10 $_ = "$_$suffix";
885             }
886             }
887             }
888 3         24 $answer;
889             }
890              
891             $SPEC{ununiquify_answer} = {
892             v => 1.1,
893             summary => 'If answer contains only one item, make it two',
894             description => <<'_',
895              
896             For example, if answer is `["a"]`, then will make answer become `["a","a "]`.
897             This will prevent shell from automatically adding space.
898              
899             _
900             args => {
901             answer => {
902             schema => ['any*', of=>['hash*','array*']], # XXX answer_t
903             req => 1,
904             pos => 0,
905             },
906             },
907             result_naked => 1,
908             result => {
909             schema => 'undef',
910             },
911             tags => ['hidden'],
912             };
913             sub ununiquify_answer {
914 0     0 0   my %args = @_;
915              
916 0           my $answer = $args{answer};
917 0 0         my $words = ref($answer) eq 'HASH' ? $answer->{words} : $answer;
918              
919 0 0         if (@$words == 1) {
920 0           push @$words, "$words->[0] ";
921             }
922 0           undef;
923             }
924              
925             1;
926             # ABSTRACT: General completion routine
927              
928             __END__