sfFinder.class.php 19 KB

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