Development

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

You must first sign up to be able to contribute.

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

Revision 17857, 18.5 kB (checked in by FabianLange, 6 years ago)

[1.1, 1.2, 1.3] allows overriding the sfFinder returned by a subclass of sfFinder::type method (fixes #6378)

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