File Coverage

blib/lib/App/Greple/Grep.pm
Criterion Covered Total %
statement 272 305 89.1
branch 73 94 77.6
condition 33 50 66.0
subroutine 48 58 82.7
pod 5 17 29.4
total 431 524 82.2


line stmt bran cond sub pod time code
1             package App::Greple::Grep;
2              
3 134     134   2812 use v5.24;
  134         516  
4 134     134   677 use warnings;
  134         256  
  134         7905  
5              
6 134     134   854 use Exporter 'import';
  134         239  
  134         13956  
7             our @EXPORT = qw(FILELABEL);
8             our %EXPORT_TAGS = ();
9             our @EXPORT_OK = qw();
10              
11             our @ISA = qw(App::Greple::Text);
12              
13 134     134   916 use Data::Dumper;
  134         271  
  134         9215  
14 134     134   895 use List::Util qw(min max reduce sum);
  134         271  
  134         11963  
15 134     134   71809 use Clone qw(clone);
  134         73741  
  134         10727  
16              
17 134     134   1014 use Getopt::EX::Func qw(callable);
  134         320  
  134         7017  
18              
19 134     134   799 use App::Greple::Common qw(%debug &FILELABEL);
  134         248  
  134         18970  
20 134     134   70485 use App::Greple::Regions;
  134         428  
  134         13433  
21 134     134   65761 use App::Greple::Pattern;
  134         414  
  134         12976  
22              
23             use constant {
24 134         16222 MATCH_NEGATIVE => 0,
25             MATCH_POSITIVE => 1,
26             MATCH_MUST => 2,
27 134     134   970 };
  134         240  
28             push @EXPORT, qw(
29             MATCH_NEGATIVE
30             MATCH_POSITIVE
31             MATCH_MUST
32             );
33              
34             use constant {
35 134         25391 POSI_BASE => 0, POSI_POSI => 0, POSI_NEGA => 1, POSI_LIST => 2,
36             NEGA_BASE => 3, NEGA_POSI => 3, NEGA_NEGA => 4, NEGA_LIST => 5,
37             MUST_BASE => 6, MUST_POSI => 6, MUST_NEGA => 7, MUST_LIST => 8,
38             INDX_POSI => 0, INDX_NEGA => 1, INDX_LIST => 2,
39 134     134   832 };
  134         247  
40              
41             my @match_base;
42             BEGIN {
43 134     134   630 $match_base[MATCH_POSITIVE] = POSI_BASE;
44 134         259 $match_base[MATCH_NEGATIVE] = NEGA_BASE;
45 134         23952 $match_base[MATCH_MUST] = MUST_BASE;
46             }
47              
48             sub category {
49 150     150 0 376 my $obj = shift;
50 150 100       725 return MATCH_MUST if $obj->is_required;
51 140 100       686 return MATCH_NEGATIVE if $obj->is_negative;
52 136         391 return MATCH_POSITIVE;
53             }
54              
55             sub new {
56 124     124 0 386 my $class = shift;
57 124         3838 my $obj = bless { @_ }, $class;
58 124         959 $obj;
59             }
60              
61             sub run {
62 124     124 0 1056 my $self = shift;
63 124         1615 $self->prepare->compose;
64             }
65              
66             package App::Greple::Grep::Block {
67 134     134   1076 use strict;
  134         286  
  134         18369  
68 208     208   849 sub min :lvalue { $_[0]->[0] }
69 4     4   14 sub max :lvalue { $_[0]->[1] }
70 1213     1213   4580 sub number :lvalue { $_[0]->[2] }
71             }
72              
73             package App::Greple::Grep::Match {
74 134     134   1067 use strict;
  134         345  
  134         18664  
75 557     557   1451 sub min :lvalue { $_[0]->[0] }
76 518     518   1104 sub max :lvalue { $_[0]->[1] }
77 557     557   3119 sub index :lvalue { $_[0]->[2] }
78 3     3   16 sub callback :lvalue { $_[0]->[3] }
79             }
80              
81             package App::Greple::Grep::Result {
82 134     134   926 use strict;
  134         259  
  134         446097  
83 249     249   1068 sub block { $_[0]->[0] }
84 249     249   494 sub matched { $_[0]->@[ 1 .. $#{$_[0]} ] }
  249         902  
85 0     0   0 sub min { $_[0]->block->min }
86 0     0   0 sub max { $_[0]->block->max }
87 0     0   0 sub number { $_[0]->block->number }
88             }
89              
90             sub prepare {
91 124     124 0 898 my $self = shift;
92              
93 124         6033 local *_ = $self->{text};
94 124         1530 my $pat_holder = $self->{pattern};
95 124         1240 my @blocks;
96              
97 124         869 $self->{RESULT} = [];
98 124         709 $self->{BLOCKS} = [];
99              
100             ##
101             ## build match result list
102             ##
103 124         248 my @result;
104 124         259 my $positive_count = 0;
105 124         256 my $group_index_offset = 0;
106 124         889 my @patlist = $pat_holder->patterns;
107 124         845 while (my($i, $pat) = each @patlist) {
108 153         310 my($func, @args) = do {
109 153 50       698 if ($pat->is_function) {
110 0         0 $pat->function;
111             } else {
112             Getopt::EX::Func->new(\&match_regions,
113             pattern => $pat->regex,
114             group => $self->{group_index},
115 153         758 index => $self->{group_index} >= 2,
116             );
117             }
118             };
119 153         2525 my @p = $func->call(@args, &FILELABEL => $self->{filename});
120 153 100       768 if (@p == 0) {
121             ##
122             ## $self->{need} can be negative value, which means
123             ## required pattern can be compromised upto that number.
124             ##
125 6 50 33     20 return $self if $pat->is_required and $self->{need} >= 0;
126             ##
127             ## Update offset even when no match for --ci=G
128             ##
129 6 100 66     36 if ($self->{group_index} == 2 and not $pat->is_function) {
130 1   33     3 $group_index_offset += $pat->group_count //= do {
131 1         2 "" =~ /@{[$pat->regex]}?/;
  1         1  
132 1 50       5 $#+ || 1;
133             };
134             }
135             } else {
136 147         1586 bless $_, 'App::Greple::Grep::Match' for @p;
137 147 100       1602 if ($pat->is_positive) {
138 143         597 push @blocks, @{clone(\@p)};
  143         5118  
139 143         1110 $self->{stat}->{match_positive} += @p;
140 143         362 $positive_count++;
141             }
142             else {
143 4         11 $self->{stat}->{match_negative} += @p;
144             }
145             ##
146             ## Adjust group index for --ci=G option
147             ## group_index: 0=off, 1=group, 2=sequential, 3=per-pattern
148             ##
149 147 100       1282 if ($self->{group_index} == 2) {
150 4         5 my $max_index = 0;
151 4         6 for (@p) {
152 6 50       17 if (defined $_->index) {
153 6         9 $_->index += $group_index_offset;
154 6 50       65 $max_index = $_->index + 1 if $_->index >= $max_index;
155             }
156             }
157 4         6 $group_index_offset = $max_index;
158             }
159 147   100     1427 $_->index //= $i for @p;
160 147 100       1168 if (my $n = $self->{callback}->@*) {
161 3 50       21 if (my $callback = $self->{callback}->[ $i % $n ]) {
162 3   33     68 $_->callback //= $callback for @p;
163             }
164             }
165             }
166 153         1866 push @result, \@p;
167             }
168 124         481 $self->{stat}->{match_block} += @blocks;
169              
170             ##
171             ## optimization for inadequate match
172             ##
173 124 100       1131 return $self if $positive_count < $self->{need} + $self->{must};
174              
175             ##
176             ## --inside, --outside
177             ##
178 121 100       1259 if (my @reg_union = $self->{regions}->union) {
179 7         19 my @tmp = map { [] } @result;
  7         26  
180 7         36 while (my($regi, $reg) = each @reg_union) {
181 7         40 my @select = get_regions($self->{filename}, \$_, $reg->spec);
182 7 100 50     39 @select or next if $reg->is_inside;
183 7         36 while (my($resi, $r) = each @result) {
184             my @l = select_regions({ strict => $self->{strict} },
185 7         39 $r, \@select, $reg->flag);
186 7 50 33     92 if ($self->{region_index} // @result == 1) {
187 7         17 map { $_->[2] = $regi } @l;
  58         137  
188             }
189 7         18 push @{$tmp[$resi]}, @l;
  7         94  
190             }
191             }
192 7         20 @result = map { [ merge_regions { nojoin => 1, destructive => 1 }, @$_ ] } @tmp;
  7         51  
193             }
194              
195             ##
196             ## --include, --exclude
197             ##
198 121         1050 for my $reg ($self->{regions}->intersect) {
199 2         17 my @select = get_regions($self->{filename}, \$_, $reg->spec);
200 2 100 50     10 @select or next if not $reg->is_outside;
201 2         5 for my $r (@result) {
202             @$r = select_regions({ strict => $self->{strict} },
203 2         9 $r, \@select, $reg->flag);
204             }
205             }
206              
207             ##
208             ## Setup BLOCKS
209             ##
210 121         559 my $bp = $self->{BLOCKS} = [ do {
211 121 100       2134 if ($self->{block}->@*) { # --block
    100          
212 7         21 my $text = \$_;
213             merge_regions { nojoin => 1, destructive => 1 }, map {
214 7         28 get_regions($self->{filename}, $text, $_);
215 7         40 } $self->{block}->@*;
216             }
217             elsif (@blocks) { # from matched range
218             my %opt = ( A => $self->{after},
219             B => $self->{before},
220 109         901 border => [ $self->borders ] );
221 109         716 my $blocker = smart_blocker(\%opt);
222             merge_regions { nojoin => 1, destructive => 1 }, map {
223 109         656 [ $blocker->(\%opt, $_->min, $_->max) ]
  518         1323  
224             } @blocks;
225             }
226             else {
227 5         30 ( [ 0, length ] ); # nothing matched
228             }
229             } ];
230 121         866 while (my($i, $blk) = each @$bp) {
231 1213         2727 bless $blk, 'App::Greple::Grep::Block';
232             # set 1-origined block number in the 3rd entry
233 1213         2720 $blk->number = $i + 1;
234             }
235              
236             ##
237             ## build match table
238             ##
239 121         604 my @match_table = map { [ 0, 0, [], 0, 0, [], 0, 0, [] ] } @$bp;
  1213         4329  
240 121         647 while (my($ri, $r) = each @result) {
241 150         708 my $base = $match_base[category($patlist[$ri])];
242 150         1136 my @b = classify_regions({ strict => $self->{strict} }, $r, $bp);
243 150         833 while (my($bi, $b) = each @b) {
244 1265         2080 my $t = $match_table[$bi];
245 1265 100       2557 if (@$b) {
246 319         522 ${$t}[$base + INDX_POSI]++;
  319         602  
247 319         521 push @{$t->[$base + INDX_LIST]}, @$b;
  319         1491  
248             } else {
249 946         1457 ${$t}[$base + INDX_NEGA]++;
  946         2995  
250             }
251             }
252             }
253              
254 121 50       486 show_match_table(\@match_table) if $debug{g};
255              
256 121         411 $self->{MATCH_TABLE} = \@match_table;
257              
258 121         1796 $self;
259             }
260              
261             sub compose {
262 124     124 0 320 my $self = shift;
263 124         317 my $bp = $self->{BLOCKS};
264 124         339 my $mp = $self->{MATCH_TABLE};
265              
266             ##
267             ## now it is quite easy to get effective blocks
268             ##
269 124 50       572 my $compromize = $self->{need} < 0 ? abs($self->{need}) : 0;
270             my @effective_index = grep(
271             $mp->[$_][MUST_NEGA] <= $compromize &&
272             $mp->[$_][POSI_POSI] >= $self->{need} &&
273             $mp->[$_][NEGA_POSI] <= $self->{allow},
274 124 100 100     4220 keys @$bp)
275             or return $self;
276              
277             ##
278             ## --matchcount
279             ##
280 114 100       670 if (my $countcheck = $self->{countcheck}) {
281 8 50       15 @effective_index = do {
282 8         21 grep { $countcheck->(int($mp->[$_][POSI_LIST]->@*)) }
  69         159  
283             @effective_index;
284             }
285             or return $self;
286             }
287              
288             ##
289             ## --block with -ABC option
290             ##
291 114 100 66     582 if ($self->{block}->@* and ($self->{after} or $self->{before})) {
      66        
292 3         6 my @mark;
293 3         9 for my $i (@effective_index) {
294 9 100       31 map { $mark[$_] = 1 if $_ >= 0 }
295 3         14 $i - $self->{before} .. $i + $self->{after};
296             }
297 3         24 @effective_index = grep $mark[$_], keys @$bp;
298             }
299              
300             ##
301             ## compose the result
302             ##
303 114         383 my @list = ();
304 114         395 for my $bi (@effective_index) {
305 236         1493 my @matched = merge_regions({ nojoin => 1, destructive => 1 },
306             $mp->[$bi][MUST_LIST]->@*,
307             $mp->[$bi][POSI_LIST]->@*,
308             $mp->[$bi][NEGA_LIST]->@*);
309 236 100       844 if ($self->{stretch}) {
310 1         4 my $b = $bp->[$bi];
311 1         4 my $m = $matched[0];
312 1   50     2 my $i = min map { $_->[2] // 0 } @matched;
  2         11  
313 1         17 @matched = [ $b->min, $b->max, $i, $m->[3] ];
314             }
315 236 100       995 if ($self->{only}) {
    100          
316 27         70 push @list, map({ [ $_, $_ ] } @matched);
  40         182  
317             } elsif ($self->{all}) {
318 6 50       47 push @list, [ [ 0, length ] ] if @list == 0;
319 6         8 push @{$list[0]}, @matched;
  6         19  
320             } else {
321 203         649 push @list, [ $bp->[$bi], @matched ];
322             }
323             }
324 114         354 for my $r (@list) {
325 249         1018 bless $r, 'App::Greple::Grep::Result';
326 249         772 bless $r->block, 'App::Greple::Grep::Block';
327 249         728 for my $m ($r->matched) {
328 317         693 bless $m, 'App::Greple::Grep::Match';
329             }
330             }
331              
332             ##
333             ## --join-blocks
334             ##
335 114 100 66     879 if ($self->{join_blocks} and @list > 1) {
336             reduce {
337 1 50   1   5 if ($a->[-1][0]->max == $b->[0]->min) {
338 1         4 $a->[-1][0]->max = $b->[0]->max;
339 1         3 push @{$a->[-1]}, splice @$b, 1;
  1         4  
340             } else {
341 0         0 push @$a, $b;
342             }
343 1         10 $a;
344 1         13 } \@list, splice @list, 1;
345             }
346              
347             ##
348             ## ( [ [blockstart, blockend, number ], [start, end], [start, end], ... ],
349             ## [ [blockstart, blockend, number ], [start, end], [start, end], ... ], ... )
350             ##
351 114         475 $self->{RESULT} = \@list;
352              
353 114         632 $self;
354             }
355              
356             sub borders {
357 109     109 0 345 my $self = shift;
358 109         2507 local $SIG{ALRM};
359 109         363 my $alarm_start;
360 109 50 33     1192 if ($self->{alert_size} and length >= $self->{alert_size}) {
361 0         0 $alarm_start = time;
362             $SIG{ALRM} = sub {
363 0     0   0 $SIG{ALRM} = undef;
364             STDERR->printflush(
365             $self->{filename} .
366 0         0 ": Counting lines, and it may take longer...\n");
367 0         0 };
368 0         0 alarm $self->{alert_time};
369 0 0       0 warn "alert timer start ($alarm_start)\n" if $debug{a};
370             }
371 109         725 my @borders = match_borders $self->{border};
372 109 50       527 if (defined $alarm_start) {
373 0 0       0 if ($SIG{ALRM}) {
374 0         0 alarm 0;
375 0 0       0 warn "reset alert timer\n" if $debug{a};
376             } else {
377 0         0 STDERR->printflush(sprintf("Count %d lines in %d seconds.\n",
378             @borders - 1,
379             time - $alarm_start));
380             }
381             }
382 109         1976 @borders;
383             }
384              
385             sub result_ref {
386 125     125 1 292 my $obj = shift;
387 125         612 $obj->{RESULT};
388             }
389              
390             sub result {
391 236     236 1 467 my $obj = shift;
392 236         475 @{ $obj->{RESULT} };
  236         1117  
393             }
394              
395             sub matched {
396 124     124 1 337 my $obj = shift;
397 124   100     606 sum(map { $_->@* - 1 } $obj->result) // 0;
  247         1496  
398             }
399              
400             sub blocks {
401 0     0 1 0 my $obj = shift;
402 0         0 $obj->{BLOCKS}->@*;
403             }
404              
405             sub slice_result {
406 245     245 1 524 my $obj = shift;
407 245         479 my $result = shift;
408 245         644 my($block, @list) = @$result;
409 245         948 my $template = unpack_template(\@list, $block->min);
410 245         960 unpack($template, $obj->cut(@$block));
411             }
412              
413             sub slice_index {
414 0     0 0 0 my $obj = shift;
415 0         0 my $result = shift;
416 0         0 my($block, @list) = @$result;
417 0         0 map { $_ * 2 + 1 } keys @list;
  0         0  
418             }
419              
420             sub unpack_template {
421             ##
422             ## make template to split result text into matched and unmatched parts
423             ##
424 245     245 0 565 my($matched, $offset) = @_;
425 245         432 my @len;
426 245         590 for (@$matched) {
427 314         610 my($s, $e) = @$_;
428 314 100       831 $s = $offset if $s < $offset;
429 314         767 push @len, $s - $offset, $e - $s;
430 314         595 $offset = $e;
431             }
432 245         2131 join '', map "a$_", @len, '*';
433             }
434              
435             sub show_match_table {
436 0     0 0 0 my $table = shift;
437 0         0 local $Data::Dumper::Terse = 1;
438 0         0 while (my($i, $e) = each @$table) {
439 0         0 printf STDERR
440             "%4d %s", $i++, Dumper($e) =~ s/\s+(?!$)/ /gsr;
441             }
442             }
443              
444             sub get_regions {
445 16     16 0 40 my $file = shift;
446 16         39 local *_ = shift;
447 16         36 my $pattern = shift;
448              
449             ## func object
450 16 50       59 if (callable $pattern) {
451 0         0 $pattern->call(&FILELABEL => $file);
452             }
453             ## pattern
454             else {
455 16         130 match_regions(pattern => $pattern);
456             }
457             }
458              
459             sub smart_blocker {
460 109     109 0 304 my $opt = shift;
461 109 100 100     959 return \&blocker if $opt->{A} or $opt->{B};
462 106         304 my $from = my $to = -1;
463             sub {
464 515 100 100 515   2116 if ($from <= $_[1] and $_[2] < $to) {
465 200         663 return($from, $to);
466             }
467 315         888 ($from, $to) = &blocker;
468             }
469 106         694 }
470              
471 134     134   82079 use List::BinarySearch qw(binsearch_pos);
  134         344640  
  134         38224  
472              
473             sub blocker {
474 318     318 0 742 my($opt, $from, $to) = @_;
475 318         619 my $border = $opt->{border};
476              
477 318     1515   2457 my $bi = binsearch_pos { $a <=> $b } $from, @$border;
  1515         2300  
478 318 100       1378 $bi-- if $border->[$bi] != $from;
479 318 100       902 $bi = max 0, $bi - $opt->{B} if $opt->{B};
480              
481 318     1514   1416 my $ei = binsearch_pos { $a <=> $b } $to, @$border;
  1514         2063  
482 318 100 100     1279 $ei++ if $ei == $bi and $ei < $#{$border};
  58         102  
483 318 100       900 $ei = min $#{$border}, $ei + $opt->{A} if $opt->{A};
  2         40  
484              
485 318         1837 @$border[ $bi, $ei ];
486             }
487              
488             package App::Greple::Text {
489 134     134   1181 use strict;
  134         256  
  134         3175  
490 134     134   603 use warnings;
  134         245  
  134         11884  
491 134     134   953 use overload '""' => sub { ${ $_[0]->{text} } };
  134     0   319  
  134         1770  
  0         0  
  0         0  
492             sub new {
493 0     0   0 my $class = shift;
494 0         0 bless { text => \$_[0] }, $class;
495             }
496 0     0   0 sub text { ${ $_[0]->{text} } }
  0         0  
497             sub cut {
498 494     494   1050 my($obj, $from, $to) = @_;
499 494         687 substr ${ $obj->{text} }, $from, $to - $from;
  494         3871  
500             }
501             }
502              
503             1;
504              
505             __END__