Development

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

You must first sign up to be able to contribute.

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

Revision 14266, 16.6 kB (checked in by FabianLange, 5 years ago)

[1.0] minor improvement in search_in performance of sfFinder when not searching symlinks. refs #5431

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