File Coverage

bin/nl
Criterion Covered Total %
statement 105 136 77.2
branch 37 68 54.4
condition 4 11 36.3
subroutine 11 12 91.6
pod n/a
total 157 227 69.1


line stmt bran cond sub pod time code
1             #!/usr/bin/env perl
2             #
3             # nl - line numbering filter
4             #
5             # 2020.10.25 v1.00 jul : first public release
6              
7             =begin metadata
8              
9             Name: nl
10             Description: line numbering filter
11             Author: jul, kaldor@cpan.org
12             License: artistic2
13              
14             =end metadata
15              
16             =cut
17              
18 1     1   3859 use strict;
  1         1  
  1         29  
19 1     1   3 use warnings;
  1         1  
  1         40  
20 1     1   450 use utf8;
  1         205  
  1         5  
21              
22 1     1   390 use Getopt::Std qw(getopts);
  1         1784  
  1         62  
23 1     1   7 use File::Basename qw(basename);
  1         1  
  1         65  
24 1     1   494 use Pod::Usage qw(pod2usage);
  1         57581  
  1         175  
25              
26 1     1   10 use constant EX_SUCCESS => 0;
  1         2  
  1         191  
27 1     1   8 use constant EX_FAILURE => 1;
  1         8  
  1         3251  
28              
29 1         164153 our $VERSION = '1.3';
30 1         49 my $program = basename($0);
31              
32             # options
33 1         2 $Getopt::Std::STANDARD_HELP_VERSION = 1;
34 1         3 my %options = ();
35 1 50       5 getopts('b:d:f:h:i:n:ps:v:w:', \%options) or pod2usage(EX_FAILURE);
36 1         89 my $file = shift;
37 1 50       3 $file = '-' unless defined $file;
38 1 50       1 pod2usage(EX_FAILURE) if @ARGV;
39              
40 1   50     3 my $type_b = $options{b} || "t";
41 1   50     5 my $delim = $options{d} || '\:';
42 1   50     4 my $type_f = $options{f} || "n";
43 1   50     4 my $type_h = $options{h} || "n";
44 1         1 my $incr = $options{i};
45 1         2 my $format = $options{n};
46 1         2 my $single_page = $options{p};
47 1 50       3 my $sep = exists $options{'s'} ? $options{'s'} : "\t";
48 1         2 my $startnum = $options{v};
49 1         1 my $width = $options{w};
50 1 50       2 if (defined $format) {
51 1         15 my %expect = (
52             'ln' => 1,
53             'rn' => 1,
54             'rz' => 1,
55             );
56 1 50       5 unless ($expect{$format}) {
57 0         0 warn "$program: invalid line number format: '$format'\n";
58 0         0 exit EX_FAILURE;
59             }
60             }
61             else {
62 0         0 $format = 'rn';
63             }
64              
65 1 50       2 if (defined $width) {
66 0 0 0     0 if ($width !~ m/\A\+?[0-9]+\Z/ || $width == 0) {
67 0         0 warn "$program: invalid line number field width: '$width'\n";
68 0         0 exit EX_FAILURE;
69             }
70 0         0 $width = int $width; # strip '+'
71             }
72             else {
73 1         1 $width = 6;
74             }
75              
76 1 50       2 if (defined $startnum) {
77 1 50       8 if ($startnum !~ m/\A[\+\-]?[0-9]+\Z/) {
78 0         0 warn "$program: invalid starting line number: '$startnum'\n";
79 0         0 exit EX_FAILURE;
80             }
81             }
82             else {
83 0         0 $startnum = 1;
84             }
85              
86 1 50       3 if (defined $incr) {
87 0 0       0 if ($incr !~ m/\A[\+\-]?[0-9]+\Z/) {
88 0         0 warn "$program: invalid line number increment: '$incr'\n";
89 0         0 exit EX_FAILURE;
90             }
91             }
92             else {
93 1         1 $incr = 1;
94             }
95              
96             sub VERSION_MESSAGE {
97 0     0   0 print "$program version $VERSION\n";
98 0         0 exit EX_SUCCESS;
99             }
100              
101             # options -b -f -h
102 1         1 my $regex_b = "";
103 1         1 my $regex_f = "";
104 1         1 my $regex_h = "";
105              
106 1         2 ($type_b, $regex_b) = split //, $type_b, 2;
107 1         2 ($type_f, $regex_f) = split //, $type_f, 2;
108 1         3 ($type_h, $regex_h) = split //, $type_h, 2;
109              
110 1         2 my @type = ($type_h, $type_b, $type_f,); # don't change order
111 1         2 for (@type) {
112 3         5 my %expect = (
113             'a' => 1,
114             't' => 1,
115             'n' => 1,
116             'p' => 1,
117             'e' => 1,
118             );
119 3 50       6 unless ($expect{$_}) {
120 0         0 warn "$program: invalid numbering style: '$_'\n";
121 0         0 pod2usage(EX_FAILURE);
122             }
123             }
124              
125 1         2 my @regex = ($regex_h, $regex_b, $regex_f); # don't change order
126              
127             # options -d
128 1         2 my $delim_std = '\:';
129 1         2 substr($delim_std, 0, length($delim), $delim);
130 1         2 $delim = quotemeta(substr($delim_std, 0, 2)); # max 2 chars, backslash escaped
131              
132             # options -n -w
133 1         1 my $format_str = '%';
134 1 50       3 $format_str .= '-' if $format eq "ln";
135 1 50       2 $format_str .= '0' if $format eq "rz";
136 1         7 $format_str .= $width;
137 1         1 $format_str .= 'd';
138              
139             # options -v
140 1         1 my $number = $startnum;
141              
142 1         1 my $section = 1;
143 1         1 my $new_section = 1;
144              
145 1 50       4 exit (do_file($file) ? EX_FAILURE : EX_SUCCESS);
146              
147             ###############
148             # SUBROUTINES #
149             ###############
150              
151             sub print_number {
152              
153 7     7   5 my $match = shift;
154              
155 7 100       7 if ($match)
156             {
157 4         33 printf($format_str, $number);
158 4         4 $number += $incr;
159             }
160             else
161             {
162 3         4 print ' ' x $width;
163             }
164              
165 7         7 print $sep;
166             }
167              
168             sub print_line {
169              
170 7     7   7 my $line = shift;
171 7         5 my $type = shift;
172 7         5 my $regex = shift;
173              
174 7 50       29 if ($type eq 'a')
    50          
    100          
    50          
    50          
175             {
176 0         0 print_number(1);
177             }
178             elsif ($type eq 't')
179             {
180 0 0       0 my $match = $line =~ /\A\Z/ ? 0 : 1;
181 0         0 print_number($match);
182             }
183             elsif ($type eq 'n')
184             {
185 2         2 print_number(0);
186             }
187             elsif ($type eq 'p')
188             {
189 0 0       0 my $match = $line =~ /$regex/ ? 1 : 0;
190 0         0 print_number($match);
191             }
192             elsif ($type eq 'e')
193             {
194 5 100       18 my $match = $line =~ /$regex/ ? 0 : 1;
195 5         4 print_number($match);
196             }
197             else
198             {
199 0         0 warn "$program: invalid type '$type'\n";
200 0         0 pod2usage(EX_FAILURE);
201             }
202              
203 7         18 print $line;
204             }
205              
206             sub do_file {
207 1     1   1 my $name = shift;
208 1         1 my ($fh, $line);
209              
210 1 50       3 if ($name eq '-')
211             {
212 0         0 $fh = *STDIN;
213             }
214             else
215             {
216 1 50       42 if (-d $name)
217             {
218 0         0 warn "$program: '$name': is a directory\n";
219 0         0 return 1;
220             }
221 1 50       41 unless (open $fh, '<', $name)
222             {
223 0         0 warn "$program: '$name': $!\n";
224 0         0 return 1;
225             }
226             }
227              
228 1         44 while ($line = <$fh>)
229             {
230 10 100       53 if ($line =~ /^($delim)($delim)?($delim)?$/)
231             {
232 3 100       12 if ($3) {$new_section = 0} # header
  1 100       2  
233 1         2 elsif ($2) {$new_section = 1} # body
234 1         1 else {$new_section = 2} # footer
235              
236             # change page
237 3 100       5 if ($new_section <= $section)
238             {
239 1 50       2 $number = $startnum unless $single_page;
240             }
241              
242 3         4 $section = $new_section;
243             }
244             else
245             {
246 7         31 print_line($line, $type[$section], $regex[$section]);
247             }
248             }
249 1 50       12 unless (close $fh)
250             {
251 0           warn "$program: cannot close '$name': $!\n";
252 0           return 1;
253             }
254              
255 1           return 0;
256             }
257              
258             __END__