File Coverage

blib/lib/Complete/Util.pm
Criterion Covered Total %
statement 345 400 86.2
branch 209 296 70.6
condition 57 88 64.7
subroutine 16 20 80.0
pod 10 11 90.9
total 637 815 78.1


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