File Coverage

blib/lib/App/linerange.pm
Criterion Covered Total %
statement 72 72 100.0
branch 45 50 90.0
condition 27 29 93.1
subroutine 6 6 100.0
pod 1 1 100.0
total 151 158 95.5


line stmt bran cond sub pod time code
1             package App::linerange;
2              
3             our $DATE = '2019-03-21'; # DATE
4             our $VERSION = '0.003'; # VERSION
5              
6 1     1   66714 use 5.010001;
  1         10  
7 1     1   5 use strict;
  1         2  
  1         20  
8 1     1   4 use warnings;
  1         2  
  1         39  
9              
10 1     1   6 use List::Util qw(max);
  1         1  
  1         130  
11              
12 1     1   5 use Exporter qw(import);
  1         2  
  1         848  
13             our @EXPORT_OK = qw(linerange);
14              
15             our %SPEC;
16              
17             $SPEC{linerange} = {
18             v => 1.1,
19             summary => 'Retrieve line ranges from a filehandle',
20             description => <<'_',
21              
22             The routine performs a single pass on the filehandle, retrieving specified line
23             ranges.
24              
25              
26             _
27             args => {
28             fh => {
29             schema => 'filehandle*',
30             req => 1,
31             },
32             spec => {
33             summary => 'Line range specification',
34             description => <<'_',
35              
36             A comma-separated list of line numbers ("N") or line ranges ("N1..N2" or
37             "N1-N2", or "N1+M" which means N2 is set to N1+M-1), where N, N1, and N2 are
38             line number specification. Line number begins at 1; it can also be a negative
39             integer (-1 means the last line, -2 means second last, and so on). N1..N2 is the
40             same as N2..N1.
41              
42             Examples:
43              
44             * 3 (third line)
45             * 1..5 (first to fifth line)
46             * 3+0 (third line)
47             * 3+1 (third to fourth line)
48             * -3+1 (third last to fourth last)
49             * 5..1 (first to fifth line)
50             * -5..-1 (fifth last to last line)
51             * -1..-5 (fifth last to last line)
52             * 5..-3 (fifth line to third last)
53             * -3..5 (fifth line to third last)
54              
55             _
56             schema => 'str*',
57             req => 1,
58             pos => 0,
59             },
60             },
61             examples => [
62             {
63             summary => 'Get lines 1-10',
64             args => {spec=>'1-10'},
65             test => 0,
66             'x.doc.show_result' => 0,
67             },
68             {
69             summary => 'Get lines 1 to 10, .. is synonym for -',
70             args => {spec=>'1 .. 10'},
71             test => 0,
72             'x.doc.show_result' => 0,
73             },
74             {
75             summary => 'Get lines 1-10 as well as 21-30',
76             args => {spec=>'1-10, 21 - 30'},
77             test => 0,
78             'x.doc.show_result' => 0,
79             },
80             {
81             summary => 'You can specify negative number, get the 5th line until 2nd last',
82             args => {spec=>'5 .. -2'},
83             test => 0,
84             'x.doc.show_result' => 0,
85             },
86             {
87             summary => 'You can specify negative number, get the 10th last until last',
88             args => {spec=>'-10 .. -1'},
89             test => 0,
90             'x.doc.show_result' => 0,
91             },
92             ],
93             };
94              
95             sub linerange {
96 23     23 1 930 my %args = @_;
97              
98 23   50     67 my $fh = $args{fh} // \*ARGV;
99              
100 23         37 my @ranges;
101             my @buffer;
102 23         35 my $bufsize = 0;
103 23         32 my $exit_after_linum = 0; # set this to a positive line number if we can optimize
104              
105 23         101 for my $spec2 (split /\s*,\s*/, $args{spec}) {
106 29 100       198 $spec2 =~ /\A\s*([+-]?[0-9]+)\s*(?:(\.\.|-|\+)\s*([+-]?[0-9]+)\s*)?\z/
107             or return [400, "Invalid line number/range specification '$spec2'"];
108              
109 28         72 my $ln1 = $1;
110 28   66     81 my $ln2 = $3 // $1;
111 28 100 100     113 if (defined $2 && $2 eq '+') {
112 6         16 $ln2 = $ln1 + $ln2;
113 6 100       13 if ($ln1 > 0) {
114 3 50       8 $ln2 = 1 if $ln2 < 1;
115             } else {
116 3 50       8 $ln2 = -1 if $ln2 > -1;
117             }
118             }
119              
120 28 100 100     159 if ($ln1 == 0 || $ln2 == 0) {
    100 100        
    100 100        
121 2         17 return [400, "Invalid line number 0 in ".
122             "range specification '$spec2'"];
123             } elsif ($ln1 > 0 && $ln2 > 0) {
124 11 100       31 push @ranges, $ln1 > $ln2 ? [$ln2, $ln1] : [$ln1, $ln2];
125 11 50       25 unless ($exit_after_linum < 0) {
126 11 50       23 $exit_after_linum = $ln1 if $exit_after_linum < $ln1;
127 11 100       24 $exit_after_linum = $ln2 if $exit_after_linum < $ln2;
128             }
129             } elsif ($ln1 < 0 && $ln2 < 0) {
130 8 100       21 $bufsize = -$ln1 if $bufsize < -$ln1;
131 8 100       19 $bufsize = -$ln2 if $bufsize < -$ln2;
132 8 100       22 push @ranges, $ln1 > $ln2 ? [$ln1, $ln2] : [$ln2, $ln1];
133 8         20 $exit_after_linum = -1;
134             } else {
135 7         12 $exit_after_linum = -1;
136 7 100       14 if ($ln1 > 0) {
137 6 100       13 $bufsize = -$ln2 if $bufsize < -$ln2;
138 6         19 push @ranges, [$ln1, $ln2];
139             } else {
140 1 50       5 $bufsize = -$ln1 if $bufsize < -$ln1;
141 1         4 push @ranges, [$ln2, $ln1];
142             }
143             }
144             }
145              
146 20         33 my %reslines; # result lines, key = linenum
147 20         28 my $linenum = 0;
148 20         211 while (defined(my $line = <$fh>)) {
149 221         310 $linenum++;
150 221 100 100     409 last if $exit_after_linum >= 0 && $linenum > $exit_after_linum;
151 213 100       329 if ($bufsize) {
152 192         298 push @buffer, $line;
153 192 100       319 if (@buffer > $bufsize) { shift @buffer }
  147         182  
154             }
155 213         345 for my $range (@ranges) {
156             next unless
157 296 100 100     1065 $range->[0] > 0 && $linenum >= $range->[0] &&
      100        
      100        
158             ($range->[1] < 0 ||
159             $range->[1] > 0 && $linenum <= $range->[1]);
160 101         284 $reslines{$linenum} = $line;
161             }
162             }
163              
164 20         45 my $bufstartline = $linenum - @buffer + 1;
165              
166             # remove positive-only ranges
167 20         40 @ranges = grep { $_->[1] < 0 } @ranges;
  26         87  
168              
169             # add/remove result lines that are in the buffer
170 20         50 for my $stage (0..1) {
171 40         61 for my $range (@ranges) {
172 30 100       75 my $bufpos1 = $range->[0] > 0 ?
173             max($range->[0] - $bufstartline, 0) :
174             $linenum + $range->[0] + 1 - $bufstartline;
175 30         44 my $bufpos2 = $linenum + $range->[1] + 1 - $bufstartline;
176 30 100       58 ($bufpos1, $bufpos2) = ($bufpos2, $bufpos1) if $bufpos1 > $bufpos2;
177 30 100       53 if ($stage == 0) {
178 15         36 for my $offset ((0 .. $bufpos1-1), ($bufpos2+1 .. $#buffer)) {
179 27         68 delete $reslines{ $bufstartline + $offset };
180             }
181             } else {
182 15         26 for my $offset ($bufpos1 .. $bufpos2) {
183 29         74 $reslines{ $bufstartline + $offset } = $buffer[$offset];
184             }
185             }
186             }
187             }
188              
189 20         84 [200, "OK", [map {$reslines{$_}} sort {$a <=> $b} keys %reslines]];
  104         282  
  198         275  
190             }
191              
192             1;
193             # ABSTRACT: Retrieve line ranges from a filehandle
194              
195             __END__