1. sfFinder.class.php
  2. /** * * Allow to build rules to find files and directories. * * All rules may be invoked several times, except for ->in() method. * Some rules are cumulative (->name() for example) whereas others are destructive * (most recent value is used, ->maxdepth() method for example). * * All methods return the current sfFinder object to allow easy chaining: * * $files = sfFinder::type('file')->name('*.php')->in(.); * * Interface loosely based on perl File::Find::Rule module. * * @package symfony * @subpackage util * @author Fabien Potencier * @version SVN: $Id: sfFinder.class.php 23744 2009-11-10 00:52:39Z FabianLange $ */
  3. class sfFinder
  4. {
  5. protected $type = 'file';
  6. protected $names = array();
  7. protected $prunes = array();
  8. protected $discards = array();
  9. protected $execs = array();
  10. protected $mindepth = 0;
  11. protected $sizes = array();
  12. protected $maxdepth = 1000000;
  13. protected $relative = false;
  14. protected $follow_link = false;
  15. protected $sort = false;
  16. protected $ignore_version_control = true;
  17. /**
  18. * Sets maximum directory depth.
  19. *
  20. * Finder will descend at most $level levels of directories below the starting point.
  21. *
  22. * @param int $level
  23. * @return object current sfFinder object
  24. */
  25. public function maxdepth($level)
  26. {
  27. $this->maxdepth = $level;
  28. return $this;
  29. }
  30. /**
  31. * Sets minimum directory depth.
  32. *
  33. * Finder will start applying tests at level $level.
  34. *
  35. * @param int $level
  36. * @return object current sfFinder object
  37. */
  38. public function mindepth($level)
  39. {
  40. $this->mindepth = $level;
  41. return $this;
  42. }
  43. public function get_type()
  44. {
  45. return $this->type;
  46. }
  47. /**
  48. * Sets the type of elements to returns.
  49. *
  50. * @param string $name directory or file or any (for both file and directory)
  51. * @return object new sfFinder object
  52. */
  53. public static function type($name)
  54. {
  55. $finder = new self();
  56. return $finder->setType($name);
  57. }
  58. /**
  59. * Sets the type of elements to returns.
  60. *
  61. * @param string $name directory or file or any (for both file and directory)
  62. * @return sfFinder Current object
  63. */
  64. public function setType($name)
  65. {
  66. $name = strtolower($name);
  67. if (substr($name, 0, 3) === 'dir')
  68. {
  69. $this->type = 'directory';
  70. return $this;
  71. }
  72. if ($name === 'any')
  73. {
  74. $this->type = 'any';
  75. return $this;
  76. }
  77. $this->type = 'file';
  78. return $this;
  79. }
  80. /*
  81. * glob, patterns (must be //) or strings
  82. */
  83. protected function to_regex($str)
  84. {
  85. if (preg_match('/^(!)?([^a-zA-Z0-9\\\\]).+?\\2[ims]?$/', $str))
  86. {
  87. return $str;
  88. }
  89. return sfGlobToRegex::glob_to_regex($str);
  90. }
  91. protected function args_to_array($arg_list, $not = false)
  92. {
  93. $list = array();
  94. $nbArgList = count($arg_list);
  95. for ($i = 0; $i < $nbArgList; $i++)
  96. {
  97. if (is_array($arg_list[$i]))
  98. {
  99. foreach ($arg_list[$i] as $arg)
  100. {
  101. $list[] = array($not, $this->to_regex($arg));
  102. }
  103. }
  104. else
  105. {
  106. $list[] = array($not, $this->to_regex($arg_list[$i]));
  107. }
  108. }
  109. return $list;
  110. }
  111. /**
  112. * Adds rules that files must match.
  113. *
  114. * You can use patterns (delimited with / sign), globs or simple strings.
  115. *
  116. * $finder->name('*.php')
  117. * $finder->name('/\.php$/') // same as above
  118. * $finder->name('test.php')
  119. *
  120. * @param list a list of patterns, globs or strings
  121. * @return sfFinder Current object
  122. */
  123. public function name()
  124. {
  125. $args = func_get_args();
  126. $this->names = array_merge($this->names, $this->args_to_array($args));
  127. return $this;
  128. }
  129. /**
  130. * Adds rules that files must not match.
  131. *
  132. * @see ->name()
  133. * @param list a list of patterns, globs or strings
  134. * @return sfFinder Current object
  135. */
  136. public function not_name()
  137. {
  138. $args = func_get_args();
  139. $this->names = array_merge($this->names, $this->args_to_array($args, true));
  140. return $this;
  141. }
  142. /**
  143. * Adds tests for file sizes.
  144. *
  145. * $finder->size('> 10K');
  146. * $finder->size('<= 1Ki');
  147. * $finder->size(4);
  148. *
  149. * @param list a list of comparison strings
  150. * @return sfFinder Current object
  151. */
  152. public function size()
  153. {
  154. $args = func_get_args();
  155. $numargs = count($args);
  156. for ($i = 0; $i < $numargs; $i++)
  157. {
  158. $this->sizes[] = new sfNumberCompare($args[$i]);
  159. }
  160. return $this;
  161. }
  162. /**
  163. * Traverses no further.
  164. *
  165. * @param list a list of patterns, globs to match
  166. * @return sfFinder Current object
  167. */
  168. public function prune()
  169. {
  170. $args = func_get_args();
  171. $this->prunes = array_merge($this->prunes, $this->args_to_array($args));
  172. return $this;
  173. }
  174. /**
  175. * Discards elements that matches.
  176. *
  177. * @param list a list of patterns, globs to match
  178. * @return sfFinder Current object
  179. */
  180. public function discard()
  181. {
  182. $args = func_get_args();
  183. $this->discards = array_merge($this->discards, $this->args_to_array($args));
  184. return $this;
  185. }
  186. /**
  187. * Ignores version control directories.
  188. *
  189. * Currently supports Subversion, CVS, DARCS, Gnu Arch, Monotone, Bazaar-NG, GIT, Mercurial
  190. *
  191. * @param bool $ignore falase when version control directories shall be included (default is true)
  192. *
  193. * @return sfFinder Current object
  194. */
  195. public function ignore_version_control($ignore = true)
  196. {
  197. $this->ignore_version_control = $ignore;
  198. return $this;
  199. }
  200. /**
  201. * Returns files and directories ordered by name
  202. *
  203. * @return sfFinder Current object
  204. */
  205. public function sort_by_name()
  206. {
  207. $this->sort = 'name';
  208. return $this;
  209. }
  210. /**
  211. * Returns files and directories ordered by type (directories before files), then by name
  212. *
  213. * @return sfFinder Current object
  214. */
  215. public function sort_by_type()
  216. {
  217. $this->sort = 'type';
  218. return $this;
  219. }
  220. /**
  221. * Executes function or method for each element.
  222. *
  223. * Element match if functino or method returns true.
  224. *
  225. * $finder->exec('myfunction');
  226. * $finder->exec(array($object, 'mymethod'));
  227. *
  228. * @param mixed function or method to call
  229. * @return sfFinder Current object
  230. */
  231. public function exec()
  232. {
  233. $args = func_get_args();
  234. $numargs = count($args);
  235. for ($i = 0; $i < $numargs; $i++)
  236. {
  237. if (is_array($args[$i]) && !method_exists($args[$i][0], $args[$i][1]))
  238. {
  239. throw new sfException(sprintf('method "%s" does not exist for object "%s".', $args[$i][1], $args[$i][0]));
  240. }
  241. if (!is_array($args[$i]) && !function_exists($args[$i]))
  242. {
  243. throw new sfException(sprintf('function "%s" does not exist.', $args[$i]));
  244. }
  245. $this->execs[] = $args[$i];
  246. }
  247. return $this;
  248. }
  249. /**
  250. * Returns relative paths for all files and directories.
  251. *
  252. * @return sfFinder Current object
  253. */
  254. public function relative()
  255. {
  256. $this->relative = true;
  257. return $this;
  258. }
  259. /**
  260. * Symlink following.
  261. *
  262. * @return sfFinder Current object
  263. */
  264. public function follow_link()
  265. {
  266. $this->follow_link = true;
  267. return $this;
  268. }
  269. /**
  270. * Searches files and directories which match defined rules.
  271. *
  272. * @return array list of files and directories
  273. */
  274. public function in()
  275. {
  276. $files = array();
  277. $here_dir = getcwd();
  278. $finder = clone $this;
  279. if ($this->ignore_version_control)
  280. {
  281. $ignores = array('.svn', '_svn', 'CVS', '_darcs', '.arch-params', '.monotone', '.bzr', '.git', '.hg');
  282. $finder->discard($ignores)->prune($ignores);
  283. }
  284. // first argument is an array?
  285. $numargs = func_num_args();
  286. $arg_list = func_get_args();
  287. if ($numargs === 1 && is_array($arg_list[0]))
  288. {
  289. $arg_list = $arg_list[0];
  290. $numargs = count($arg_list);
  291. }
  292. for ($i = 0; $i < $numargs; $i++)
  293. {
  294. $dir = realpath($arg_list[$i]);
  295. if (!is_dir($dir))
  296. {
  297. continue;
  298. }
  299. $dir = str_replace('\\', '/', $dir);
  300. // absolute path?
  301. if (!self::isPathAbsolute($dir))
  302. {
  303. $dir = $here_dir.'/'.$dir;
  304. }
  305. $new_files = str_replace('\\', '/', $finder->search_in($dir));
  306. if ($this->relative)
  307. {
  308. $new_files = str_replace(rtrim($dir, '/').'/', '', $new_files);
  309. }
  310. $files = array_merge($files, $new_files);
  311. }
  312. if ($this->sort === 'name')
  313. {
  314. sort($files);
  315. }
  316. return array_unique($files);
  317. }
  318. protected function search_in($dir, $depth = 0)
  319. {
  320. if ($depth > $this->maxdepth)
  321. {
  322. return array();
  323. }
  324. $dir = realpath($dir);
  325. if ((!$this->follow_link) && is_link($dir))
  326. {
  327. return array();
  328. }
  329. $files = array();
  330. $temp_files = array();
  331. $temp_folders = array();
  332. if (is_dir($dir))
  333. {
  334. $current_dir = opendir($dir);
  335. while (false !== $entryname = readdir($current_dir))
  336. {
  337. if ($entryname == '.' || $entryname == '..') continue;
  338. $current_entry = $dir.DIRECTORY_SEPARATOR.$entryname;
  339. if ((!$this->follow_link) && is_link($current_entry))
  340. {
  341. continue;
  342. }
  343. if (is_dir($current_entry))
  344. {
  345. if ($this->sort === 'type')
  346. {
  347. $temp_folders[$entryname] = $current_entry;
  348. }
  349. else
  350. {
  351. 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))
  352. {
  353. $files[] = $current_entry;
  354. }
  355. if (!$this->is_pruned($dir, $entryname))
  356. {
  357. $files = array_merge($files, $this->search_in($current_entry, $depth + 1));
  358. }
  359. }
  360. }
  361. else
  362. {
  363. 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))
  364. {
  365. if ($this->sort === 'type')
  366. {
  367. $temp_files[] = $current_entry;
  368. }
  369. else
  370. {
  371. $files[] = $current_entry;
  372. }
  373. }
  374. }
  375. }
  376. if ($this->sort === 'type')
  377. {
  378. ksort($temp_folders);
  379. foreach($temp_folders as $entryname => $current_entry)
  380. {
  381. 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))
  382. {
  383. $files[] = $current_entry;
  384. }
  385. if (!$this->is_pruned($dir, $entryname))
  386. {
  387. $files = array_merge($files, $this->search_in($current_entry, $depth + 1));
  388. }
  389. }
  390. sort($temp_files);
  391. $files = array_merge($files, $temp_files);
  392. }
  393. closedir($current_dir);
  394. }
  395. return $files;
  396. }
  397. protected function match_names($dir, $entry)
  398. {
  399. if (!count($this->names)) return true;
  400. // Flags indicating that there was attempts to match
  401. // at least one "not_name" or "name" rule respectively
  402. // to following variables:
  403. $one_not_name_rule = false;
  404. $one_name_rule = false;
  405. foreach ($this->names as $args)
  406. {
  407. list($not, $regex) = $args;
  408. $not ? $one_not_name_rule = true : $one_name_rule = true;
  409. if (preg_match($regex, $entry))
  410. {
  411. // We must match ONLY ONE "not_name" or "name" rule:
  412. // if "not_name" rule matched then we return "false"
  413. // if "name" rule matched then we return "true"
  414. return $not ? false : true;
  415. }
  416. }
  417. if ($one_not_name_rule && $one_name_rule)
  418. {
  419. return false;
  420. }
  421. else if ($one_not_name_rule)
  422. {
  423. return true;
  424. }
  425. else if ($one_name_rule)
  426. {
  427. return false;
  428. }
  429. return true;
  430. }
  431. protected function size_ok($dir, $entry)
  432. {
  433. if (0 === count($this->sizes)) return true;
  434. if (!is_file($dir.DIRECTORY_SEPARATOR.$entry)) return true;
  435. $filesize = filesize($dir.DIRECTORY_SEPARATOR.$entry);
  436. foreach ($this->sizes as $number_compare)
  437. {
  438. if (!$number_compare->test($filesize)) return false;
  439. }
  440. return true;
  441. }
  442. protected function is_pruned($dir, $entry)
  443. {
  444. if (0 === count($this->prunes)) return false;
  445. foreach ($this->prunes as $args)
  446. {
  447. $regex = $args[1];
  448. if (preg_match($regex, $entry)) return true;
  449. }
  450. return false;
  451. }
  452. protected function is_discarded($dir, $entry)
  453. {
  454. if (0 === count($this->discards)) return false;
  455. foreach ($this->discards as $args)
  456. {
  457. $regex = $args[1];
  458. if (preg_match($regex, $entry)) return true;
  459. }
  460. return false;
  461. }
  462. protected function exec_ok($dir, $entry)
  463. {
  464. if (0 === count($this->execs)) return true;
  465. foreach ($this->execs as $exec)
  466. {
  467. if (!call_user_func_array($exec, array($dir, $entry))) return false;
  468. }
  469. return true;
  470. }
  471. public static function isPathAbsolute($path)
  472. {
  473. if ($path{0} === '/' || $path{0} === '\\' ||
  474. (strlen($path) > 3 && ctype_alpha($path{0}) &&
  475. $path{1} === ':' &&
  476. ($path{2} === '\\' || $path{2} === '/')
  477. )
  478. )
  479. {
  480. return true;
  481. }
  482. return false;
  483. }
  484. }

Debug toolbar