Development

/branches/1.3/lib/util/sfFinder.class.php

You must first sign up to be able to contribute.

root/branches/1.3/lib/util/sfFinder.class.php

Revision 30528, 18.7 kB (checked in by fabien, 4 years ago)

[1.3, 1.4] fixed phpdoc (closes #8874)

  • Property svn:mime-type set to text/x-php
  • Property svn:eol-style set to native
  • Property svn:keywords set to Id
Line 
1 <?php
2
3 /*
4  * This file is part of the symfony package.
5  * (c) 2004-2006 Fabien Potencier <fabien.potencier@symfony-project.com>
6  *
7  * For the full copyright and license information, please view the LICENSE
8  * file that was distributed with this source code.
9  */
10
11
12 /**
13  *
14  * Allow to build rules to find files and directories.
15  *
16  * All rules may be invoked several times, except for ->in() method.
17  * Some rules are cumulative (->name() for example) whereas others are destructive
18  * (most recent value is used, ->maxdepth() method for example).
19  *
20  * All methods return the current sfFinder object to allow easy chaining:
21  *
22  * $files = sfFinder::type('file')->name('*.php')->in(.);
23  *
24  * Interface loosely based on perl File::Find::Rule module.
25  *
26  * @package    symfony
27  * @subpackage util
28  * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
29  * @version    SVN: $Id$
30  */
31 class sfFinder
32 {
33   protected $type                   = 'file';
34   protected $names                  = array();
35   protected $prunes                 = array();
36   protected $discards               = array();
37   protected $execs                  = array();
38   protected $mindepth               = 0;
39   protected $sizes                  = array();
40   protected $maxdepth               = 1000000;
41   protected $relative               = false;
42   protected $follow_link            = false;
43   protected $sort                   = false;
44   protected $ignore_version_control = true;
45
46   /**
47    * Sets maximum directory depth.
48    *
49    * Finder will descend at most $level levels of directories below the starting point.
50    *
51    * @param  int $level
52    * @return sfFinder current sfFinder object
53    */
54   public function maxdepth($level)
55   {
56     $this->maxdepth = $level;
57
58     return $this;
59   }
60
61   /**
62    * Sets minimum directory depth.
63    *
64    * Finder will start applying tests at level $level.
65    *
66    * @param  int $level
67    * @return sfFinder current sfFinder object
68    */
69   public function mindepth($level)
70   {
71     $this->mindepth = $level;
72
73     return $this;
74   }
75
76   public function get_type()
77   {
78     return $this->type;
79   }
80
81   /**
82    * Sets the type of elements to returns.
83    *
84    * @param  string $name  directory or file or any (for both file and directory)
85    * @return sfFinder new sfFinder object
86    */
87   public static function type($name)
88   {
89     $finder = new self();
90     return $finder->setType($name);
91   }
92   /**
93    * Sets the type of elements to returns.
94    *
95    * @param  string $name  directory or file or any (for both file and directory)
96    * @return sfFinder Current object
97    */
98   public function setType($name)
99   {
100     $name = strtolower($name);
101
102     if (substr($name, 0, 3) === 'dir')
103     {
104       $this->type = 'directory';
105
106       return $this;
107     }
108     if ($name === 'any')
109     {
110       $this->type = 'any';
111
112       return $this;
113     }
114
115       $this->type = 'file';
116
117     return $this;
118   }
119
120   /*
121    * glob, patterns (must be //) or strings
122    */
123   protected function to_regex($str)
124   {
125     if (preg_match('/^(!)?([^a-zA-Z0-9\\\\]).+?\\2[ims]?$/', $str))
126     {
127       return $str;
128     }
129
130     return sfGlobToRegex::glob_to_regex($str);
131   }
132
133   protected function args_to_array($arg_list, $not = false)
134   {
135     $list = array();
136     $nbArgList = count($arg_list);
137     for ($i = 0; $i < $nbArgList; $i++)
138     {
139       if (is_array($arg_list[$i]))
140       {
141         foreach ($arg_list[$i] as $arg)
142         {
143           $list[] = array($not, $this->to_regex($arg));
144         }
145       }
146       else
147       {
148         $list[] = array($not, $this->to_regex($arg_list[$i]));
149       }
150     }
151
152     return $list;
153   }
154
155   /**
156    * Adds rules that files must match.
157    *
158    * You can use patterns (delimited with / sign), globs or simple strings.
159    *
160    * $finder->name('*.php')
161    * $finder->name('/\.php$/') // same as above
162    * $finder->name('test.php')
163    *
164    * @param  list   a list of patterns, globs or strings
165    * @return sfFinder Current object
166    */
167   public function name()
168   {
169     $args = func_get_args();
170     $this->names = array_merge($this->names, $this->args_to_array($args));
171
172     return $this;
173   }
174
175   /**
176    * Adds rules that files must not match.
177    *
178    * @see    ->name()
179    * @param  list   a list of patterns, globs or strings
180    * @return sfFinder Current object
181    */
182   public function not_name()
183   {
184     $args = func_get_args();
185     $this->names = array_merge($this->names, $this->args_to_array($args, true));
186
187     return $this;
188   }
189
190   /**
191    * Adds tests for file sizes.
192    *
193    * $finder->size('> 10K');
194    * $finder->size('<= 1Ki');
195    * $finder->size(4);
196    *
197    * @param  list   a list of comparison strings
198    * @return sfFinder Current object
199    */
200   public function size()
201   {
202     $args = func_get_args();
203     $numargs = count($args);
204     for ($i = 0; $i < $numargs; $i++)
205     {
206       $this->sizes[] = new sfNumberCompare($args[$i]);
207     }
208
209     return $this;
210   }
211
212   /**
213    * Traverses no further.
214    *
215    * @param  list   a list of patterns, globs to match
216    * @return sfFinder Current object
217    */
218   public function prune()
219   {
220     $args = func_get_args();
221     $this->prunes = array_merge($this->prunes, $this->args_to_array($args));
222
223     return $this;
224   }
225
226   /**
227    * Discards elements that matches.
228    *
229    * @param  list   a list of patterns, globs to match
230    * @return sfFinder Current object
231    */
232   public function discard()
233   {
234     $args = func_get_args();
235     $this->discards = array_merge($this->discards, $this->args_to_array($args));
236
237     return $this;
238   }
239
240   /**
241    * Ignores version control directories.
242    *
243    * Currently supports Subversion, CVS, DARCS, Gnu Arch, Monotone, Bazaar-NG, GIT, Mercurial
244    *
245    * @param  bool   $ignore  falase when version control directories shall be included (default is true)
246    *
247    * @return sfFinder Current object
248    */
249   public function ignore_version_control($ignore = true)
250   {
251     $this->ignore_version_control = $ignore;
252
253     return $this;
254   }
255
256   /**
257    * Returns files and directories ordered by name
258    *
259    * @return sfFinder Current object
260    */
261   public function sort_by_name()
262   {
263     $this->sort = 'name';
264
265     return $this;
266   }
267
268   /**
269    * Returns files and directories ordered by type (directories before files), then by name
270    *
271    * @return sfFinder Current object
272    */
273   public function sort_by_type()
274   {
275     $this->sort = 'type';
276
277     return $this;
278   }
279
280   /**
281    * Executes function or method for each element.
282    *
283    * Element match if functino or method returns true.
284    *
285    * $finder->exec('myfunction');
286    * $finder->exec(array($object, 'mymethod'));
287    *
288    * @param  mixed  function or method to call
289    * @return sfFinder Current object
290    */
291   public function exec()
292   {
293     $args = func_get_args();
294     $numargs = count($args);
295     for ($i = 0; $i < $numargs; $i++)
296     {
297       if (is_array($args[$i]) && !method_exists($args[$i][0], $args[$i][1]))
298       {
299         throw new sfException(sprintf('method "%s" does not exist for object "%s".', $args[$i][1], $args[$i][0]));
300       }
301       if (!is_array($args[$i]) && !function_exists($args[$i]))
302       {
303         throw new sfException(sprintf('function "%s" does not exist.', $args[$i]));
304       }
305
306       $this->execs[] = $args[$i];
307     }
308
309     return $this;
310   }
311
312   /**
313    * Returns relative paths for all files and directories.
314    *
315    * @return sfFinder Current object
316    */
317   public function relative()
318   {
319     $this->relative = true;
320
321     return $this;
322   }
323
324   /**
325    * Symlink following.
326    *
327    * @return sfFinder Current object
328    */
329   public function follow_link()
330   {
331     $this->follow_link = true;
332
333     return $this;
334   }
335
336   /**
337    * Searches files and directories which match defined rules.
338    *
339    * @return array list of files and directories
340    */
341   public function in()
342   {
343     $files    = array();
344     $here_dir = getcwd();
345
346     $finder = clone $this;
347
348     if ($this->ignore_version_control)
349     {
350       $ignores = array('.svn', '_svn', 'CVS', '_darcs', '.arch-params', '.monotone', '.bzr', '.git', '.hg');
351
352       $finder->discard($ignores)->prune($ignores);
353     }
354
355     // first argument is an array?
356     $numargs  = func_num_args();
357     $arg_list = func_get_args();
358     if ($numargs === 1 && is_array($arg_list[0]))
359     {
360       $arg_list = $arg_list[0];
361       $numargs  = count($arg_list);
362     }
363
364     for ($i = 0; $i < $numargs; $i++)
365     {
366       $dir = realpath($arg_list[$i]);
367
368       if (!is_dir($dir))
369       {
370         continue;
371       }
372
373       $dir = str_replace('\\', '/', $dir);
374
375       // absolute path?
376       if (!self::isPathAbsolute($dir))
377       {
378         $dir = $here_dir.'/'.$dir;
379       }
380
381       $new_files = str_replace('\\', '/', $finder->search_in($dir));
382
383       if ($this->relative)
384       {
385         $new_files = str_replace(rtrim($dir, '/').'/', '', $new_files);
386       }
387
388       $files = array_merge($files, $new_files);
389     }
390
391     if ($this->sort === 'name')
392     {
393       sort($files);
394     }
395
396     return array_unique($files);
397   }
398
399   protected function search_in($dir, $depth = 0)
400   {
401     if ($depth > $this->maxdepth)
402     {
403       return array();
404     }
405
406     $dir = realpath($dir);
407
408     if ((!$this->follow_link) && is_link($dir))
409     {
410       return array();
411     }
412
413     $files = array();
414     $temp_files = array();
415     $temp_folders = array();
416     if (is_dir($dir) && is_readable($dir))
417     {
418       $current_dir = opendir($dir);
419       while (false !== $entryname = readdir($current_dir))
420       {
421         if ($entryname == '.' || $entryname == '..') continue;
422
423         $current_entry = $dir.DIRECTORY_SEPARATOR.$entryname;
424         if ((!$this->follow_link) && is_link($current_entry))
425         {
426           continue;
427         }
428
429         if (is_dir($current_entry))
430         {
431           if ($this->sort === 'type')
432           {
433             $temp_folders[$entryname] = $current_entry;
434           }
435           else
436           {
437             if (($this->type === 'directory' || $this->type === 'any') && ($depth >= $this->mindepth) && !$this->is_discarded($dir, $entryname) && $this->match_names($dir, $entryname) && $this->exec_ok($dir, $entryname))
438             {
439               $files[] = $current_entry;
440             }
441
442             if (!$this->is_pruned($dir, $entryname))
443             {
444               $files = array_merge($files, $this->search_in($current_entry, $depth + 1));
445             }
446           }
447         }
448         else
449         {
450           if (($this->type !== 'directory' || $this->type === 'any') && ($depth >= $this->mindepth) && !$this->is_discarded($dir, $entryname) && $this->match_names($dir, $entryname) && $this->size_ok($dir, $entryname) && $this->exec_ok($dir, $entryname))
451           {
452             if ($this->sort === 'type')
453             {
454               $temp_files[] = $current_entry;
455             }
456             else
457             {
458               $files[] = $current_entry;
459             }
460           }
461         }
462       }
463
464       if ($this->sort === 'type')
465       {
466         ksort($temp_folders);
467         foreach($temp_folders as $entryname => $current_entry)
468         {
469           if (($this->type === 'directory' || $this->type === 'any') && ($depth >= $this->mindepth) && !$this->is_discarded($dir, $entryname) && $this->match_names($dir, $entryname) && $this->exec_ok($dir, $entryname))
470           {
471             $files[] = $current_entry;
472           }
473
474           if (!$this->is_pruned($dir, $entryname))
475           {
476             $files = array_merge($files, $this->search_in($current_entry, $depth + 1));
477           }
478         }
479
480         sort($temp_files);
481         $files = array_merge($files, $temp_files);
482       }
483
484       closedir($current_dir);
485     }
486
487     return $files;
488   }
489
490   protected function match_names($dir, $entry)
491   {
492     if (!count($this->names)) return true;
493
494     // Flags indicating that there was attempts to match
495     // at least one "not_name" or "name" rule respectively
496     // to following variables:
497     $one_not_name_rule = false;
498     $one_name_rule = false;
499
500     foreach ($this->names as $args)
501     {
502       list($not, $regex) = $args;
503       $not ? $one_not_name_rule = true : $one_name_rule = true;
504       if (preg_match($regex, $entry))
505       {
506         // We must match ONLY ONE "not_name" or "name" rule:
507         // if "not_name" rule matched then we return "false"
508         // if "name" rule matched then we return "true"
509         return $not ? false : true;
510       }
511     }
512
513     if ($one_not_name_rule && $one_name_rule)
514     {
515       return false;
516     }
517     else if ($one_not_name_rule)
518     {
519       return true;
520     }
521     else if ($one_name_rule)
522     {
523       return false;
524     }
525     return true;
526   }
527
528   protected function size_ok($dir, $entry)
529   {
530     if (0 === count($this->sizes)) return true;
531
532     if (!is_file($dir.DIRECTORY_SEPARATOR.$entry)) return true;
533
534     $filesize = filesize($dir.DIRECTORY_SEPARATOR.$entry);
535     foreach ($this->sizes as $number_compare)
536     {
537       if (!$number_compare->test($filesize)) return false;
538     }
539
540     return true;
541   }
542
543   protected function is_pruned($dir, $entry)
544   {
545     if (0 === count($this->prunes)) return false;
546
547     foreach ($this->prunes as $args)
548     {
549       $regex = $args[1];
550       if (preg_match($regex, $entry)) return true;
551     }
552
553     return false;
554   }
555
556   protected function is_discarded($dir, $entry)
557   {
558     if (0 === count($this->discards)) return false;
559
560     foreach ($this->discards as $args)
561     {
562       $regex = $args[1];
563       if (preg_match($regex, $entry)) return true;
564     }
565
566     return false;
567   }
568
569   protected function exec_ok($dir, $entry)
570   {
571     if (0 === count($this->execs)) return true;
572
573     foreach ($this->execs as $exec)
574     {
575       if (!call_user_func_array($exec, array($dir, $entry))) return false;
576     }
577
578     return true;
579   }
580
581   public static function isPathAbsolute($path)
582   {
583     if ($path{0} === '/' || $path{0} === '\\' ||
584         (strlen($path) > 3 && ctype_alpha($path{0}) &&
585          $path{1} === ':' &&
586          ($path{2} === '\\' || $path{2} === '/')
587         )
588        )
589     {
590       return true;
591     }
592
593     return false;
594   }
595 }
596
597 /**
598  * Match globbing patterns against text.
599  *
600  *   if match_glob("foo.*", "foo.bar") echo "matched\n";
601  *
602  * // prints foo.bar and foo.baz
603  * $regex = glob_to_regex("foo.*");
604  * for (array('foo.bar', 'foo.baz', 'foo', 'bar') as $t)
605  * {
606  *   if (/$regex/) echo "matched: $car\n";
607  * }
608  *
609  * sfGlobToRegex implements glob(3) style matching that can be used to match
610  * against text, rather than fetching names from a filesystem.
611  *
612  * based on perl Text::Glob module.
613  *
614  * @package    symfony
615  * @subpackage util
616  * @author     Fabien Potencier <fabien.potencier@gmail.com> php port
617  * @author     Richard Clamp <richardc@unixbeard.net> perl version
618  * @copyright  2004-2005 Fabien Potencier <fabien.potencier@gmail.com>
619  * @copyright  2002 Richard Clamp <richardc@unixbeard.net>
620  * @version    SVN: $Id$
621  */
622 class sfGlobToRegex
623 {
624   protected static $strict_leading_dot = true;
625   protected static $strict_wildcard_slash = true;
626
627   public static function setStrictLeadingDot($boolean)
628   {
629     self::$strict_leading_dot = $boolean;
630   }
631
632   public static function setStrictWildcardSlash($boolean)
633   {
634     self::$strict_wildcard_slash = $boolean;
635   }
636
637   /**
638    * Returns a compiled regex which is the equiavlent of the globbing pattern.
639    *
640    * @param  string $glob  pattern
641    * @return string regex
642    */
643   public static function glob_to_regex($glob)
644   {
645     $first_byte = true;
646     $escaping = false;
647     $in_curlies = 0;
648     $regex = '';
649     $sizeGlob = strlen($glob);
650     for ($i = 0; $i < $sizeGlob; $i++)
651     {
652       $car = $glob[$i];
653       if ($first_byte)
654       {
655         if (self::$strict_leading_dot && $car !== '.')
656         {
657           $regex .= '(?=[^\.])';
658         }
659
660         $first_byte = false;
661       }
662
663       if ($car === '/')
664       {
665         $first_byte = true;
666       }
667
668       if ($car === '.' || $car === '(' || $car === ')' || $car === '|' || $car === '+' || $car === '^' || $car === '$')
669       {
670         $regex .= "\\$car";
671       }
672       elseif ($car === '*')
673       {
674         $regex .= ($escaping ? '\\*' : (self::$strict_wildcard_slash ? '[^/]*' : '.*'));
675       }
676       elseif ($car === '?')
677       {
678         $regex .= ($escaping ? '\\?' : (self::$strict_wildcard_slash ? '[^/]' : '.'));
679       }
680       elseif ($car === '{')
681       {
682         $regex .= ($escaping ? '\\{' : '(');
683         if (!$escaping) ++$in_curlies;
684       }
685       elseif ($car === '}' && $in_curlies)
686       {
687         $regex .= ($escaping ? '}' : ')');
688         if (!$escaping) --$in_curlies;
689       }
690       elseif ($car === ',' && $in_curlies)
691       {
692         $regex .= ($escaping ? ',' : '|');
693       }
694       elseif ($car === '\\')
695       {
696         if ($escaping)
697         {
698           $regex .= '\\\\';
699           $escaping = false;
700         }
701         else
702         {
703           $escaping = true;
704         }
705
706         continue;
707       }
708       else
709       {
710         $regex .= $car;
711       }
712       $escaping = false;
713     }
714
715     return '#^'.$regex.'$#';
716   }
717 }
718
719 /**
720  * Numeric comparisons.
721  *
722  * sfNumberCompare compiles a simple comparison to an anonymous
723  * subroutine, which you can call with a value to be tested again.
724
725  * Now this would be very pointless, if sfNumberCompare didn't understand
726  * magnitudes.
727
728  * The target value may use magnitudes of kilobytes (k, ki),
729  * megabytes (m, mi), or gigabytes (g, gi).  Those suffixed
730  * with an i use the appropriate 2**n version in accordance with the
731  * IEC standard: http://physics.nist.gov/cuu/Units/binary.html
732  *
733  * based on perl Number::Compare module.
734  *
735  * @package    symfony
736  * @subpackage util
737  * @author     Fabien Potencier <fabien.potencier@gmail.com> php port
738  * @author     Richard Clamp <richardc@unixbeard.net> perl version
739  * @copyright  2004-2005 Fabien Potencier <fabien.potencier@gmail.com>
740  * @copyright  2002 Richard Clamp <richardc@unixbeard.net>
741  * @see        http://physics.nist.gov/cuu/Units/binary.html
742  * @version    SVN: $Id$
743  */
744 class sfNumberCompare
745 {
746   protected $test = '';
747
748   public function __construct($test)
749   {
750     $this->test = $test;
751   }
752
753   public function test($number)
754   {
755     if (!preg_match('{^([<>]=?)?(.*?)([kmg]i?)?$}i', $this->test, $matches))
756     {
757       throw new sfException(sprintf('don\'t understand "%s" as a test.', $this->test));
758     }
759
760     $target = array_key_exists(2, $matches) ? $matches[2] : '';
761     $magnitude = array_key_exists(3, $matches) ? $matches[3] : '';
762     if (strtolower($magnitude) === 'k'$target *=           1000;
763     if (strtolower($magnitude) === 'ki') $target *=           1024;
764     if (strtolower($magnitude) === 'm'$target *=        1000000;
765     if (strtolower($magnitude) === 'mi') $target *=      1024*1024;
766     if (strtolower($magnitude) === 'g'$target *=     1000000000;
767     if (strtolower($magnitude) === 'gi') $target *= 1024*1024*1024;
768
769     $comparison = array_key_exists(1, $matches) ? $matches[1] : '==';
770     if ($comparison === '==' || $comparison == '')
771     {
772       return ($number == $target);
773     }
774     if ($comparison === '>')
775     {
776       return ($number > $target);
777     }
778     if ($comparison === '>=')
779     {
780       return ($number >= $target);
781     }
782     if ($comparison === '<')
783     {
784       return ($number < $target);
785     }
786     if ($comparison === '<=')
787     {
788       return ($number <= $target);
789     }
790
791     return false;
792   }
793 }
794
Note: See TracBrowser for help on using the browser.