AbstractFindAdapter.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.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. namespace Symfony\Component\Finder\Adapter;
  11. use Symfony\Component\Finder\Exception\AccessDeniedException;
  12. use Symfony\Component\Finder\Iterator;
  13. use Symfony\Component\Finder\Shell\Shell;
  14. use Symfony\Component\Finder\Expression\Expression;
  15. use Symfony\Component\Finder\Shell\Command;
  16. use Symfony\Component\Finder\Iterator\SortableIterator;
  17. use Symfony\Component\Finder\Comparator\NumberComparator;
  18. use Symfony\Component\Finder\Comparator\DateComparator;
  19. /**
  20. * Shell engine implementation using GNU find command.
  21. *
  22. * @author Jean-François Simon <contact@jfsimon.fr>
  23. */
  24. abstract class AbstractFindAdapter extends AbstractAdapter
  25. {
  26. /**
  27. * @var Shell
  28. */
  29. protected $shell;
  30. /**
  31. * Constructor.
  32. */
  33. public function __construct()
  34. {
  35. $this->shell = new Shell();
  36. }
  37. /**
  38. * {@inheritdoc}
  39. */
  40. public function searchInDirectory($dir)
  41. {
  42. // having "/../" in path make find fail
  43. $dir = realpath($dir);
  44. // searching directories containing or not containing strings leads to no result
  45. if (Iterator\FileTypeFilterIterator::ONLY_DIRECTORIES === $this->mode && ($this->contains || $this->notContains)) {
  46. return new Iterator\FilePathsIterator(array(), $dir);
  47. }
  48. $command = Command::create();
  49. $find = $this->buildFindCommand($command, $dir);
  50. if ($this->followLinks) {
  51. $find->add('-follow');
  52. }
  53. $find->add('-mindepth')->add($this->minDepth + 1);
  54. if (PHP_INT_MAX !== $this->maxDepth) {
  55. $find->add('-maxdepth')->add($this->maxDepth + 1);
  56. }
  57. if (Iterator\FileTypeFilterIterator::ONLY_DIRECTORIES === $this->mode) {
  58. $find->add('-type d');
  59. } elseif (Iterator\FileTypeFilterIterator::ONLY_FILES === $this->mode) {
  60. $find->add('-type f');
  61. }
  62. $this->buildNamesFiltering($find, $this->names);
  63. $this->buildNamesFiltering($find, $this->notNames, true);
  64. $this->buildPathsFiltering($find, $dir, $this->paths);
  65. $this->buildPathsFiltering($find, $dir, $this->notPaths, true);
  66. $this->buildSizesFiltering($find, $this->sizes);
  67. $this->buildDatesFiltering($find, $this->dates);
  68. $useGrep = $this->shell->testCommand('grep') && $this->shell->testCommand('xargs');
  69. $useSort = is_int($this->sort) && $this->shell->testCommand('sort') && $this->shell->testCommand('cut');
  70. if ($useGrep && ($this->contains || $this->notContains)) {
  71. $grep = $command->ins('grep');
  72. $this->buildContentFiltering($grep, $this->contains);
  73. $this->buildContentFiltering($grep, $this->notContains, true);
  74. }
  75. if ($useSort) {
  76. $this->buildSorting($command, $this->sort);
  77. }
  78. $command->setErrorHandler(
  79. $this->ignoreUnreadableDirs
  80. // If directory is unreadable and finder is set to ignore it, `stderr` is ignored.
  81. ? function ($stderr) { return; }
  82. : function ($stderr) { throw new AccessDeniedException($stderr); }
  83. );
  84. $paths = $this->shell->testCommand('uniq') ? $command->add('| uniq')->execute() : array_unique($command->execute());
  85. $iterator = new Iterator\FilePathsIterator($paths, $dir);
  86. if ($this->exclude) {
  87. $iterator = new Iterator\ExcludeDirectoryFilterIterator($iterator, $this->exclude);
  88. }
  89. if (!$useGrep && ($this->contains || $this->notContains)) {
  90. $iterator = new Iterator\FilecontentFilterIterator($iterator, $this->contains, $this->notContains);
  91. }
  92. if ($this->filters) {
  93. $iterator = new Iterator\CustomFilterIterator($iterator, $this->filters);
  94. }
  95. if (!$useSort && $this->sort) {
  96. $iteratorAggregate = new Iterator\SortableIterator($iterator, $this->sort);
  97. $iterator = $iteratorAggregate->getIterator();
  98. }
  99. return $iterator;
  100. }
  101. /**
  102. * {@inheritdoc}
  103. */
  104. protected function canBeUsed()
  105. {
  106. return $this->shell->testCommand('find');
  107. }
  108. /**
  109. * @param Command $command
  110. * @param string $dir
  111. *
  112. * @return Command
  113. */
  114. protected function buildFindCommand(Command $command, $dir)
  115. {
  116. return $command
  117. ->ins('find')
  118. ->add('find ')
  119. ->arg($dir)
  120. ->add('-noleaf'); // the -noleaf option is required for filesystems that don't follow the '.' and '..' conventions
  121. }
  122. /**
  123. * @param Command $command
  124. * @param string[] $names
  125. * @param Boolean $not
  126. */
  127. private function buildNamesFiltering(Command $command, array $names, $not = false)
  128. {
  129. if (0 === count($names)) {
  130. return;
  131. }
  132. $command->add($not ? '-not' : null)->cmd('(');
  133. foreach ($names as $i => $name) {
  134. $expr = Expression::create($name);
  135. // Find does not support expandable globs ("*.{a,b}" syntax).
  136. if ($expr->isGlob() && $expr->getGlob()->isExpandable()) {
  137. $expr = Expression::create($expr->getGlob()->toRegex(false));
  138. }
  139. // Fixes 'not search' and 'full path matching' regex problems.
  140. // - Jokers '.' are replaced by [^/].
  141. // - We add '[^/]*' before and after regex (if no ^|$ flags are present).
  142. if ($expr->isRegex()) {
  143. $regex = $expr->getRegex();
  144. $regex->prepend($regex->hasStartFlag() ? '/' : '/[^/]*')
  145. ->setStartFlag(false)
  146. ->setStartJoker(true)
  147. ->replaceJokers('[^/]');
  148. if (!$regex->hasEndFlag() || $regex->hasEndJoker()) {
  149. $regex->setEndJoker(false)->append('[^/]*');
  150. }
  151. }
  152. $command
  153. ->add($i > 0 ? '-or' : null)
  154. ->add($expr->isRegex()
  155. ? ($expr->isCaseSensitive() ? '-regex' : '-iregex')
  156. : ($expr->isCaseSensitive() ? '-name' : '-iname')
  157. )
  158. ->arg($expr->renderPattern());
  159. }
  160. $command->cmd(')');
  161. }
  162. /**
  163. * @param Command $command
  164. * @param string $dir
  165. * @param string[] $paths
  166. * @param Boolean $not
  167. */
  168. private function buildPathsFiltering(Command $command, $dir, array $paths, $not = false)
  169. {
  170. if (0 === count($paths)) {
  171. return;
  172. }
  173. $command->add($not ? '-not' : null)->cmd('(');
  174. foreach ($paths as $i => $path) {
  175. $expr = Expression::create($path);
  176. // Find does not support expandable globs ("*.{a,b}" syntax).
  177. if ($expr->isGlob() && $expr->getGlob()->isExpandable()) {
  178. $expr = Expression::create($expr->getGlob()->toRegex(false));
  179. }
  180. // Fixes 'not search' regex problems.
  181. if ($expr->isRegex()) {
  182. $regex = $expr->getRegex();
  183. $regex->prepend($regex->hasStartFlag() ? $dir.DIRECTORY_SEPARATOR : '.*')->setEndJoker(!$regex->hasEndFlag());
  184. } else {
  185. $expr->prepend('*')->append('*');
  186. }
  187. $command
  188. ->add($i > 0 ? '-or' : null)
  189. ->add($expr->isRegex()
  190. ? ($expr->isCaseSensitive() ? '-regex' : '-iregex')
  191. : ($expr->isCaseSensitive() ? '-path' : '-ipath')
  192. )
  193. ->arg($expr->renderPattern());
  194. }
  195. $command->cmd(')');
  196. }
  197. /**
  198. * @param Command $command
  199. * @param NumberComparator[] $sizes
  200. */
  201. private function buildSizesFiltering(Command $command, array $sizes)
  202. {
  203. foreach ($sizes as $i => $size) {
  204. $command->add($i > 0 ? '-and' : null);
  205. switch ($size->getOperator()) {
  206. case '<=':
  207. $command->add('-size -'.($size->getTarget() + 1).'c');
  208. break;
  209. case '>=':
  210. $command->add('-size +'. ($size->getTarget() - 1).'c');
  211. break;
  212. case '>':
  213. $command->add('-size +'.$size->getTarget().'c');
  214. break;
  215. case '!=':
  216. $command->add('-size -'.$size->getTarget().'c');
  217. $command->add('-size +'.$size->getTarget().'c');
  218. case '<':
  219. default:
  220. $command->add('-size -'.$size->getTarget().'c');
  221. }
  222. }
  223. }
  224. /**
  225. * @param Command $command
  226. * @param DateComparator[] $dates
  227. */
  228. private function buildDatesFiltering(Command $command, array $dates)
  229. {
  230. foreach ($dates as $i => $date) {
  231. $command->add($i > 0 ? '-and' : null);
  232. $mins = (int) round((time()-$date->getTarget()) / 60);
  233. if (0 > $mins) {
  234. // mtime is in the future
  235. $command->add(' -mmin -0');
  236. // we will have no result so we don't need to continue
  237. return;
  238. }
  239. switch ($date->getOperator()) {
  240. case '<=':
  241. $command->add('-mmin +'.($mins - 1));
  242. break;
  243. case '>=':
  244. $command->add('-mmin -'.($mins + 1));
  245. break;
  246. case '>':
  247. $command->add('-mmin -'.$mins);
  248. break;
  249. case '!=':
  250. $command->add('-mmin +'.$mins.' -or -mmin -'.$mins);
  251. break;
  252. case '<':
  253. default:
  254. $command->add('-mmin +'.$mins);
  255. }
  256. }
  257. }
  258. /**
  259. * @param Command $command
  260. * @param string $sort
  261. *
  262. * @throws \InvalidArgumentException
  263. */
  264. private function buildSorting(Command $command, $sort)
  265. {
  266. $this->buildFormatSorting($command, $sort);
  267. }
  268. /**
  269. * @param Command $command
  270. * @param string $sort
  271. */
  272. abstract protected function buildFormatSorting(Command $command, $sort);
  273. /**
  274. * @param Command $command
  275. * @param array $contains
  276. * @param Boolean $not
  277. */
  278. abstract protected function buildContentFiltering(Command $command, array $contains, $not = false);
  279. }