File Coverage

blib/lib/Complete/Bash/History.pm
Criterion Covered Total %
statement 62 131 47.3
branch 14 50 28.0
condition 0 28 0.0
subroutine 5 6 83.3
pod 2 2 100.0
total 83 217 38.2


line stmt bran cond sub pod time code
1             package Complete::Bash::History;
2              
3             our $DATE = '2020-01-29'; # DATE
4             our $VERSION = '0.060'; # VERSION
5              
6 2     2   1138 use 5.010001;
  2         18  
7 2     2   9 use strict;
  2         4  
  2         50  
8 2     2   9 use warnings;
  2         4  
  2         154  
9              
10             require Exporter;
11             our @ISA = qw(Exporter);
12             our @EXPORT_OK = qw(
13             complete_cmdline_from_hist
14             );
15              
16 2     2   1185 use Complete::Bash qw(parse_cmdline join_wordbreak_words);
  2         11362  
  2         2976  
17              
18             our %SPEC;
19              
20             $SPEC{':package'} = {
21             v => 1.1,
22             };
23              
24             $SPEC{parse_options} = {
25             v => 1.1,
26             summary => 'Parse command-line for options and arguments, '.
27             'more or less like Getopt::Long',
28             description => <<'_',
29              
30             Parse command-line into words using 's `parse_cmdline()` then
31             separate options and arguments. Since this routine does not accept
32             (this routine is meant to be a generic option parsing of
33             command-lines), it uses a few simple rules to server the common cases:
34              
35             * After `--`, the rest of the words are arguments (just like Getopt::Long).
36              
37             * If we get something like `-abc` (a single dash followed by several letters) it
38             is assumed to be a bundle of short options.
39              
40             * If we get something like `-MData::Dump` (a single dash, followed by a letter,
41             followed by some letters *and* non-letters/numbers) it is assumed to be an
42             option (`-M`) followed by a value.
43              
44             * If we get something like `--foo` it is a long option. If the next word is an
45             option (starts with a `-`) then it is assumed that this option does not have
46             argument. Otherwise, the next word is assumed to be this option's value.
47              
48             * Otherwise, it is an argument (that is, permute is assumed).
49              
50             _
51              
52             args => {
53             cmdline => {
54             summary => 'Command-line, defaults to COMP_LINE environment',
55             schema => 'str*',
56             },
57             point => {
58             summary => 'Point/position to complete in command-line, '.
59             'defaults to COMP_POINT',
60             schema => 'int*',
61             },
62             words => {
63             summary => 'Alternative to passing `cmdline` and `point`',
64             schema => ['array*', of=>'str*'],
65             description => <<'_',
66              
67             If you already did a `parse_cmdline()`, you can pass the words result (the first
68             element) here to avoid calling `parse_cmdline()` twice.
69              
70             _
71             },
72             cword => {
73             summary => 'Alternative to passing `cmdline` and `point`',
74             schema => ['array*', of=>'str*'],
75             description => <<'_',
76              
77             If you already did a `parse_cmdline()`, you can pass the cword result (the
78             second element) here to avoid calling `parse_cmdline()` twice.
79              
80             _
81             },
82             },
83             result => {
84             schema => 'hash*',
85             },
86             };
87             sub parse_options {
88 2     2 1 1788 my %args = @_;
89              
90 2         7 my ($words, $cword) = @_;
91 2 50       7 if ($args{words}) {
92 0         0 ($words, $cword) = ($args{words}, $args{cword});
93             } else {
94 2         3 ($words, $cword) = @{parse_cmdline($args{cmdline}, $args{point}, {truncate_current_word=>1})};
  2         12  
95             }
96              
97 2         446 ($words, $cword) = @{join_wordbreak_words($words, $cword)};
  2         9  
98              
99             #use DD; dd [$words, $cword];
100              
101 2         100 my @types;
102             my %opts;
103 2         0 my @argv;
104 2         0 my $type;
105 2         5 $types[0] = 'command';
106 2         4 my $i = 1;
107 2         6 while ($i < @$words) {
108 7         13 my $word = $words->[$i];
109 7 100       47 if ($word eq '--') {
    100          
    100          
    100          
110 1 50       4 if ($i == $cword) {
111 0         0 $types[$i] = 'opt_name';
112 0         0 $i++; next;
  0         0  
113             }
114 1         3 $types[$i] = 'separator';
115 1         4 for ($i+1 .. @$words-1) {
116 1         3 $types[$_] = 'arg,' . @argv;
117 1         4 push @argv, $words->[$_];
118             }
119 1         2 last;
120             } elsif ($word =~ /\A-(\w*)\z/) {
121 1         4 $types[$i] = 'opt_name';
122 1         6 for (split '', $1) {
123 3         4 push @{ $opts{$_} }, undef;
  3         10  
124             }
125 1         2 $i++; next;
  1         4  
126             } elsif ($word =~ /\A-([\w?])(.*)/) {
127 1         3 $types[$i] = 'opt_name';
128             # XXX currently not completing option value
129 1         2 push @{ $opts{$1} }, $2;
  1         4  
130 1         2 $i++; next;
  1         3  
131             } elsif ($word =~ /\A--(\w[\w-]*)\z/) {
132 2         6 $types[$i] = 'opt_name';
133 2         4 my $opt = $1;
134 2         8 $i++;
135 2 50       5 if ($i < @$words) {
136 2 50       5 if ($words->[$i] eq '=') {
137 0         0 $types[$i] = 'separator';
138 0         0 $i++;
139             }
140 2 100       8 if ($words->[$i] =~ /\A-/) {
141 1         2 push @{ $opts{$opt} }, undef;
  1         4  
142 1         3 next;
143             }
144 1         2 $types[$i] = 'opt_val';
145 1         2 push @{ $opts{$opt} }, $words->[$i];
  1         5  
146 1         2 $i++; next;
  1         3  
147             }
148             } else {
149 2         6 $types[$i] = 'arg,' . @argv;
150 2         5 push @argv, $word;
151 2         2 $i++; next;
  2         5  
152             }
153             }
154              
155             return {
156 2         28 opts => \%opts,
157             argv => \@argv,
158             cword => $cword,
159             words => $words,
160             word_type => $types[$cword],
161             #_types => \@types,
162             };
163             }
164              
165             $SPEC{complete_cmdline_from_hist} = {
166             v => 1.1,
167             summary => 'Complete command line from recent entries in bash history',
168             description => <<'_',
169              
170             This routine will search your bash history file (recent first a.k.a. backward)
171             for entries for the same command, and complete option with the same name or
172             argument in the same position. For example, if you have history like this:
173              
174             cmd1 --opt1 val arg1 arg2
175             cmd1 --opt1 valb arg1b arg2b arg3b
176             cmd2 --foo
177              
178             Then if you do:
179              
180             complete_cmdline_from_hist(comp_line=>'cmd1 --bar --opt1 ', comp_point=>18);
181              
182             then it means the routine will search for values for option `--opt1` and will
183             return:
184              
185             ["val", "valb"]
186              
187             Or if you do:
188              
189             complete_cmdline_from_hist(comp_line=>'cmd1 baz ', comp_point=>9);
190              
191             then it means the routine will search for second argument (argv[1]) and will
192             return:
193              
194             ["arg2", "arg2b"]
195              
196             _
197             args => {
198             path => {
199             summary => 'Path to `.bash_history` file',
200             schema => 'str*',
201             description => <<'_',
202              
203             Defaults to `~/.bash_history`.
204              
205             If file does not exist or unreadable, will return empty completion answer.
206              
207             _
208             },
209             max_hist_lines => {
210             summary => 'Stop searching after this amount of history lines',
211             schema => ['int*'],
212             default => 3000,
213             description => <<'_',
214              
215             -1 means unlimited (search all lines in the file).
216              
217             Timestamp comments are not counted.
218              
219             _
220             },
221             max_result => {
222             summary => 'Stop after finding this number of distinct results',
223             schema => 'int*',
224             default => 100,
225             description => <<'_',
226              
227             -1 means unlimited.
228              
229             _
230             },
231             cmdline => {
232             summary => 'Command line, defaults to COMP_LINE',
233             schema => 'str*',
234             },
235             point => {
236             summary => 'Command line, defaults to COMP_POINT',
237             schema => 'int*',
238             },
239             },
240             result_naked=>1,
241             };
242             sub complete_cmdline_from_hist {
243 0     0 1   require Complete::Util;
244 0           require File::ReadBackwards;
245              
246 0           my %args = @_;
247              
248 0   0       my $path = $args{path} // $ENV{HISTFILE} // "$ENV{HOME}/.bash_history";
      0        
249 0 0         my $fh = File::ReadBackwards->new($path) or return [];
250              
251 0   0       my $max_hist_lines = $args{max_hist_lines} // 3000;
252 0   0       my $max_result = $args{max_result} // 100;
253              
254 0           my $word;
255 0           my ($cmd, $opt, $pos);
256 0   0       my $cl = $args{cmdline} // $ENV{COMP_LINE} // '';
      0        
257             my $res = parse_options(
258             cmdline => $cl,
259 0   0       point => $args{point} // $ENV{COMP_POINT} // length($cl),
      0        
260             );
261 0           $cmd = $res->{words}[0];
262 0           $cmd =~ s!.+/!!;
263              
264 0           my $which;
265 0 0         if ($res->{word_type} eq 'opt_val') {
    0          
    0          
266 0           $which = 'opt_val';
267 0           $opt = $res->{words}->[$res->{cword}-1];
268 0           $word = $res->{words}->[$res->{cword}];
269             } elsif ($res->{word_type} eq 'opt_name') {
270 0           $which = 'opt_name';
271 0           $opt = $res->{words}->[ $res->{cword} ];
272 0           $word = $opt;
273             } elsif ($res->{word_type} =~ /\Aarg,(\d+)\z/) {
274 0           $which = 'arg';
275 0           $pos = $1;
276 0           $word = $res->{words}->[$res->{cword}];
277             } else {
278 0           return [];
279             }
280              
281             #use DD; dd {which=>$which, pos=>$pos, word=>$word};
282              
283 0           my %res;
284 0           my $num_hist_lines = 0;
285 0           while (my $line = $fh->readline) {
286 0           chomp($line);
287              
288             # skip timestamp comment
289 0 0         next if $line =~ /^#\d+$/;
290              
291 0 0 0       last if $max_hist_lines >= 0 && $num_hist_lines++ >= $max_hist_lines;
292              
293 0           my ($hwords, $hcword) = @{ parse_cmdline($line, 0) };
  0            
294 0 0         next unless @$hwords;
295              
296             # COMP_LINE (and COMP_WORDS) is provided by bash and does not include
297             # multiple commands (e.g. in '( foo; bar 1 2 )' or 'foo -1 2 | bar
298             # 1 2', bash already only supplies us with 'bash 1 2' instead of
299             # the full command-line. This is different when we try to parse the full
300             # command-line from history. Complete::Bash::parse_cmdline() is not
301             # sophisticated enough to understand full bash syntax. So currently we
302             # don't support multiple/complex statements. We'll need a more
303             # proper/feature-complete bash parser for that.
304              
305             # strip ad-hoc environment setting, e.g.: DEBUG=1 ANOTHER="foo bar" cmd
306 0           while (1) {
307 0 0         if ($hwords->[0] =~ /\A[A-Za-z_][A-Za-z0-9_]*=/) {
308 0           shift @$hwords; $hcword--;
  0            
309 0           next;
310             }
311 0           last;
312             }
313 0 0         next unless @$hwords;
314              
315             # get the first word as command name
316 0           my $hcmd = $hwords->[0];
317 0           $hcmd =~ s!.+/!!;
318             #say "D:hcmd=$hcmd, cmd=$cmd";
319 0 0         next unless $hcmd eq $cmd;
320              
321 0           my $hpo = parse_options(words=>$hwords, cword=>$hcword);
322              
323 0 0         if ($which eq 'opt_name') {
324 0           for (keys %{ $hpo->{opts} }) {
  0            
325 0 0         $res{length($_) > 1 ? "--$_":"-$_"}++;
326             }
327 0           next;
328             }
329              
330 0 0         if ($which eq 'opt_val') {
331 0   0       for (@{ $hpo->{opts} // []}) {
  0            
332 0 0         next unless defined;
333 0           $res{$_}++;
334             }
335 0           next;
336             }
337              
338 0 0         if ($which eq 'arg') {
339 0 0         next unless @{ $hpo->{argv} } > $pos;
  0            
340 0           $res{ $hpo->{argv}[$pos] }++;
341 0           next;
342             }
343              
344 0           die "BUG: invalid which value '$which'";
345             }
346              
347             Complete::Util::complete_array_elem(
348 0   0       array => [keys %res],
349             word => $word // '',
350             );
351             }
352              
353             1;
354             # ABSTRACT: Parse command-line for options and arguments, more or less like Getopt::Long
355              
356             __END__