8d1103b6a1fbcc11f8467aa2b2117311ad7bb22d
[perl.git] / ext / Pod-Parser / lib / Pod / Find.pm
1 #############################################################################  
2 # Pod/Find.pm -- finds files containing POD documentation
3 #
4 # Author: Marek Rouchal <marekr@cpan.org>
5
6 # Copyright (C) 1999-2000 by Marek Rouchal (and borrowing code
7 # from Nick Ing-Simmon's PodToHtml). All rights reserved.
8 # This file is part of "PodParser". Pod::Find is free software;
9 # you can redistribute it and/or modify it under the same terms
10 # as Perl itself.
11 #############################################################################
12
13 package Pod::Find;
14 use strict;
15
16 use vars qw($VERSION);
17 $VERSION = '1.35';   ## Current version of this package
18 require  5.005;   ## requires this Perl version or later
19 use Carp;
20
21 BEGIN {
22    if ($] < 5.006) {
23       require Symbol;
24       import Symbol;
25    }
26 }
27
28 #############################################################################
29
30 =head1 NAME
31
32 Pod::Find - find POD documents in directory trees
33
34 =head1 SYNOPSIS
35
36   use Pod::Find qw(pod_find simplify_name);
37   my %pods = pod_find({ -verbose => 1, -inc => 1 });
38   foreach(keys %pods) {
39      print "found library POD `$pods{$_}' in $_\n";
40   }
41
42   print "podname=",simplify_name('a/b/c/mymodule.pod'),"\n";
43
44   $location = pod_where( { -inc => 1 }, "Pod::Find" );
45
46 =head1 DESCRIPTION
47
48 B<Pod::Find> provides a set of functions to locate POD files.  Note that
49 no function is exported by default to avoid pollution of your namespace,
50 so be sure to specify them in the B<use> statement if you need them:
51
52   use Pod::Find qw(pod_find);
53
54 From this version on the typical SCM (software configuration management)
55 files/directories like RCS, CVS, SCCS, .svn are ignored.
56
57 =cut
58
59 #use diagnostics;
60 use Exporter;
61 use File::Spec;
62 use File::Find;
63 use Cwd;
64
65 use vars qw(@ISA @EXPORT_OK $VERSION);
66 @ISA = qw(Exporter);
67 @EXPORT_OK = qw(&pod_find &simplify_name &pod_where &contains_pod);
68
69 # package global variables
70 my $SIMPLIFY_RX;
71
72 =head2 C<pod_find( { %opts } , @directories )>
73
74 The function B<pod_find> searches for POD documents in a given set of
75 files and/or directories. It returns a hash with the file names as keys
76 and the POD name as value. The POD name is derived from the file name
77 and its position in the directory tree.
78
79 E.g. when searching in F<$HOME/perl5lib>, the file
80 F<$HOME/perl5lib/MyModule.pm> would get the POD name I<MyModule>,
81 whereas F<$HOME/perl5lib/Myclass/Subclass.pm> would be
82 I<Myclass::Subclass>. The name information can be used for POD
83 translators.
84
85 Only text files containing at least one valid POD command are found.
86
87 A warning is printed if more than one POD file with the same POD name
88 is found, e.g. F<CPAN.pm> in different directories. This usually
89 indicates duplicate occurrences of modules in the I<@INC> search path.
90
91 B<OPTIONS> The first argument for B<pod_find> may be a hash reference
92 with options. The rest are either directories that are searched
93 recursively or files.  The POD names of files are the plain basenames
94 with any Perl-like extension (.pm, .pl, .pod) stripped.
95
96 =over 4
97
98 =item C<-verbose =E<gt> 1>
99
100 Print progress information while scanning.
101
102 =item C<-perl =E<gt> 1>
103
104 Apply Perl-specific heuristics to find the correct PODs. This includes
105 stripping Perl-like extensions, omitting subdirectories that are numeric
106 but do I<not> match the current Perl interpreter's version id, suppressing
107 F<site_perl> as a module hierarchy name etc.
108
109 =item C<-script =E<gt> 1>
110
111 Search for PODs in the current Perl interpreter's installation 
112 B<scriptdir>. This is taken from the local L<Config|Config> module.
113
114 =item C<-inc =E<gt> 1>
115
116 Search for PODs in the current Perl interpreter's I<@INC> paths. This
117 automatically considers paths specified in the C<PERL5LIB> environment
118 as this is included in I<@INC> by the Perl interpreter itself.
119
120 =back
121
122 =cut
123
124 # return a hash of the POD files found
125 # first argument may be a hashref (options),
126 # rest is a list of directories to search recursively
127 sub pod_find
128 {
129     my %opts;
130     if(ref $_[0]) {
131         %opts = %{shift()};
132     }
133
134     $opts{-verbose} ||= 0;
135     $opts{-perl}    ||= 0;
136
137     my (@search) = @_;
138
139     if($opts{-script}) {
140         require Config;
141         push(@search, $Config::Config{scriptdir})
142             if -d $Config::Config{scriptdir};
143         $opts{-perl} = 1;
144     }
145
146     if($opts{-inc}) {
147         if ($^O eq 'MacOS') {
148             # tolerate '.', './some_dir' and '(../)+some_dir' on Mac OS
149             my @new_INC = @INC;
150             for (@new_INC) {
151                 if ( $_ eq '.' ) {
152                     $_ = ':';
153                 } elsif ( $_ =~ s{^((?:\.\./)+)}{':' x (length($1)/3)}e ) {
154                     $_ = ':'. $_;
155                 } else {
156                     $_ =~ s{^\./}{:};
157                 }
158             }
159             push(@search, grep($_ ne File::Spec->curdir, @new_INC));
160         } else {
161             push(@search, grep($_ ne File::Spec->curdir, @INC));
162         }
163
164         $opts{-perl} = 1;
165     }
166
167     if($opts{-perl}) {
168         require Config;
169         # this code simplifies the POD name for Perl modules:
170         # * remove "site_perl"
171         # * remove e.g. "i586-linux" (from 'archname')
172         # * remove e.g. 5.00503
173         # * remove pod/ if followed by *.pod (e.g. in pod/perlfunc.pod)
174
175         # Mac OS:
176         # * remove ":?site_perl:"
177         # * remove :?pod: if followed by *.pod (e.g. in :pod:perlfunc.pod)
178
179         if ($^O eq 'MacOS') {
180             $SIMPLIFY_RX =
181               qq!^(?i:\:?site_perl\:|\:?pod\:(?=.*?\\.pod\\z))*!;
182         } else {
183             $SIMPLIFY_RX =
184               qq!^(?i:site(_perl)?/|\Q$Config::Config{archname}\E/|\\d+\\.\\d+([_.]?\\d+)?/|pod/(?=.*?\\.pod\\z))*!;
185         }
186     }
187
188     my %dirs_visited;
189     my %pods;
190     my %names;
191     my $pwd = cwd();
192
193     foreach my $try (@search) {
194         unless(File::Spec->file_name_is_absolute($try)) {
195             # make path absolute
196             $try = File::Spec->catfile($pwd,$try);
197         }
198         # simplify path
199         # on VMS canonpath will vmsify:[the.path], but File::Find::find
200         # wants /unixy/paths
201         $try = File::Spec->canonpath($try) if ($^O ne 'VMS');
202         $try = VMS::Filespec::unixify($try) if ($^O eq 'VMS');
203         my $name;
204         if(-f $try) {
205             if($name = _check_and_extract_name($try, $opts{-verbose})) {
206                 _check_for_duplicates($try, $name, \%names, \%pods);
207             }
208             next;
209         }
210         my $root_rx = $^O eq 'MacOS' ? qq!^\Q$try\E! : qq!^\Q$try\E/!;
211         File::Find::find( sub {
212             my $item = $File::Find::name;
213             if(-d) {
214                 if($item =~ m{/(?:RCS|CVS|SCCS|\.svn)$}) {
215                     $File::Find::prune = 1;
216                     return;
217                 }
218                 elsif($dirs_visited{$item}) {
219                     warn "Directory '$item' already seen, skipping.\n"
220                         if($opts{-verbose});
221                     $File::Find::prune = 1;
222                     return;
223                 }
224                 else {
225                     $dirs_visited{$item} = 1;
226                 }
227                 if($opts{-perl} && /^(\d+\.[\d_]+)\z/s && eval "$1" != $]) {
228                     $File::Find::prune = 1;
229                     warn "Perl $] version mismatch on $_, skipping.\n"
230                         if($opts{-verbose});
231                 }
232                 return;
233             }
234             if($name = _check_and_extract_name($item, $opts{-verbose}, $root_rx)) {
235                 _check_for_duplicates($item, $name, \%names, \%pods);
236             }
237         }, $try); # end of File::Find::find
238     }
239     chdir $pwd;
240     return %pods;
241 }
242
243 sub _check_for_duplicates {
244     my ($file, $name, $names_ref, $pods_ref) = @_;
245     if($$names_ref{$name}) {
246         warn "Duplicate POD found (shadowing?): $name ($file)\n";
247         warn '    Already seen in ',
248             join(' ', grep($$pods_ref{$_} eq $name, keys %$pods_ref)),"\n";
249     }
250     else {
251         $$names_ref{$name} = 1;
252     }
253     return $$pods_ref{$file} = $name;
254 }
255
256 sub _check_and_extract_name {
257     my ($file, $verbose, $root_rx) = @_;
258
259     # check extension or executable flag
260     # this involves testing the .bat extension on Win32!
261     unless(-f $file && -T $file && ($file =~ /\.(pod|pm|plx?)\z/i || -x $file )) {
262       return;
263     }
264
265     return unless contains_pod($file,$verbose);
266
267     # strip non-significant path components
268     # TODO what happens on e.g. Win32?
269     my $name = $file;
270     if(defined $root_rx) {
271         $name =~ s/$root_rx//s;
272         $name =~ s/$SIMPLIFY_RX//s if(defined $SIMPLIFY_RX);
273     }
274     else {
275         if ($^O eq 'MacOS') {
276             $name =~ s/^.*://s;
277         } else {
278             $name =~ s{^.*/}{}s;
279         }
280     }
281     _simplify($name);
282     $name =~ s{/+}{::}g;
283     if ($^O eq 'MacOS') {
284         $name =~ s{:+}{::}g; # : -> ::
285     } else {
286         $name =~ s{/+}{::}g; # / -> ::
287     }
288     return $name;
289 }
290
291 =head2 C<simplify_name( $str )>
292
293 The function B<simplify_name> is equivalent to B<basename>, but also
294 strips Perl-like extensions (.pm, .pl, .pod) and extensions like
295 F<.bat>, F<.cmd> on Win32 and OS/2, or F<.com> on VMS, respectively.
296
297 =cut
298
299 # basic simplification of the POD name:
300 # basename & strip extension
301 sub simplify_name {
302     my ($str) = @_;
303     # remove all path components
304     if ($^O eq 'MacOS') {
305         $str =~ s/^.*://s;
306     } else {
307         $str =~ s{^.*/}{}s;
308     }
309     _simplify($str);
310     return $str;
311 }
312
313 # internal sub only
314 sub _simplify {
315     # strip Perl's own extensions
316     $_[0] =~ s/\.(pod|pm|plx?)\z//i;
317     # strip meaningless extensions on Win32 and OS/2
318     $_[0] =~ s/\.(bat|exe|cmd)\z//i if($^O =~ /mswin|os2/i);
319     # strip meaningless extensions on VMS
320     $_[0] =~ s/\.(com)\z//i if($^O eq 'VMS');
321 }
322
323 # contribution from Tim Jenness <t.jenness@jach.hawaii.edu>
324
325 =head2 C<pod_where( { %opts }, $pod )>
326
327 Returns the location of a pod document given a search directory
328 and a module (e.g. C<File::Find>) or script (e.g. C<perldoc>) name.
329
330 Options:
331
332 =over 4
333
334 =item C<-inc =E<gt> 1>
335
336 Search @INC for the pod and also the C<scriptdir> defined in the
337 L<Config|Config> module.
338
339 =item C<-dirs =E<gt> [ $dir1, $dir2, ... ]>
340
341 Reference to an array of search directories. These are searched in order
342 before looking in C<@INC> (if B<-inc>). Current directory is used if
343 none are specified.
344
345 =item C<-verbose =E<gt> 1>
346
347 List directories as they are searched
348
349 =back
350
351 Returns the full path of the first occurrence to the file.
352 Package names (eg 'A::B') are automatically converted to directory
353 names in the selected directory. (eg on unix 'A::B' is converted to
354 'A/B'). Additionally, '.pm', '.pl' and '.pod' are appended to the
355 search automatically if required.
356
357 A subdirectory F<pod/> is also checked if it exists in any of the given
358 search directories. This ensures that e.g. L<perlfunc|perlfunc> is
359 found.
360
361 It is assumed that if a module name is supplied, that that name
362 matches the file name. Pods are not opened to check for the 'NAME'
363 entry.
364
365 A check is made to make sure that the file that is found does 
366 contain some pod documentation.
367
368 =cut
369
370 sub pod_where {
371
372   # default options
373   my %options = (
374          '-inc' => 0,
375          '-verbose' => 0,
376          '-dirs' => [ File::Spec->curdir ],
377         );
378
379   # Check for an options hash as first argument
380   if (defined $_[0] && ref($_[0]) eq 'HASH') {
381     my $opt = shift;
382
383     # Merge default options with supplied options
384     %options = (%options, %$opt);
385   }
386
387   # Check usage
388   carp 'Usage: pod_where({options}, $pod)' unless (scalar(@_));
389
390   # Read argument
391   my $pod = shift;
392
393   # Split on :: and then join the name together using File::Spec
394   my @parts = split (/::/, $pod);
395
396   # Get full directory list
397   my @search_dirs = @{ $options{'-dirs'} };
398
399   if ($options{'-inc'}) {
400
401     require Config;
402
403     # Add @INC
404     if ($^O eq 'MacOS' && $options{'-inc'}) {
405         # tolerate '.', './some_dir' and '(../)+some_dir' on Mac OS
406         my @new_INC = @INC;
407         for (@new_INC) {
408             if ( $_ eq '.' ) {
409                 $_ = ':';
410             } elsif ( $_ =~ s{^((?:\.\./)+)}{':' x (length($1)/3)}e ) {
411                 $_ = ':'. $_;
412             } else {
413                 $_ =~ s{^\./}{:};
414             }
415         }
416         push (@search_dirs, @new_INC);
417     } elsif ($options{'-inc'}) {
418         push (@search_dirs, @INC);
419     }
420
421     # Add location of pod documentation for perl man pages (eg perlfunc)
422     # This is a pod directory in the private install tree
423     #my $perlpoddir = File::Spec->catdir($Config::Config{'installprivlib'},
424     #                                   'pod');
425     #push (@search_dirs, $perlpoddir)
426     #  if -d $perlpoddir;
427
428     # Add location of binaries such as pod2text
429     push (@search_dirs, $Config::Config{'scriptdir'})
430       if -d $Config::Config{'scriptdir'};
431   }
432
433   warn 'Search path is: '.join(' ', @search_dirs)."\n"
434         if $options{'-verbose'};
435
436   # Loop over directories
437   Dir: foreach my $dir ( @search_dirs ) {
438
439     # Don't bother if can't find the directory
440     if (-d $dir) {
441       warn "Looking in directory $dir\n"
442         if $options{'-verbose'};
443
444       # Now concatenate this directory with the pod we are searching for
445       my $fullname = File::Spec->catfile($dir, @parts);
446       warn "Filename is now $fullname\n"
447         if $options{'-verbose'};
448
449       # Loop over possible extensions
450       foreach my $ext ('', '.pod', '.pm', '.pl') {
451         my $fullext = $fullname . $ext;
452         if (-f $fullext &&
453          contains_pod($fullext, $options{'-verbose'}) ) {
454           warn "FOUND: $fullext\n" if $options{'-verbose'};
455           return $fullext;
456         }
457       }
458     } else {
459       warn "Directory $dir does not exist\n"
460         if $options{'-verbose'};
461       next Dir;
462     }
463     # for some strange reason the path on MacOS/darwin/cygwin is
464     # 'pods' not 'pod'
465     # this could be the case also for other systems that
466     # have a case-tolerant file system, but File::Spec
467     # does not recognize 'darwin' yet. And cygwin also has "pods",
468     # but is not case tolerant. Oh well...
469     if((File::Spec->case_tolerant || $^O =~ /macos|darwin|cygwin/i)
470      && -d File::Spec->catdir($dir,'pods')) {
471       $dir = File::Spec->catdir($dir,'pods');
472       redo Dir;
473     }
474     if(-d File::Spec->catdir($dir,'pod')) {
475       $dir = File::Spec->catdir($dir,'pod');
476       redo Dir;
477     }
478   }
479   # No match;
480   return;
481 }
482
483 =head2 C<contains_pod( $file , $verbose )>
484
485 Returns true if the supplied filename (not POD module) contains some pod
486 information.
487
488 =cut
489
490 sub contains_pod {
491   my $file = shift;
492   my $verbose = 0;
493   $verbose = shift if @_;
494
495   # check for one line of POD
496   my $podfh;
497   if ($] < 5.006) {
498     $podfh = gensym();
499   }
500
501   unless(open($podfh,"<$file")) {
502     warn "Error: $file is unreadable: $!\n";
503     return;
504   }
505   
506   local $/ = undef;
507   my $pod = <$podfh>;
508   close($podfh) || die "Error closing $file: $!\n";
509   unless($pod =~ /^=(head\d|pod|over|item|cut)\b/m) {
510     warn "No POD in $file, skipping.\n"
511       if($verbose);
512     return 0;
513   }
514
515   return 1;
516 }
517
518 =head1 AUTHOR
519
520 Please report bugs using L<http://rt.cpan.org>.
521
522 Marek Rouchal E<lt>marekr@cpan.orgE<gt>,
523 heavily borrowing code from Nick Ing-Simmons' PodToHtml.
524
525 Tim Jenness E<lt>t.jenness@jach.hawaii.eduE<gt> provided
526 C<pod_where> and C<contains_pod>.
527
528 =head1 SEE ALSO
529
530 L<Pod::Parser>, L<Pod::Checker>, L<perldoc>
531
532 =cut
533
534 1;
535