File Coverage

blib/lib/App/Greple/Grep.pm
Criterion Covered Total %
statement 364 417 87.2
branch 107 148 72.3
condition 43 67 64.1
subroutine 55 65 84.6
pod 5 22 22.7
total 574 719 79.8


line stmt bran cond sub pod time code
1             package App::Greple::Grep;
2              
3 171     171   2751 use v5.24;
  171         489  
4 171     171   625 use warnings;
  171         209  
  171         7753  
5              
6 171     171   695 use Exporter 'import';
  171         259  
  171         12922  
7             our @EXPORT = qw(FILELABEL);
8             our %EXPORT_TAGS = ();
9             our @EXPORT_OK = qw();
10              
11             our @ISA = qw(App::Greple::Text);
12              
13 171     171   715 use Data::Dumper;
  171         257  
  171         8566  
14 171     171   708 use List::Util qw(min max reduce sum);
  171         246  
  171         10181  
15 171     171   64584 use Clone qw(clone);
  171         72092  
  171         9627  
16 171     171   1053 use POSIX ();
  171         263  
  171         2946  
17              
18 171     171   543 use Getopt::EX::Func qw(callable);
  171         247  
  171         6372  
19              
20 171     171   649 use App::Greple::Common qw(%debug &FILELABEL);
  171         203  
  171         16173  
21 171     171   68783 use App::Greple::Regions;
  171         362  
  171         12765  
22 171     171   69730 use App::Greple::Pattern;
  171         372  
  171         12287  
23              
24             use constant {
25 171         15377 MATCH_NEGATIVE => 0,
26             MATCH_POSITIVE => 1,
27             MATCH_MUST => 2,
28 171     171   997 };
  171         255  
29             push @EXPORT, qw(
30             MATCH_NEGATIVE
31             MATCH_POSITIVE
32             MATCH_MUST
33             );
34              
35             use constant {
36 171         20900 POSI_BASE => 0, POSI_POSI => 0, POSI_NEGA => 1, POSI_LIST => 2,
37             NEGA_BASE => 3, NEGA_POSI => 3, NEGA_NEGA => 4, NEGA_LIST => 5,
38             MUST_BASE => 6, MUST_POSI => 6, MUST_NEGA => 7, MUST_LIST => 8,
39             INDX_POSI => 0, INDX_NEGA => 1, INDX_LIST => 2,
40 171     171   727 };
  171         234  
41              
42             my @match_base;
43             BEGIN {
44 171     171   437 $match_base[MATCH_POSITIVE] = POSI_BASE;
45 171         236 $match_base[MATCH_NEGATIVE] = NEGA_BASE;
46 171         20891 $match_base[MATCH_MUST] = MUST_BASE;
47             }
48              
49             sub category {
50 209     209 0 366 my $obj = shift;
51 209 100       664 return MATCH_MUST if $obj->is_required;
52 197 100       563 return MATCH_NEGATIVE if $obj->is_negative;
53 191         402 return MATCH_POSITIVE;
54             }
55              
56             sub new {
57 161     161 0 1402 my $class = shift;
58 161         3309 my $obj = bless { @_ }, $class;
59 161         856 $obj;
60             }
61              
62             sub run {
63 161     161 0 446 my $self = shift;
64 161         706 $self->prepare->compose;
65             }
66              
67             package App::Greple::Grep::Block {
68 171     171   941 use strict;
  171         258  
  171         18205  
69 223     223   870 sub min :lvalue { $_[0]->[0] }
70 4     4   10 sub max :lvalue { $_[0]->[1] }
71 1267     1267   2904 sub number :lvalue { $_[0]->[2] }
72             }
73              
74             package App::Greple::Grep::Match {
75 171     171   771 use strict;
  171         243  
  171         14307  
76 620     620   1196 sub min :lvalue { $_[0]->[0] }
77 578     578   939 sub max :lvalue { $_[0]->[1] }
78 623     623   3769 sub index :lvalue { $_[0]->[2] }
79 3     3   12 sub callback :lvalue { $_[0]->[3] }
80             }
81              
82             package App::Greple::Grep::Result {
83 171     171   661 use strict;
  171         244  
  171         18686  
84 530     530   1370 sub block { $_[0]->[0] }
85 267     267   468 sub matched { $_[0]->@[ 1 .. $#{$_[0]} ] }
  267         758  
86 0     0   0 sub min { $_[0]->block->min }
87 0     0   0 sub max { $_[0]->block->max }
88 0     0   0 sub number { $_[0]->block->number }
89             }
90              
91             ##
92             ## Match multiple patterns in parallel using child processes.
93             ## Each child scans $_ (shared by copy-on-write) with a single
94             ## pattern and returns [from, to, index] triplets in packed binary
95             ## format. Returns a list indexed by pattern number; undef elements
96             ## mean the pattern was not processed and should be matched in the
97             ## calling process.
98             ##
99 171     171   756 use constant NO_INDEX => ~0;
  171         279  
  171         545137  
100              
101             our $default_threshold = 1024 * 1024;
102              
103             sub parallel_match {
104 161     161 0 251 my $self = shift;
105 161         286 my $patlist = shift;
106 161   100     953 my $max = $self->{parallel} // 0;
107 161 100       886 return () if $max < 2;
108 16   33     59 my $threshold = $ENV{GREPLE_PARALLEL_THRESHOLD} // $default_threshold;
109 16 50       95 return () if length() < $threshold;
110 16         55 my @eligible = grep { not $patlist->[$_]->is_function } keys @$patlist;
  32         77  
111 16 100       48 return () if @eligible < 2;
112             warn sprintf("parallel_match: %d patterns, max %d processes\n",
113 14 50       71 scalar @eligible, $max) if $debug{m};
114 14         25 my @result;
115 14         67 while (my @chunk = splice @eligible, 0, $max) {
116 14         22 my @child;
117 14         101 for my $i (@chunk) {
118 30         123 my $pat = $patlist->[$i];
119 30 50       1762 pipe(my $r, my $w) or last;
120 30         60263 my $pid = fork;
121 30 50       2474 if (not defined $pid) { # fall back to sequential
122 0         0 close $r; close $w;
  0         0  
123 0         0 last;
124             }
125 30 50       1221 if ($pid == 0) {
126 0         0 close $r;
127 0         0 binmode $w;
128             my @p = match_regions(pattern => $pat->regex,
129             group => $self->{group_index},
130 0         0 index => $self->{group_index} >= 2);
131             my $data = pack 'J*',
132 0   0     0 map { ($_->[0], $_->[1], $_->[2] // NO_INDEX) } @p;
  0         0  
133 0 0       0 syswrite $w, $data if length $data;
134 0         0 close $w;
135 0         0 POSIX::_exit(0);
136             }
137 30         1758 close $w;
138 30         312 binmode $r;
139 30         3894 push @child, [ $i, $pid, $r ];
140             }
141 14         954 for (@child) {
142 30         477 my($i, $pid, $r) = @$_;
143 30         1254 my $data = do { local $/; <$r> };
  30         731  
  30         3191  
144 30         2347 close $r;
145 30         28798 waitpid $pid, 0;
146 30 50       713 next if $? != 0; # fall back to sequential
147 30   50     586 my @v = unpack 'J*', $data // '';
148 30         117 my @p;
149 30         231 for (my $j = 0; $j < @v; $j += 3) {
150 27 50       308 push @p, $v[$j+2] == NO_INDEX
151             ? [ @v[$j, $j+1] ] : [ @v[$j .. $j+2] ];
152             }
153 30         370 $result[$i] = \@p;
154             }
155             }
156 14         159 @result;
157             }
158              
159             ##
160             ## Compute line borders in a child process, overlapping with pattern
161             ## matching in the calling process. Border positions are independent
162             ## from the match result and required only after the search.
163             ##
164             sub start_borders {
165 14     14 0 32 my $self = shift;
166 14 50       70 return if $self->{BORDERS_CHILD}; # already started
167 14 50       50 return if $self->{block}->@*; # borders not used for --block
168 14   33     72 my $threshold = $ENV{GREPLE_PARALLEL_THRESHOLD} // $default_threshold;
169 14 50       55 return if length() < $threshold;
170 14 50       499 pipe(my $r, my $w) or return;
171 14         16460 my $pid = fork;
172 14 50       1215 if (not defined $pid) { # fall back to sequential
173 0         0 close $r; close $w;
  0         0  
174 0         0 return;
175             }
176 14 50       556 if ($pid == 0) {
177 0         0 close $r;
178 0         0 binmode $w;
179 0         0 my $data = pack 'J*', match_borders $self->{border};
180 0 0       0 syswrite $w, $data if length $data;
181 0         0 close $w;
182 0         0 POSIX::_exit(0);
183             }
184 14         747 close $w;
185 14         298 binmode $r;
186 14         851 $self->{BORDERS_CHILD} = [ $pid, $r ];
187 14 50       1318 warn "started borders child process\n" if $debug{m};
188             }
189              
190             sub read_borders {
191 13     13 0 49 my $self = shift;
192 13         30 my($pid, $r) = @{delete $self->{BORDERS_CHILD}};
  13         82  
193 13         51 my $data = do { local $/; <$r> };
  13         321  
  13         11965  
194 13         206 close $r;
195 13         12943 waitpid $pid, 0;
196 13 50       207 return if $? != 0; # fall back to sequential
197 13   50     254 unpack 'J*', $data // '';
198             }
199              
200             sub discard_borders {
201 161     161 0 334 my $self = shift;
202 161 100       674 if (my $child = delete $self->{BORDERS_CHILD}) {
203 1         2 my($pid, $r) = @$child;
204 1         42 kill 'TERM', $pid;
205 1         21 close $r;
206 1         856 waitpid $pid, 0;
207             }
208 161         454 $self;
209             }
210              
211             sub prepare {
212 161     161 0 247 my $self = shift;
213              
214 161         5383 local *_ = $self->{text};
215 161         357 my $pat_holder = $self->{pattern};
216 161         244 my @blocks;
217              
218 161         443 $self->{RESULT} = [];
219 161         445 $self->{BLOCKS} = [];
220              
221             ##
222             ## build match result list
223             ##
224 161         275 my @result;
225 161         258 my $positive_count = 0;
226 161         266 my $group_index_offset = 0;
227 161         688 my @patlist = $pat_holder->patterns;
228 161         803 my @parallel = $self->parallel_match(\@patlist);
229 161         1124 while (my($i, $pat) = each @patlist) {
230 224         407 my($func, @args) = do {
231 224 50       1666 if ($pat->is_function) {
232 0         0 $pat->function;
233             } else {
234             Getopt::EX::Func->new(\&match_regions,
235             pattern => $pat->regex,
236             group => $self->{group_index},
237 224         1314 index => $self->{group_index} >= 2,
238             );
239             }
240             };
241             my @p = $parallel[$i]
242 30         90 ? @{$parallel[$i]}
243 224 100       3249 : $func->call(@args, &FILELABEL => $self->{filename});
244 224 100       827 if (@p == 0) {
245             ##
246             ## $self->{need} can be negative value, which means
247             ## required pattern can be compromised upto that number.
248             ##
249 16 50 33     96 return $self->discard_borders if $pat->is_required and $self->{need} >= 0;
250             ##
251             ## Update offset even when no match for --ci=G
252             ##
253 16 100 66     66 if ($self->{group_index} == 2 and not $pat->is_function) {
254 1   33     2 $group_index_offset += $pat->group_count //= do {
255 1         1 "" =~ /@{[$pat->regex]}?/;
  1         2  
256 1 50       5 $#+ || 1;
257             };
258             }
259             } else {
260 208         2136 bless $_, 'App::Greple::Grep::Match' for @p;
261 208 100       1418 if ($pat->is_positive) {
262             ##
263             ## Borders are required only when something matched.
264             ## Do not fork a child process for unmatched files.
265             ##
266 202 100 100     2038 $self->start_borders if $self->{parallel} and not @blocks;
267 202         678 push @blocks, @{clone(\@p)};
  202         5855  
268 202         1220 $self->{stat}->{match_positive} += @p;
269 202         720 $positive_count++;
270             }
271             else {
272 6         15 $self->{stat}->{match_negative} += @p;
273             }
274             ##
275             ## Adjust group index for --ci=G option
276             ## group_index: 0=off, 1=group, 2=sequential, 3=per-pattern
277             ##
278 208 100       2315 if ($self->{group_index} == 2) {
279 4         6 my $max_index = 0;
280 4         5 for (@p) {
281 6 50       43 if (defined $_->index) {
282 6         7 $_->index += $group_index_offset;
283 6 50       9 $max_index = $_->index + 1 if $_->index >= $max_index;
284             }
285             }
286 4         4 $group_index_offset = $max_index;
287             }
288 208   100     3453 $_->index //= $i for @p;
289 208 100       1245 if (my $n = $self->{callback}->@*) {
290 3 50       9 if (my $callback = $self->{callback}->[ $i % $n ]) {
291 3   33     7 $_->callback //= $callback for @p;
292             }
293             }
294             }
295 224         3577 push @result, \@p;
296             }
297 161         439 $self->{stat}->{match_block} += @blocks;
298              
299             ##
300             ## optimization for inadequate match
301             ##
302 161 100       859 return $self->discard_borders if $positive_count < $self->{need} + $self->{must};
303              
304             ##
305             ## --inside, --outside
306             ##
307 152 100       1905 if (my @reg_union = $self->{regions}->union) {
308 9         20 my @tmp = map { [] } @result;
  11         25  
309 9         39 while (my($regi, $reg) = each @reg_union) {
310 9         62 my @select = get_regions($self->{filename}, \$_, $reg->spec);
311 9 100 50     45 @select or next if $reg->is_inside;
312 9         44 while (my($resi, $r) = each @result) {
313             my @l = select_regions({ strict => $self->{strict} },
314 11         60 $r, \@select, $reg->flag);
315 11 100 66     64 if ($self->{region_index} // @result == 1) {
316 7         14 map { $_->[2] = $regi } @l;
  58         97  
317             }
318 11         19 push @{$tmp[$resi]}, @l;
  11         64  
319             }
320             }
321 9         23 @result = map { [ merge_regions { nojoin => 1, destructive => 1 }, @$_ ] } @tmp;
  11         59  
322             }
323              
324             ##
325             ## --include, --exclude
326             ##
327 152         680 for my $reg ($self->{regions}->intersect) {
328 2         6 my @select = get_regions($self->{filename}, \$_, $reg->spec);
329 2 100 50     10 @select or next if not $reg->is_outside;
330 2         5 for my $r (@result) {
331             @$r = select_regions({ strict => $self->{strict} },
332 2         12 $r, \@select, $reg->flag);
333             }
334             }
335              
336             ##
337             ## Setup BLOCKS
338             ##
339 152         315 my $bp = $self->{BLOCKS} = [ do {
340 152 100       939 if ($self->{block}->@*) { # --block
    100          
341 7         38 my $text = \$_;
342             merge_regions { nojoin => 1, destructive => 1 }, map {
343 7         31 get_regions($self->{filename}, $text, $_);
344 7         42 } $self->{block}->@*;
345             }
346             elsif (@blocks) { # from matched range
347             my %opt = ( A => $self->{after},
348             B => $self->{before},
349 140         1043 border => [ $self->borders ] );
350 140         811 my $blocker = smart_blocker(\%opt);
351             merge_regions { nojoin => 1, destructive => 1 }, map {
352 140         825 [ $blocker->(\%opt, $_->min, $_->max) ]
  578         1101  
353             } @blocks;
354             }
355             else {
356 5         23 ( [ 0, length ] ); # nothing matched
357             }
358             } ];
359 152         641 $self->discard_borders; # no-op if consumed by borders()
360 152         696 while (my($i, $blk) = each @$bp) {
361 1267         1985 bless $blk, 'App::Greple::Grep::Block';
362             # set 1-origined block number in the 3rd entry
363 1267         1748 $blk->number = $i + 1;
364             }
365              
366             ##
367             ## build match table
368             ##
369 152         431 my @match_table = map { [ 0, 0, [], 0, 0, [], 0, 0, [] ] } @$bp;
  1267         3049  
370 152         661 while (my($ri, $r) = each @result) {
371 209         636 my $base = $match_base[category($patlist[$ri])];
372 209         1330 my @b = classify_regions({ strict => $self->{strict} }, $r, $bp);
373 209         819 while (my($bi, $b) = each @b) {
374 1370         1375 my $t = $match_table[$bi];
375 1370 100       1631 if (@$b) {
376 377         475 ${$t}[$base + INDX_POSI]++;
  377         551  
377 377         463 push @{$t->[$base + INDX_LIST]}, @$b;
  377         1227  
378             } else {
379 993         837 ${$t}[$base + INDX_NEGA]++;
  993         1783  
380             }
381             }
382             }
383              
384 152 50       504 show_match_table(\@match_table) if $debug{g};
385              
386 152         424 $self->{MATCH_TABLE} = \@match_table;
387              
388 152         1552 $self;
389             }
390              
391             sub compose {
392 161     161 0 363 my $self = shift;
393 161         410 my $bp = $self->{BLOCKS};
394 161         417 my $mp = $self->{MATCH_TABLE};
395              
396             ##
397             ## now it is quite easy to get effective blocks
398             ##
399 161 50       598 my $compromize = $self->{need} < 0 ? abs($self->{need}) : 0;
400             my @effective_index = grep(
401             $mp->[$_][MUST_NEGA] <= $compromize &&
402             $mp->[$_][POSI_POSI] >= $self->{need} &&
403             $mp->[$_][NEGA_POSI] <= $self->{allow},
404 161 100 100     3106 keys @$bp)
405             or return $self;
406              
407             ##
408             ## --matchcount
409             ##
410 129 100       485 if (my $countcheck = $self->{countcheck}) {
411 8 50       10 @effective_index = do {
412 8         14 grep { $countcheck->(int($mp->[$_][POSI_LIST]->@*)) }
  69         156  
413             @effective_index;
414             }
415             or return $self;
416             }
417              
418             ##
419             ## --block with -ABC option
420             ##
421 129 100 66     505 if ($self->{block}->@* and ($self->{after} or $self->{before})) {
      66        
422 3         4 my @mark;
423 3         7 for my $i (@effective_index) {
424 9 100       44 map { $mark[$_] = 1 if $_ >= 0 }
425 3         12 $i - $self->{before} .. $i + $self->{after};
426             }
427 3         20 @effective_index = grep $mark[$_], keys @$bp;
428             }
429              
430             ##
431             ## compose the result
432             ##
433 129         282 my @list = ();
434 129         358 for my $bi (@effective_index) {
435 253         1235 my @matched = merge_regions({ nojoin => 1, destructive => 1 },
436             $mp->[$bi][MUST_LIST]->@*,
437             $mp->[$bi][POSI_LIST]->@*,
438             $mp->[$bi][NEGA_LIST]->@*);
439 253 100       694 if ($self->{stretch}) {
440 1         2 my $b = $bp->[$bi];
441 1         2 my $m = $matched[0];
442 1   50     1 my $i = min map { $_->[2] // 0 } @matched;
  2         14  
443 1         7 @matched = [ $b->min, $b->max, $i, $m->[3] ];
444             }
445 253 100       721 if ($self->{only}) {
    100          
446 29         62 push @list, map({ [ $_, $_ ] } @matched);
  43         105  
447             } elsif ($self->{all}) {
448 6 50       26 push @list, [ [ 0, length ] ] if @list == 0;
449 6         13 push @{$list[0]}, @matched;
  6         16  
450             } else {
451 218         523 push @list, [ $bp->[$bi], @matched ];
452             }
453             }
454 129         293 for my $r (@list) {
455 267         779 bless $r, 'App::Greple::Grep::Result';
456 267         598 bless $r->block, 'App::Greple::Grep::Block';
457 267         577 for my $m ($r->matched) {
458 339         586 bless $m, 'App::Greple::Grep::Match';
459             }
460             }
461              
462             ##
463             ## --join-blocks
464             ##
465 129 100 66     604 if ($self->{join_blocks} and @list > 1) {
466             reduce {
467 1 50   1   2 if ($a->[-1][0]->max == $b->[0]->min) {
468 1         2 $a->[-1][0]->max = $b->[0]->max;
469 1         1 push @{$a->[-1]}, splice @$b, 1;
  1         3  
470             } else {
471 0         0 push @$a, $b;
472             }
473 1         2 $a;
474 1         9 } \@list, splice @list, 1;
475             }
476              
477             ##
478             ## ( [ [blockstart, blockend, number ], [start, end], [start, end], ... ],
479             ## [ [blockstart, blockend, number ], [start, end], [start, end], ... ], ... )
480             ##
481 129         313 $self->{RESULT} = \@list;
482              
483 129         561 $self;
484             }
485              
486             sub borders {
487 140     140 0 320 my $self = shift;
488 140         2676 local $SIG{ALRM};
489 140         327 my $alarm_start;
490 140 50 33     1397 if ($self->{alert_size} and length >= $self->{alert_size}) {
491 0         0 $alarm_start = time;
492             $SIG{ALRM} = sub {
493 0     0   0 $SIG{ALRM} = undef;
494             STDERR->printflush(
495             $self->{filename} .
496 0         0 ": Counting lines, and it may take longer...\n");
497 0         0 };
498 0         0 alarm $self->{alert_time};
499 0 0       0 warn "alert timer start ($alarm_start)\n" if $debug{a};
500             }
501 140         248 my @borders = do {
502 140 100       470 if ($self->{BORDERS_CHILD}) {
503 13         90 my @b = $self->read_borders;
504 13 50       140 @b ? @b : match_borders $self->{border};
505             } else {
506 127         608 match_borders $self->{border};
507             }
508             };
509 140 50       533 if (defined $alarm_start) {
510 0 0       0 if ($SIG{ALRM}) {
511 0         0 alarm 0;
512 0 0       0 warn "reset alert timer\n" if $debug{a};
513             } else {
514 0         0 STDERR->printflush(sprintf("Count %d lines in %d seconds.\n",
515             @borders - 1,
516             time - $alarm_start));
517             }
518             }
519 140         2043 @borders;
520             }
521              
522             sub result_ref {
523 160     160 1 259 my $obj = shift;
524 160         727 $obj->{RESULT};
525             }
526              
527             sub result {
528 417     417 1 571 my $obj = shift;
529 417         503 @{ $obj->{RESULT} };
  417         1198  
530             }
531              
532             sub matched {
533 161     161 1 302 my $obj = shift;
534 161   100     723 sum(map { $_->@* - 1 } $obj->result) // 0;
  265         1229  
535             }
536              
537             sub blocks {
538 0     0 1 0 my $obj = shift;
539 0         0 $obj->{BLOCKS}->@*;
540             }
541              
542             ##
543             ## Cut all result blocks (and gaps between them) from the text in a
544             ## single pass. Returns ( gap0, block0, gap1, block1, ..., rest ),
545             ## or an empty list when not applicable. Character based substr on
546             ## a large utf8 string requires linear scan for every call to convert
547             ## character offset to byte offset, so cutting each block separately
548             ## makes the output routine quadratic.
549             ##
550             sub slice_blocks {
551 127     127 0 209 my $self = shift;
552 127         446 my @blocks = map { $_->block } $self->result;
  263         597  
553 127 100       621 return () if @blocks < 2;
554 44         70 my $pos = 0;
555 44         110 for (@blocks) {
556 180 50       323 return () if $_->[0] < $pos; # not in order
557 180         222 $pos = $_->[1];
558             }
559 44         168 my $template = unpack_template(\@blocks, 0);
560 44         1366 unpack $template, ${ $self->{text} };
  44         475  
561             }
562              
563             sub slice_result {
564 263     263 1 407 my $obj = shift;
565 263         2132 my $result = shift;
566 263         493 my($block, @list) = @$result;
567 263 100       815 my $text = @_ ? shift : $obj->cut(@$block);
568 263         708 my $template = unpack_template(\@list, $block->min);
569 263         1246 unpack($template, $text);
570             }
571              
572             sub slice_index {
573 0     0 0 0 my $obj = shift;
574 0         0 my $result = shift;
575 0         0 my($block, @list) = @$result;
576 0         0 map { $_ * 2 + 1 } keys @list;
  0         0  
577             }
578              
579             sub unpack_template {
580             ##
581             ## make template to split result text into matched and unmatched parts
582             ##
583 307     307 0 548 my($matched, $offset) = @_;
584 307         409 my @len;
585 307         595 for (@$matched) {
586 516         717 my($s, $e) = @$_;
587 516 100       874 $s = $offset if $s < $offset;
588 516         845 push @len, $s - $offset, $e - $s;
589 516         695 $offset = $e;
590             }
591 307         2151 join '', map "a$_", @len, '*';
592             }
593              
594             sub show_match_table {
595 0     0 0 0 my $table = shift;
596 0         0 local $Data::Dumper::Terse = 1;
597 0         0 while (my($i, $e) = each @$table) {
598 0         0 printf STDERR
599             "%4d %s", $i++, Dumper($e) =~ s/\s+(?!$)/ /gsr;
600             }
601             }
602              
603             sub get_regions {
604 18     18 0 42 my $file = shift;
605 18         43 local *_ = shift;
606 18         33 my $pattern = shift;
607              
608             ## func object
609 18 50       83 if (callable $pattern) {
610 0         0 $pattern->call(&FILELABEL => $file);
611             }
612             ## pattern
613             else {
614 18         136 match_regions(pattern => $pattern);
615             }
616             }
617              
618             sub smart_blocker {
619 140     140 0 289 my $opt = shift;
620 140 100 100     977 return \&blocker if $opt->{A} or $opt->{B};
621 134         286 my $from = my $to = -1;
622             sub {
623 570 100 100 570   1889 if ($from <= $_[1] and $_[2] < $to) {
624 203         511 return($from, $to);
625             }
626 367         796 ($from, $to) = &blocker;
627             }
628 134         883 }
629              
630 171     171   79604 use List::BinarySearch qw(binsearch_pos);
  171         311160  
  171         35677  
631              
632             sub blocker {
633 375     375 0 730 my($opt, $from, $to) = @_;
634 375         590 my $border = $opt->{border};
635              
636 375     1792   2223 my $bi = binsearch_pos { $a <=> $b } $from, @$border;
  1792         2064  
637 375 100       1277 $bi-- if $border->[$bi] != $from;
638 375 100       846 $bi = max 0, $bi - $opt->{B} if $opt->{B};
639              
640 375     1791   1262 my $ei = binsearch_pos { $a <=> $b } $to, @$border;
  1791         1900  
641 375 100 100     1092 $ei++ if $ei == $bi and $ei < $#{$border};
  58         95  
642 375 100       771 $ei = min $#{$border}, $ei + $opt->{A} if $opt->{A};
  7         40  
643              
644 375         1788 @$border[ $bi, $ei ];
645             }
646              
647             package App::Greple::Text {
648 171     171   1104 use strict;
  171         253  
  171         3013  
649 171     171   527 use warnings;
  171         574  
  171         12733  
650 171     171   918 use overload '""' => sub { ${ $_[0]->{text} } };
  171     0   460  
  171         2367  
  0         0  
  0         0  
651             sub new {
652 0     0   0 my $class = shift;
653 0         0 bless { text => \$_[0] }, $class;
654             }
655 0     0   0 sub text { ${ $_[0]->{text} } }
  0         0  
656             sub cut {
657 171     171   354 my($obj, $from, $to) = @_;
658 171         198 substr ${ $obj->{text} }, $from, $to - $from;
  171         829  
659             }
660             }
661              
662             1;
663              
664             __END__