File Coverage

blib/lib/File/FindRoot.pm
Criterion Covered Total %
statement 76 76 100.0
branch 19 20 95.0
condition 12 16 75.0
subroutine 11 12 91.6
pod 1 1 100.0
total 119 125 95.2


line stmt bran cond sub pod time code
1 3     3   878101 use v5.36;
  3         12  
2 3     3   921 use utf8;
  3         624  
  3         28  
3              
4             package File::FindRoot;
5 3     3   128 use strict;
  3         5  
  3         154  
6              
7 3     3   15 use warnings;
  3         6  
  3         170  
8              
9 3     3   19 use Carp ();
  3         4  
  3         173  
10 3     3   20 use Cwd ();
  3         6  
  3         103  
11 3     3   15 use File::Basename ();
  3         5  
  3         84  
12 3     3   29 use File::Spec ();
  3         6  
  3         2509  
13              
14             our $VERSION = '0.003';
15              
16             =encoding utf8
17              
18             =head1 NAME
19              
20             File::FindRoot - Find the directory that's the root for a project
21              
22             =head1 SYNOPSIS
23              
24             use File::FindRoot;
25              
26             Start looking in the current directory and in each ancestor directory
27             until you find one that contains the relative path. Return that directory:
28              
29             my $dir = File::FindRoot->dir_contains( $rel_path );
30             unless( defined $dir ) { ... }
31              
32             Or matches a pattern:
33              
34             my $dir = File::FindRoot->dir_contains( qr/$patten/ );
35              
36             Start in a different directory:
37              
38             my $dir = File::FindRoot->dir_contains( $file, { start_at => $path } );
39              
40             Limit the number of ancestors checked:
41              
42             my $dir = File::FindRoot->dir_contains( $file, { limit => $n } );
43              
44             =head1 DESCRIPTION
45              
46             Lately I've done a number of things where a program deep in a project had to
47             find its project config file or library directory.
48              
49             =over 4
50              
51             =item File::FindRoot->dir_contains( REL_PATH [, OPTIONS] )
52              
53             Returns the directory that contains C, and the empty list otherwise.
54              
55             =over 4
56              
57             =item * callback - (default: check that path exists) a subroutine that returns true
58             if the current directory is the one you want based on whatever you decide.
59              
60             =item * debug - (default: 0) if true, output progress information. If you do not
61             specify a value, it uses the defined value of the C environment
62             variable, or finally, 0.
63              
64             =item * debug_fh - (default: STDERR) the output filehandle for debugging info
65              
66             =item * limit - (default: inf) the maximum number of ancestors to inspect.
67              
68             =item * start_at - (default: current working directory) the directory in which
69             to start looking. If the string is not a directory, such as a filename, it uses
70             the directory name of the path. Any value is turned into an absolute path.
71              
72             =back
73              
74             The C argument takes a code reference with three positional parameters:
75             the current candidate directory, the passed C argument, and the C
76             hash:
77              
78             my $coderef = sub ( $candidate_dir, $target, $options ) { ... };
79             File::FindRoot->dir_contains( '.git', { callback => $coderef });
80              
81             If you don't specify a C argument, if uses one that catfiles
82             the directory and target and returns the value of C<-e> on the result:
83              
84             my $coderef = sub ($candidate_dir, $target, $options) {
85             -e File::Spec->catfile($candidate_dir, $target)
86             };
87              
88             =cut
89              
90 26     26 1 141145 sub dir_contains ($class, $target, $options = {}) {
  26         48  
  26         36  
  26         36  
  26         62  
91 26 100       104 unless( ref $options eq ref {} ) {
92 1         217 Carp::carp "dir_contains: options argument must be a hash reference";
93 1         8 return;
94             }
95              
96 25   100     158 $options->{'debug'} //= $ENV{'FILE_FINDROOT_DEBUG'} // 0;
      66        
97 25   66     105 $options->{'debug_fh'} //= *STDERR;
98              
99 25 100   12   112 my $debug = ! $options->{'debug'} ? sub {} : sub ($message) { say { $options->{'debug_fh'} } "dir_contains: $message" };
  12         15  
  12         58  
  12         18  
  12         15  
  12         18  
100              
101 25   66 31   171 $options->{'callback'} //= sub ($candidate_dir, $target, $options) { -e File::Spec->catfile($candidate_dir, $target) };
  31         1080  
  31         31  
  31         37  
  31         29  
  31         34  
  31         46  
102 25 100   0   93 unless( ref $options->{'callback'} eq ref sub {} ) {
103 1         172 Carp::carp "callback value is not a subroutine reference";
104 1         10 return;
105             }
106              
107 24   100     115 $options->{'limit'} //= (9**9**9);
108 24 50 66     280 $options->{'start_at'} //= ( $^O eq 'MSWin32' ? Cwd::getdcwd() : Cwd::getcwd() );
109 24         38 my $original = $options->{'start_at'};
110              
111 24         75 $debug->( "before preprocessing, starting at <$original>" );
112 24 100       336 $options->{'start_at'} = File::Spec->rel2abs($options->{'start_at'}) unless File::Spec->file_name_is_absolute($options->{'start_at'});
113 24         1066 $options->{'start_at'} = Cwd::realpath($options->{'start_at'});
114 24 100       78 unless( length $options->{'start_at'} ) {
115 2         426 Carp::carp "Dir <$original> does not exist";
116 2         18 return;
117             }
118              
119 22 100       313 $options->{'start_at'} = File::Basename::dirname($options->{'start_at'}) if ! -d $options->{'start_at'};
120 22         74 $debug->( "after preprocessing, starting at <$options->{'start_at'}>" );
121              
122 22 100       54 if( $options->{'limit'} < 0 ) {
123 1         170 Carp::carp "Initial limit <$options->{'limit'}> was less than zero";
124 1         9 return;
125             }
126              
127 21         28 my $rounds = 0;
128 21         28 my $candidate_dir = $options->{'start_at'};
129 21         65 while( $rounds <= $options->{'limit'} ) {
130 31         106 $debug->( "round <$rounds> - looking in <$candidate_dir> for <$target>" );
131 31 100       60 if( $options->{'callback'}->( $candidate_dir, $target, $options ) ) {
132 20         159 return $candidate_dir;
133             }
134 11         322 my $ancestor = File::Basename::dirname($candidate_dir);
135 11         25 $debug->("ancestor is <$ancestor>");
136 11 100       19 last if $candidate_dir eq $ancestor;
137 10         16 $candidate_dir = $ancestor;
138 10         22 $rounds++;
139             }
140 1         5 $debug->( "stopped at <$candidate_dir> before finding <$target>" );
141              
142 1         9 return;
143             }
144              
145             =back
146              
147             =head1 TO DO
148              
149              
150             =head1 SEE ALSO
151              
152             =over 4
153              
154             =item * L
155              
156             =back
157              
158             =head1 SOURCE AVAILABILITY
159              
160             This source is in Github:
161              
162             http://github.com/briandfoy/file-findroot
163              
164             =head1 AUTHOR
165              
166             brian d foy, C<< >>
167              
168             =head1 COPYRIGHT AND LICENSE
169              
170             Copyright © 2026-2026, brian d foy, All Rights Reserved.
171              
172             You may redistribute this under the terms of the Artistic License 2.0.
173              
174             =cut
175              
176             1;