File Coverage

blib/lib/App/PodLinkCheck.pm
Criterion Covered Total %
statement 119 349 34.1
branch 32 160 20.0
condition 9 53 16.9
subroutine 21 46 45.6
pod 4 13 30.7
total 185 621 29.7


line stmt bran cond sub pod time code
1             # Copyright 2010, 2011, 2012, 2013, 2016, 2017 Kevin Ryde
2              
3             # This file is part of PodLinkCheck.
4              
5             # PodLinkCheck is free software; you can redistribute it and/or modify it
6             # under the terms of the GNU General Public License as published by the Free
7             # Software Foundation; either version 3, or (at your option) any later
8             # version.
9             #
10             # PodLinkCheck is distributed in the hope that it will be useful, but
11             # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
12             # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
13             # for more details.
14             #
15             # You should have received a copy of the GNU General Public License along
16             # with PodLinkCheck. If not, see .
17              
18             package App::PodLinkCheck;
19 1     1   599 use 5.006;
  1         4  
20 1     1   5 use strict;
  1         2  
  1         16  
21 1     1   3 use warnings;
  1         3  
  1         23  
22 1     1   5 use Carp;
  1         1  
  1         63  
23 1     1   4 use File::Spec;
  1         2  
  1         34  
24 1     1   526 use Locale::TextDomain ('App-PodLinkCheck');
  1         17879  
  1         7  
25              
26 1     1   7013 use vars '$VERSION';
  1         2  
  1         368  
27             $VERSION = 15;
28              
29             # uncomment this to run the ### lines
30             # use Smart::Comments;
31              
32             sub command_line {
33 0     0 1 0 my ($self) = @_;
34             ### command_line(): @ARGV
35 0 0       0 ref $self or $self = $self->new;
36              
37 0         0 require Getopt::Long;
38 0         0 Getopt::Long::Configure ('permute', # options with args, callback '<>'
39             'no_ignore_case',
40             'bundling');
41             Getopt::Long::GetOptions
42 0     0   0 ('version' => sub { $self->action_version },
43 0     0   0 'help' => sub { $self->action_help },
44             'verbose:i' => \$self->{'verbose'},
45             'V+' => \$self->{'verbose'},
46             'I=s' => $self->{'extra_INC'}, # push onto arrayref
47             '<>' => sub {
48 0     0   0 my ($value) = @_;
49             # stringize to avoid Getopt::Long object
50 0         0 $self->check_tree ("$value");
51             },
52 0         0 );
53             ### final ARGV: @ARGV
54 0         0 $self->check_tree (@ARGV);
55 0 0       0 return ($self->{'report_count'} ? 1 : 0);
56             }
57              
58             sub action_version {
59 0     0 0 0 my ($self) = @_;
60 0         0 print __x("PodLinkCheck version {version}\n", version => $self->VERSION);
61 0 0       0 if ($self->{'verbose'} >= 2) {
62 0         0 require Pod::Simple;
63 0         0 print __x(" Perl version {version}\n", version => $]);
64 0         0 print __x(" Pod::Simple version {version}\n", version => Pod::Simple->VERSION);
65             }
66 0         0 return 0;
67             }
68              
69             sub action_help {
70 0     0 0 0 my ($self) = @_;
71 0         0 require FindBin;
72 1     1   7 no warnings 'once';
  1         2  
  1         492  
73 0         0 my $progname = $FindBin::Script;
74 0         0 print __x("Usage: $progname [--options] file-or-dir...\n");
75 0         0 print __x(" --help print this message\n");
76 0         0 print __x(" --version print version number (and module versions if --verbose=2)\n");
77 0         0 print __x(" --verbose print diagnostic details\n");
78 0         0 print __x(" --verbose=2 print even more diagnostics\n");
79 0         0 return 0;
80             }
81              
82              
83             #------------------------------------------------------------------------------
84              
85             sub new {
86 6     6 1 4224 my ($class, @options) = @_;
87 6         42 return bless { verbose => 0,
88             cpan_methods => ['CPAN_SQLite','cpanminus','CPAN','CPANPLUS'],
89             extra_INC => [],
90             report_count => 0,
91             @options }, $class;
92             }
93              
94             sub check_tree {
95 0     0 1 0 my ($self, @files_or_directories) = @_;
96             ### check_tree(): \@files_or_directories
97              
98 0         0 foreach my $filename (@files_or_directories) {
99 0 0       0 if (-d $filename) {
100 0         0 require File::Find::Iterator;
101 0         0 my $finder = File::Find::Iterator->create (dir => [$filename],
102             order => \&_find_order,
103             filter => \&_is_perlfile);
104 0         0 while ($filename = $finder->next) {
105 0         0 print "$filename:\n";
106 0         0 $self->check_file ($filename);
107             }
108             } else {
109 0         0 print "$filename:\n";
110 0         0 $self->check_file ($filename);
111             }
112             }
113              
114             # ### recurse dir: $filename
115             # require File::Find;
116             # File::Find::find ({ wanted => sub {
117             # #### file: $_
118             # if (_is_perlfile()) {
119             # print "$_:\n";
120             # $self->check_file ($_);
121             # }
122             # },
123             # follow_fast => 1,
124             # preprocess => \&_find_sort,
125             # no_chdir => 1,
126             # },
127             # $filename);
128             # } else {
129             # print "$filename:\n";
130             # $self->check_file ($filename);
131             # }
132             # }
133             }
134              
135             # $_ is a filename.
136             # Return true if $_ looks like a Perl .pl .pm .pod filename.
137             # Any emacs .#foo.pm etc lockfile is excluded.
138             # (Emacs auto-saves and backups change the suffix so don't have
139             # to be excluded.)
140             #
141             sub _is_perlfile {
142             ### _is_perlfile(): $_
143             return (! -d
144 0   0 0   0 && ! m{/\.#} # not emacs lockfile
145             && /\.p([lm]|od)$/);
146             }
147              
148             use constant::defer _HAVE_SORT_KEY_NATURAL => sub {
149 0         0 eval { require Sort::Key::Natural; 1 };
  0         0  
  0         0  
150 1     1   564 };
  1         1436  
  1         15  
151              
152             # _find_order($x,$y) compares filenames $x,$y
153             # Return spaceship style 1,false,-1 order.
154             # File::Find::Iterator 0.4 wants a reverse order since it pop()s off its
155             # pending filenames, hence the nasty hack to swap here.
156             #
157             # File::Find::Iterator also sorts with a mixture of current directory and
158             # subdirectory. Not sure if the result is a strict pre-order traversal if
159             # some directory names have chars before or after "/". Would like to ask it
160             # for pre-order.
161             #
162             sub _find_order {
163 0     0   0 my ($x, $y) = @_;
164 0         0 ($x,$y) = ($y,$x);
165             return (_cmp_file_before_directory($x,$y)
166 0   0     0 || do {
167             if (_HAVE_SORT_KEY_NATURAL) {
168             $x = Sort::Key::Natural::mkkey_natural($x);
169             $y = Sort::Key::Natural::mkkey_natural($y);
170             }
171             lc($x) cmp lc($y) || $x cmp $y
172             });
173             }
174              
175             # $x,$y are filenames.
176             # Return spaceship style <=> comparision 1,false,-1 which reckons
177             # file < directory.
178             sub _cmp_file_before_directory {
179 4     4   400 my ($x, $y) = @_;
180             # If $x or $y is a dangling symlink then -d is undef rather than '' false,
181             # hence "||0" for the compare.
182 4   100     105 return (-d $x || 0) <=> (-d $y || 0);
      100        
183             }
184              
185             sub check_file {
186 0     0 1 0 my ($self, $filename) = @_;
187 0         0 require App::PodLinkCheck::ParseLinks;
188 0         0 my $parser = App::PodLinkCheck::ParseLinks->new ($self);
189 0         0 $parser->parse_from_file ($filename);
190              
191 0         0 my $own_sections = $parser->sections_hashref;
192             ### $own_sections
193              
194 0         0 foreach my $link (@{$parser->links_arrayref}) {
  0         0  
195 0         0 my ($type, $to, $section, $linenum, $column) = @$link;
196              
197 0 0       0 if ($self->{'verbose'}) {
198 0 0       0 print "Link: $type ",(defined $to ? $to : '[undef]'),
    0          
199             (defined $section ? " / $section" : ""), "\n";
200             }
201              
202 0 0       0 if ($type eq 'man') {
203 0 0       0 if (! $self->manpage_is_known($to)) {
204 0         0 $self->report ($filename, $linenum, $column,
205             __x('no man page "{name}"', name => $to));
206             }
207 0         0 next;
208             }
209              
210 0 0       0 if (! defined $to) {
211 0 0 0     0 if (defined $section
212             && ! $own_sections->{$section}) {
213 0 0       0 if (my $approximations
214             = _section_approximations($section,$own_sections)) {
215 0         0 $self->report ($filename, $linenum, $column,
216             __x("no section \"{section}\"\n perhaps it should be {approximations}",
217             section => $section,
218             approximations => $approximations));
219             } else {
220 0         0 $self->report ($filename, $linenum, $column,
221             __x('no section "{section}"',
222             section => $section));
223             }
224 0 0       0 if ($self->{'verbose'} >= 2) {
225 0         0 print __(" available sections:\n");
226 0         0 foreach my $section (sort keys %$own_sections) {
227 0         0 print " $section\n";
228             }
229             }
230             }
231 0         0 next;
232             }
233              
234 0   0     0 my $podfile = ($self->module_to_podfile($to)
235             || $self->find_script($to));
236             ### $podfile
237 0 0       0 if (! defined $podfile) {
238 0 0       0 if (my $method = $self->_module_known_cpan($to)) {
239 0 0 0     0 if (defined $section && $section ne '') {
240 0         0 $self->report ($filename, $linenum, $column,
241             __x('target "{name}" on cpan ({method}) but no local copy to check section "{section}"',
242             name => $to,
243             method => $method,
244             section => $section));
245             }
246 0         0 next;
247             }
248             }
249              
250 0 0 0     0 if (! defined $podfile
      0        
251             && ! defined $section
252             && $self->manpage_is_known($to)) {
253             # perhaps a script or something we can't find the source but does
254             # have a manpage -- take that as good enough
255 0         0 next;
256             }
257 0 0 0     0 if (! defined $section
      0        
258             && _is_one_word($to)
259             && $own_sections->{$to}) {
260             # one-word internal section
261 0 0       0 if (defined $podfile) {
262             # print "$filename:$linenum:$column: target \"$to\" is both external module/program and internal section\n";
263             } else {
264 0         0 $self->report ($filename, $linenum, $column,
265             __x('internal one-word link recommend guard against ambiguity with {slash} or {quote}',
266             slash => "L",
267             quote => "L<\""._escape_angles($to)."\">"));
268             }
269 0         0 next;
270             }
271 0 0       0 if (! defined $podfile) {
272 0 0       0 if ($own_sections->{$to}) {
273             # multi-word internal section
274 0         0 return;
275             }
276 0         0 $self->report ($filename, $linenum, $column,
277             "no module/program/pod \"$to\"");
278 0         0 next;
279             }
280              
281 0 0 0     0 if (defined $section && $section ne '') {
282 0         0 my $podfile_sections = $self->filename_to_sections ($podfile);
283 0 0       0 if (! $podfile_sections->{$section}) {
284 0 0       0 if (my $approximations
285             = _section_approximations($section,$podfile_sections)) {
286 0         0 $self->report ($filename, $linenum, $column,
287             __x("no section \"{section}\" in \"{name}\" (file {filename})\n perhaps it should be {approximations}",
288             name => $to,
289             section => $section,
290             filename => $podfile,
291             approximations => $approximations));
292             } else {
293 0         0 $self->report ($filename, $linenum, $column,
294             __x('no section "{section}" in "{name}" (file {filename})',
295             name => $to,
296             section => $section,
297             filename => $podfile));
298             }
299 0 0       0 if ($self->{'verbose'} >= 2) {
300 0         0 print __(" available sections:\n");
301 0         0 foreach my $section (keys %$podfile_sections) {
302 0         0 print " $section\n";
303             }
304             }
305             }
306             }
307             }
308             }
309              
310             sub report {
311 0     0 0 0 my ($self, $filename, $linenum, $column, $message) = @_;
312 0         0 print "$filename:$linenum:$column: $message\n";
313 0         0 $self->{'report_count'}++;
314             }
315              
316             # return a string of close matches of $section in the keys of %$hashref
317             sub _section_approximations {
318 0     0   0 my ($section, $hashref) = @_;
319 0         0 $section = _section_approximation_crunch($section);
320             return join(' or ',
321 0         0 map {"\"$_\""}
322 0         0 grep {_section_approximation_crunch($_) eq $section}
  0         0  
323             keys %$hashref);
324             }
325             sub _section_approximation_crunch {
326 0     0   0 my ($section) = @_;
327 0         0 $section =~ s/(\W|_)+//g;
328 0         0 return lc($section);
329             }
330              
331             sub _is_one_word {
332 0     0   0 my ($link) = @_;
333 0         0 return ($link !~ /\W/);
334             }
335              
336             # change all < and > in $str to pod style E and E
337             sub _escape_angles {
338 3     3   1697 my ($str) = @_;
339 3         19 $str =~ s{([<>])}
340 4 100       27 { 'E<'.($1 eq '<' ? 'lt' : 'gt').'>' }ge;
341 3         22 return $str;
342             }
343              
344             sub module_to_podfile {
345 0     0 0 0 my ($self, $module) = @_;
346             ### module_to_podfile(): $module
347             ### dirs: $self->{'extra_INC'}
348 0         0 require Pod::Find;
349 0         0 return Pod::Find::pod_where ({ '-dirs' => $self->{'extra_INC'},
350             '-inc' => 1,
351             },
352             $module);
353             }
354              
355             # return hashref
356             sub filename_to_sections {
357 0     0 0 0 my ($self, $filename) = @_;
358 0   0     0 return ($self->{'sections_cache'}->{$filename} ||= do {
359             ### parse file for sections: $filename
360 0         0 my $parser = App::PodLinkCheck::ParseSections->new;
361 0         0 $parser->parse_file ($filename);
362             ### file sections: $parser->sections_hashref
363 0         0 $parser->sections_hashref;
364             });
365             }
366              
367             #------------------------------------------------------------------------------
368             # CPAN
369             #
370             # cf CPAN::API::HOWTO
371              
372             # look for $module in the cpan indexes
373             # if found return the name of the cpan method it was found in
374             # if not found return false
375             sub _module_known_cpan {
376 0     0   0 my ($self, $module) = @_;
377 0         0 foreach my $method (@{$self->{'cpan_methods'}}) {
  0         0  
378 0         0 my $fullmethod = "_module_known_$method";
379 0 0       0 if ($self->$fullmethod ($module)) {
380 0         0 return $method;
381             }
382             }
383 0         0 return 0;
384             }
385              
386             {
387             # a bit of a hack to suppress CPAN.pm messages, unless our verbose
388             package App::PodLinkCheck::CPANquiet;
389             our @ISA;
390       0     sub print_ornamented { }
391             }
392             use constant::defer _CPAN_config => sub {
393 1         27 my ($self) = @_;
394             ### _CPAN_config() ...
395              
396 1         3 my $result = 0;
397             eval {
398 1         951 require CPAN;
399 1 50       226903 if (! $self->{'verbose'}) {
400             # usually $CPAN::Frontend is CPAN::Shell
401 1         17 @App::PodLinkCheck::CPANquiet::ISA = ($CPAN::Frontend);
402 1         6 $CPAN::Frontend = 'App::PodLinkCheck::CPANquiet::ISA';
403             }
404             # not sure how far back this will work, maybe only 5.8.0 up
405 1 50 33     20 if (! $CPAN::Config_loaded
406             && CPAN::HandleConfig->can('load')) {
407             # fake $loading to avoid running the CPAN::FirstTime dialog -- is
408             # this the right way to do that?
409 1         4 local $CPAN::HandleConfig::loading = 1;
410 1 50       6 if ($self->{'verbose'}) {
411 0         0 print __x("PodLinkCheck: {module} configs\n",
412             module => 'CPAN');
413             }
414 1         9 CPAN::HandleConfig->load;
415             }
416 1         400 $result = 1;
417             }
418 1 50       2 or do {
419 0 0       0 if ($self->{'verbose'}) {
420 0         0 print "CPAN.pm config error: $@\n";
421             }
422             };
423 1         8 return $result;
424 1     1   1208 };
  1         2  
  1         7  
425              
426             sub _module_known_CPAN_SQLite {
427 3     3   658 my ($self, $module) = @_;
428              
429 3 100       16 if (! defined $self->{'cpan_sqlite'}) {
430 1         4 $self->{'cpan_sqlite'} = 0; # no sqlite, unless we succeed below
431              
432 1 50       7 if ($self->_CPAN_config($self->{'verbose'})) {
433             # configs loaded
434              
435 1 50       67 if ($self->{'verbose'}) {
436 0         0 print __x("PodLinkCheck: loading {module} for module existence checking\n",
437             module => 'CPAN::SQLite');
438             }
439 1 50       4 if (! eval { require CPAN::SQLite }) {
  1         211  
440 1 50       5 if ($self->{'verbose'}) {
441 0         0 print __x("Cannot load {module}, skipping -- {error}\n",
442             module => 'CPAN::SQLite',
443             error => $@);
444             }
445 1         10 return 0;
446             }
447              
448             # Quieten warning messags from CPAN::SQLite apparently when never yet run
449             local $SIG{'__WARN__'} = sub {
450 0 0   0   0 if ($self->{'verbose'}) {
451 0         0 warn @_;
452             }
453 0         0 };
454 0 0       0 if (! eval {
455             # fake $loading to avoid running the CPAN::FirstTime dialog -- is
456             # this the right way to do that?
457 0         0 local $CPAN::HandleConfig::loading = 1;
458 0         0 $self->{'cpan_sqlite'} = CPAN::SQLite->new (update_indices => 0);
459             }) {
460 0 0       0 if ($self->{'verbose'}) {
461 0         0 print __x("{module} error: {error}\n",
462             module => 'CPAN::SQLite',
463             error => $@);
464             }
465             }
466             }
467             }
468              
469 2   50     21 my $cpan_sqlite = $self->{'cpan_sqlite'} || return 0;
470              
471             # Have struck errors from cpantesters creating db tables. Not sure if it
472             # might happen in a real run. Guard with an eval.
473             #
474 0         0 my $result;
475 0 0       0 if (! eval { $result = $cpan_sqlite->query (mode => 'module',
  0         0  
476             name => $module);
477 0         0 1 }) {
478 0 0       0 if ($self->{'verbose'}) {
479 0         0 print __x("{module} error, disabling -- {error}\n",
480             module => 'CPAN::SQLite',
481             error => $@);
482             }
483 0         0 $self->{'cpan_sqlite'} = 0;
484 0         0 return 0;
485             }
486 0         0 return $result;
487             }
488              
489             my $use_CPAN;
490             sub _module_known_CPAN {
491 3     3   942 my ($self, $module) = @_;
492             ### _module_known_CPAN(): $module
493              
494 3 100       9 if (! defined $use_CPAN) {
495 1         3 $use_CPAN = 0;
496              
497 1 50       8 if ($self->_CPAN_config($self->{'verbose'})) {
498             eval {
499 1 50       4 if ($self->{'verbose'}) {
500 0         0 print __x("PodLinkCheck: load {module} for module existence checking\n",
501             module => 'CPAN');
502             }
503              
504 1 50 33     31 if (defined $CPAN::META && %$CPAN::META) {
    50          
505 0         0 $use_CPAN = 1;
506             } elsif (! CPAN::Index->can('read_metadata_cache')) {
507 0 0       0 if ($self->{'verbose'}) {
508 0         0 print __("PodLinkCheck: no Metadata cache in this CPAN.pm\n");
509             }
510             } else {
511             # try the .cpan/Metadata even if CPAN::SQLite is installed, just in
512             # case the SQLite is not up-to-date or has not been used yet
513 1         4 local $CPAN::Config->{use_sqlite} = 0;
514 1         7 CPAN::Index->read_metadata_cache;
515 0 0 0     0 if (defined $CPAN::META && %$CPAN::META) {
516 0         0 $use_CPAN = 1;
517             } else {
518 0 0       0 if ($self->{'verbose'}) {
519 0         0 print __("PodLinkCheck: empty Metadata cache\n");
520             }
521             }
522             }
523 0         0 1;
524             }
525 1 50       3 or do {
526 1 50       4629 if ($self->{'verbose'}) {
527 0         0 print "CPAN.pm error: $@\n";
528             }
529             };
530             }
531             }
532              
533             return ($use_CPAN
534 3   33     17 && exists($CPAN::META->{'readwrite'}->{'CPAN::Module'}->{$module}));
535             }
536              
537             sub _module_known_CPANPLUS {
538 3     3   913 my ($self, $module) = @_;
539             ### _module_known_CPANPLUS(): $module
540              
541 3 100       12 if (! defined $self->{'cpanplus'}) {
542 1 50       6 if ($self->{'verbose'}) {
543 0         0 print __x("PodLinkCheck: load {module} for module existence checking\n",
544             module => 'CPANPLUS');
545             }
546 1 50       2 if (! eval { require CPANPLUS::Backend;
  1         322  
547 0         0 require CPANPLUS::Configure;
548             }) {
549 1         7 $self->{'cpanplus'} = 0;
550 1 50       5 if ($self->{'verbose'}) {
551 0         0 print __x("Cannot load {module}, skipping -- {error}\n",
552             module => 'CPANPLUS',
553             error => $@);
554             }
555 1         5 return 0;
556             }
557 0         0 my $conf = CPANPLUS::Configure->new;
558 0         0 $conf->set_conf (verbose => 1);
559 0         0 $conf->set_conf (no_update => 1);
560 0         0 $self->{'cpanplus'} = CPANPLUS::Backend->new ($conf);
561             }
562              
563 2   50     14 my $cpanplus = $self->{'cpanplus'} || return 0;
564              
565             # module_tree() returns false '' for not found.
566             #
567             # Struck an error from module_tree() somehow relating to
568             # CPANPLUS::Internals::Source::SQLite on cpantesters at one time, so guard
569             # with an eval.
570             #
571 0         0 my $result;
572 0 0       0 if (! eval { $result = $cpanplus->module_tree($module); 1 }) {
  0         0  
  0         0  
573 0 0       0 if ($self->{'verbose'}) {
574 0         0 print __x("{module} error, disabling -- {error}\n",
575             module => 'CPANPLUS',
576             error => $@);
577             }
578 0         0 $self->{'cpanplus'} = 0;
579 0         0 return 0;
580             }
581 0         0 return $result;
582             }
583              
584             sub _module_known_cpanminus {
585 3     3   803 my ($self, $module) = @_;
586             ### _module_known_cpanminus(): $module
587              
588 3         12 foreach my $filename ($self->_cpanminus_packages_details_filenames()) {
589 0         0 my $fh;
590 0 0       0 unless (open $fh, '<', $filename) {
591 0 0       0 unless ($self->{'cpanminus-warned'}->{$filename}++) {
592 0 0       0 if ($self->{'verbose'}) {
593 0         0 print __x("PodLinkCheck: cannot open {filename}: {error}\n",
594             filename => $filename,
595             error => "$!");
596             }
597             }
598 0         0 next;
599             }
600 0 0       0 unless ($self->{'cpanminus'}->{$filename}++) {
601 0 0       0 if ($self->{'verbose'}) {
602 0         0 print __x("PodLinkCheck: module existence checking in {filename}\n",
603             filename => $filename);
604             }
605             }
606              
607             # binary search
608 0 0       0 if (_packages_details_bsearch($fh, $module)) {
609 0         0 return 1;
610             }
611              
612             # Plain search.
613             # while (defined(my $line = readline $fh)) {
614             # if ($line =~ /^\Q$module\E /) {
615             # return 1;
616             # }
617             # }
618             }
619 3         13 return 0;
620             }
621              
622             # Return a list of all the 02packages.details.txt files in App::cpanminus.
623             # eg. "/home/foo/.cpanm/sources/http%www.cpan.org/02packages.details.txt".
624             # ENHANCE-ME: Only one of the filenames returned will be its configured
625             # mirror. Will it tell us which?
626             sub _cpanminus_packages_details_filenames {
627             # my ($self) = @_;
628 3     3   14 require File::HomeDir;
629 3         16 my $home = File::HomeDir->my_home;
630 3 50       91 if (! defined $home) { return; } # undef if no $HOME
  0         0  
631              
632 3         43 my $wildcard = File::Spec->catfile($home, '.cpanm', 'sources',
633             '*', '02packages.details.txt');
634 3         76 return glob $wildcard;
635             }
636              
637             # $fh is a file handle open on an 02packages.details.txt file.
638             # Return true if $module exists in the file (any version, any author).
639             #
640             # 02packages header lines are first field with trailing colon like
641             # File: 02packages.details.txt
642             # and a blank line before first module.
643             #
644             # Sort order is lc($a) cmp lc($a) per PAUSE::mldistwatch rewrite02().
645             # https://github.com/andk/pause/raw/master/lib/PAUSE/mldistwatch.pm
646             # This means modules differing only in upper/lower case are ordered by
647             # version number and author, which will be semi-random. Hence linear search
648             # after the bsearch finds the first. This doesn't happen often so the
649             # result is still good speed-wise.
650             #
651             sub _packages_details_bsearch {
652 0     0   0 my ($fh, $module) = @_;
653              
654 0         0 require Search::Dict;
655 0         0 my $lc_module = lc($module);
656             ### $lc_module
657 0         0 my $pos = Search::Dict::look ($fh, $lc_module,
658             { xfrm => \&_packages_details_line_to_module });
659             ### $pos
660 0 0       0 next if ! defined $pos;
661              
662 0         0 while (defined(my $line = readline $fh)) {
663 0 0       0 return 1 if $line =~ /^\Q$module\E /;
664 0 0       0 last if $line !~ /^\Q$module\E /i;
665             }
666 0         0 return 0;
667             }
668              
669             # $line is a line from an 02packages.details.txt file.
670             # Return the module name on the line, or empty string "" if not a module line.
671             sub _packages_details_line_to_module {
672 0     0   0 my ($line) = @_;
673 0 0       0 if ($line =~ /^([^ ]*[^ :]) /) {
674             ### at: lc($1)
675 0         0 return lc($1);
676             } else {
677 0         0 return '';
678             }
679             }
680              
681             #------------------------------------------------------------------------------
682             # PATH
683              
684             sub find_script {
685 0     0 0 0 my ($self, $name) = @_;
686 0         0 foreach my $dir ($self->PATH_list) {
687 0         0 my $filename = File::Spec->catfile($dir,$name);
688             #### $filename
689 0 0       0 if (-e $filename) {
690 0         0 return $filename;
691             }
692             }
693 0         0 return undef;
694             }
695              
696             # return list of directories
697             sub PATH_list {
698 0     0 0 0 my ($self) = @_;
699 0         0 require Config;
700 0         0 return split /\Q$Config::Config{'path_sep'}/o, $self->PATH;
701             }
702              
703             # return string
704             sub PATH {
705 0     0 0 0 my ($self) = @_;
706 0 0       0 if (defined $self->{'PATH'}) {
707 0         0 return $self->{'PATH'};
708             } else {
709 0         0 return $ENV{'PATH'};
710             }
711             }
712              
713             #------------------------------------------------------------------------------
714             # man
715              
716             # return bool
717             sub manpage_is_known {
718 6     6 0 3117 my ($self, $name) = @_;
719 6         15 my @manargs;
720 6         17 my $section = '';
721 6 100       41 if ($name =~ s/\s*\((.+)\)$//) {
722 2         10 $section = $1;
723 2         8 @manargs = ($section);
724             }
725              
726 6         34 my $r = \$self->{'manpage_is_known'}->{$section}->{$name};
727 6 50       26 if (defined $$r) {
728 0         0 return $$r;
729             }
730 6         17 push @manargs, $name;
731             ### man: \@manargs
732              
733 6 50       32 return ($$r = ($self->_man_has_location_option()
734             ? $self->_manpage_is_known_by_location(@manargs)
735             : $self->_manpage_is_known_by_output(@manargs)));
736             }
737              
738             # --location is not in posix,
739             # http://www.opengroup.org/onlinepubs/009695399/utilities/man.html
740             # Is it man-db specific, or does it have a chance of working elsewhere?
741             #
742             use constant::defer _man_has_location_option => sub {
743 1         31 my ($self) = @_;
744             ### _man_has_location_option() ...
745 1         925 require IPC::Run;
746 1         30502 my $str = '';
747 1         2 eval {
748 1         18 IPC::Run::run (['man','--help'],
749             '<', \undef,
750             '>', \$str,
751             '2>', File::Spec->devnull);
752             };
753 1         1348 my $ret = ($str =~ /--location\b/);
754 1 50       5 if ($self->{'verbose'} >= 2) {
755 0 0       0 if ($ret) {
756 0         0 print __("man \"--location\" option is available\n");
757             } else {
758 0         0 print __("man \"--location\" option not available (not in its \"--help\")\n");
759             }
760             }
761             ### $ret
762 1         5 return $ret;
763 1     1   1322 };
  1         3  
  1         7  
764              
765             sub _manpage_is_known_by_location {
766 0     0   0 my ($self, @manargs) = @_;
767             ### _manpage_is_known_by_location() run: \@manargs
768 0         0 require IPC::Run;
769 0         0 my $str;
770 0 0       0 if (! eval {
771 0         0 IPC::Run::run (['man', '--location', @manargs],
772             '<', \undef, # stdin
773             '>', \$str, # stdout
774             '2>', File::Spec->devnull);
775 0         0 1;
776             }) {
777 0         0 my $err = $@;
778 0         0 $err =~ s/\s+$//;
779 0         0 print __x("PodLinkCheck: error running 'man': {error}\n", error => $err);
780 0         0 return 0;
781             }
782             ### _manpage_is_known_by_location() output: $str
783 0 0       0 return ($str =~ /\n/ ? 1 : 0);
784             }
785              
786             sub _manpage_is_known_by_output {
787 6     6   67 my ($self, @manargs) = @_;
788             ### _manpage_is_known_by_output() run: \@manargs
789 6         45 require IPC::Run;
790 6         622 require File::Temp;
791 6         8439 my $fh = File::Temp->new (TEMPLATE => 'PodLinkCheck-man-XXXXXX',
792             TMPDIR => 1);
793 6 50       3114 if (! eval {
794 6         67 IPC::Run::run (['man', @manargs],
795             '<', \undef, # stdin
796             '>', $fh, # stdout
797             '2>', File::Spec->devnull);
798 0         0 1;
799             }) {
800 6         11940 my $err = $@;
801 6         51 $err =~ s/\s+$//;
802 6         30 print __x("PodLinkCheck: error running 'man': {error}\n", error => $err);
803 6         860 return 0;
804             }
805              
806 0           seek $fh, 0, 0;
807 0           foreach (1 .. 5) {
808 0 0         if (! defined (readline $fh)) {
809 0           return 0;
810             }
811             }
812 0           return 1;
813             }
814              
815             1;
816             __END__