File Coverage

blib/lib/Complete/Bash/History.pm
Criterion Covered Total %
statement 8 70 11.4
branch 0 32 0.0
condition 0 25 0.0
subroutine 3 4 75.0
pod 1 1 100.0
total 12 132 9.0


line stmt bran cond sub pod time code
1             package Complete::Bash::History;
2              
3             our $DATE = '2015-09-09'; # DATE
4             our $VERSION = '0.04'; # VERSION
5              
6 1     1   645 use 5.010001;
  1         3  
7 1     1   4 use strict;
  1         2  
  1         17  
8 1     1   4 use warnings;
  1         1  
  1         831  
9              
10             #use Complete;
11              
12             require Exporter;
13             our @ISA = qw(Exporter);
14             our @EXPORT_OK = qw(
15             complete_cmdline_from_hist
16             );
17              
18             our %SPEC;
19              
20             $SPEC{':package'} = {
21             v => 1.1,
22             links => [
23             {url => 'pm:Complete'},
24             ],
25             };
26              
27             $SPEC{complete_cmdline_from_hist} = {
28             v => 1.1,
29             summary => 'Complete command line from recent entries in bash history',
30             description => <<'_',
31              
32             This routine will search your bash history file (recent first a.k.a. backward)
33             for entries for the same command, and complete option with the same name or
34             argument in the same position. For example, if you have history like this:
35              
36             cmd1 --opt1 val arg1 arg2
37             cmd1 --opt1 valb arg1b arg2b arg3b
38             cmd2 --foo
39              
40             Then if you do:
41              
42             complete_cmdline_from_hist(comp_line=>'cmd1 --bar --opt1 ', comp_point=>18);
43              
44             then it means the routine will search for values for option `--opt1` and will
45             return:
46              
47             ["val", "valb"]
48              
49             Or if you do:
50              
51             complete_cmdline_from_hist(comp_line=>'cmd1 baz ', comp_point=>9);
52              
53             then it means the routine will search for second argument (argv[1]) and will
54             return:
55              
56             ["arg2", "arg2b"]
57              
58             _
59             args => {
60             path => {
61             summary => 'Path to `.bash_history` file',
62             schema => 'str*',
63             description => <<'_',
64              
65             Defaults to `~/.bash_history`.
66              
67             If file does not exist or unreadable, will return empty completion answer.
68              
69             _
70             },
71             max_hist_lines => {
72             summary => 'Stop searching after this amount of history lines',
73             schema => ['int*'],
74             default => 3000,
75             description => <<'_',
76              
77             -1 means unlimited (search all lines in the file).
78              
79             Timestamp comments are not counted.
80              
81             _
82             },
83             max_result => {
84             summary => 'Stop after finding this number of distinct results',
85             schema => 'int*',
86             default => 100,
87             description => <<'_',
88              
89             -1 means unlimited.
90              
91             _
92             },
93             cmdline => {
94             summary => 'Command line, defaults to COMP_LINE',
95             schema => 'str*',
96             },
97             point => {
98             summary => 'Command line, defaults to COMP_POINT',
99             schema => 'int*',
100             },
101             },
102             result_naked=>1,
103             };
104             sub complete_cmdline_from_hist {
105 0     0 1   require Complete::Bash;
106 0           require Complete::Util;
107 0           require File::ReadBackwards;
108              
109 0           my %args = @_;
110              
111 0   0       my $path = $args{path} // "$ENV{HOME}/.bash_history";
112 0 0         my $fh = File::ReadBackwards->new($path) or return [];
113              
114 0   0       my $max_hist_lines = $args{max_hist_lines} // 3000;
115 0   0       my $max_result = $args{max_result} // 100;
116              
117 0           my $word;
118 0           my ($cmd, $opt, $pos);
119 0   0       my $cl = $args{cmdline} // $ENV{COMP_LINE} // '';
      0        
120             my $res = Complete::Bash::parse_options(
121             cmdline => $cl,
122 0   0       point => $args{point} // $ENV{COMP_POINT} // length($cl),
      0        
123             );
124 0           $cmd = $res->{words}[0];
125 0           my $which;
126 0 0         if ($res->{word_type} eq 'opt_val') {
    0          
    0          
127 0           $which = 'opt_val';
128 0           $opt = $res->{words}->[$res->{cword}-1];
129 0           $word = $res->{words}->[$res->{cword}];
130             } elsif ($res->{word_type} eq 'opt_name') {
131 0           $which = 'opt_name';
132 0           $opt = $res->{words}->[ $res->{cword} ];
133 0           $word = $opt;
134             } elsif ($res->{word_type} =~ /\Aarg,(\d+)\z/) {
135 0           $which = 'arg';
136 0           $pos = $1;
137 0           $word = $res->{words}->[$res->{cword}];
138             } else {
139 0           return [];
140             }
141              
142 0           my %res;
143 0           my $num_hist_lines = 0;
144 0           while (my $line = $fh->readline) {
145 0           chomp($line);
146              
147             # skip timestamp comment
148 0 0         next if $line =~ /^#\d+$/;
149              
150 0 0 0       last if $max_hist_lines >= 0 && $num_hist_lines++ >= $max_hist_lines;
151              
152 0           my ($hwords, $hcword) = @{ Complete::Bash::parse_cmdline($line, 0) };
  0            
153 0 0         next unless @$hwords;
154              
155             # COMP_LINE (and COMP_WORDS) is provided by bash and does not include
156             # multiple commands (e.g. in '( foo; bar 1 2 )' or 'foo -1 2 | bar
157             # 1 2', bash already only supplies us with 'bash 1 2' instead of
158             # the full command-line. This is different when we try to parse the full
159             # command-line from history. Complete::Bash::parse_cmdline() is not
160             # sophisticated enough to understand full bash syntax. So currently we
161             # don't support multiple/complex statements. We'll need a more
162             # proper/feature-complete bash parser for that.
163              
164             # strip ad-hoc environment setting, e.g.: DEBUG=1 ANOTHER="foo bar" cmd
165 0           while (1) {
166 0 0         if ($hwords->[0] =~ /\A[A-Za-z_][A-Za-z0-9_]*=/) {
167 0           shift @$hwords; $hcword--;
  0            
168 0           next;
169             }
170 0           last;
171             }
172 0 0         next unless @$hwords;
173              
174             # get the first word as command name
175 0           my $hcmd = $hwords->[0];
176 0 0         next unless $hcmd eq $cmd;
177              
178 0           my $hpo = Complete::Bash::parse_options(words=>$hwords, cword=>$hcword);
179              
180 0 0         if ($which eq 'opt_name') {
181 0           for (keys %{ $hpo->{opts} }) {
  0            
182 0 0         $res{length($_) > 1 ? "--$_":"-$_"}++;
183             }
184 0           next;
185             }
186              
187 0 0         if ($which eq 'opt_val') {
188 0   0       for (@{ $hpo->{opts} // []}) {
  0            
189 0 0         next unless defined;
190 0           $res{$_}++;
191             }
192 0           next;
193             }
194              
195 0 0         if ($which eq 'arg') {
196 0 0         next unless @{ $hpo->{argv} } > $pos;
  0            
197 0           $res{ $hpo->{argv}[$pos] }++;
198 0           next;
199             }
200              
201 0           die "BUG: invalid which value '$which'";
202             }
203              
204             Complete::Util::complete_array_elem(
205 0   0       array => [keys %res],
206             word => $word // '',
207             );
208             }
209              
210             1;
211             # ABSTRACT: Complete command line from recent entries in bash history
212              
213             __END__