4 * This file is part of the Symfony package.
6 * (c) Fabien Potencier <fabien@symfony.com>
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
12 namespace Symfony\Component\Finder\Adapter
;
14 use Symfony\Component\Finder\Exception\AccessDeniedException
;
15 use Symfony\Component\Finder\Iterator
;
16 use Symfony\Component\Finder\Shell\Shell
;
17 use Symfony\Component\Finder\Expression\Expression
;
18 use Symfony\Component\Finder\Shell\Command
;
19 use Symfony\Component\Finder\Comparator\NumberComparator
;
20 use Symfony\Component\Finder\Comparator\DateComparator
;
23 * Shell engine implementation using GNU find command.
25 * @author Jean-François Simon <contact@jfsimon.fr>
27 abstract class AbstractFindAdapter
extends AbstractAdapter
37 public function __construct()
39 $this->shell
= new Shell();
45 public function searchInDirectory($dir)
47 // having "/../" in path make find fail
48 $dir = realpath($dir);
50 // searching directories containing or not containing strings leads to no result
51 if (Iterator\FileTypeFilterIterator
::ONLY_DIRECTORIES
=== $this->mode
&& ($this->contains ||
$this->notContains
)) {
52 return new Iterator\
FilePathsIterator(array(), $dir);
55 $command = Command
::create();
56 $find = $this->buildFindCommand($command, $dir);
58 if ($this->followLinks
) {
59 $find->add('-follow');
62 $find->add('-mindepth')->add($this->minDepth +
1);
64 if (PHP_INT_MAX
!== $this->maxDepth
) {
65 $find->add('-maxdepth')->add($this->maxDepth +
1);
68 if (Iterator\FileTypeFilterIterator
::ONLY_DIRECTORIES
=== $this->mode
) {
69 $find->add('-type d');
70 } elseif (Iterator\FileTypeFilterIterator
::ONLY_FILES
=== $this->mode
) {
71 $find->add('-type f');
74 $this->buildNamesFiltering($find, $this->names
);
75 $this->buildNamesFiltering($find, $this->notNames
, true);
76 $this->buildPathsFiltering($find, $dir, $this->paths
);
77 $this->buildPathsFiltering($find, $dir, $this->notPaths
, true);
78 $this->buildSizesFiltering($find, $this->sizes
);
79 $this->buildDatesFiltering($find, $this->dates
);
81 $useGrep = $this->shell
->testCommand('grep') && $this->shell
->testCommand('xargs');
82 $useSort = is_int($this->sort
) && $this->shell
->testCommand('sort') && $this->shell
->testCommand('cut');
84 if ($useGrep && ($this->contains ||
$this->notContains
)) {
85 $grep = $command->ins('grep');
86 $this->buildContentFiltering($grep, $this->contains
);
87 $this->buildContentFiltering($grep, $this->notContains
, true);
91 $this->buildSorting($command, $this->sort
);
94 $command->setErrorHandler(
95 $this->ignoreUnreadableDirs
96 // If directory is unreadable and finder is set to ignore it, `stderr` is ignored.
97 ?
function ($stderr) { }
98 : function ($stderr) { throw new AccessDeniedException($stderr); }
101 $paths = $this->shell
->testCommand('uniq') ?
$command->add('| uniq')->execute() : array_unique($command->execute());
102 $iterator = new Iterator\
FilePathsIterator($paths, $dir);
104 if ($this->exclude
) {
105 $iterator = new Iterator\
ExcludeDirectoryFilterIterator($iterator, $this->exclude
);
108 if (!$useGrep && ($this->contains ||
$this->notContains
)) {
109 $iterator = new Iterator\
FilecontentFilterIterator($iterator, $this->contains
, $this->notContains
);
112 if ($this->filters
) {
113 $iterator = new Iterator\
CustomFilterIterator($iterator, $this->filters
);
116 if (!$useSort && $this->sort
) {
117 $iteratorAggregate = new Iterator\
SortableIterator($iterator, $this->sort
);
118 $iterator = $iteratorAggregate->getIterator();
127 protected function canBeUsed()
129 return $this->shell
->testCommand('find');
133 * @param Command $command
138 protected function buildFindCommand(Command
$command, $dir)
144 ->add('-noleaf'); // the -noleaf option is required for filesystems that don't follow the '.' and '..' conventions
148 * @param Command $command
149 * @param string[] $names
152 private function buildNamesFiltering(Command
$command, array $names, $not = false)
154 if (0 === count($names)) {
158 $command->add($not ?
'-not' : null)->cmd('(');
160 foreach ($names as $i => $name) {
161 $expr = Expression
::create($name);
163 // Find does not support expandable globs ("*.{a,b}" syntax).
164 if ($expr->isGlob() && $expr->getGlob()->isExpandable()) {
165 $expr = Expression
::create($expr->getGlob()->toRegex(false));
168 // Fixes 'not search' and 'full path matching' regex problems.
169 // - Jokers '.' are replaced by [^/].
170 // - We add '[^/]*' before and after regex (if no ^|$ flags are present).
171 if ($expr->isRegex()) {
172 $regex = $expr->getRegex();
173 $regex->prepend($regex->hasStartFlag() ?
'/' : '/[^/]*')
174 ->setStartFlag(false)
175 ->setStartJoker(true)
176 ->replaceJokers('[^/]');
177 if (!$regex->hasEndFlag() ||
$regex->hasEndJoker()) {
178 $regex->setEndJoker(false)->append('[^/]*');
183 ->add($i > 0 ?
'-or' : null)
184 ->add($expr->isRegex()
185 ?
($expr->isCaseSensitive() ?
'-regex' : '-iregex')
186 : ($expr->isCaseSensitive() ?
'-name' : '-iname')
188 ->arg($expr->renderPattern());
195 * @param Command $command
197 * @param string[] $paths
200 private function buildPathsFiltering(Command
$command, $dir, array $paths, $not = false)
202 if (0 === count($paths)) {
206 $command->add($not ?
'-not' : null)->cmd('(');
208 foreach ($paths as $i => $path) {
209 $expr = Expression
::create($path);
211 // Find does not support expandable globs ("*.{a,b}" syntax).
212 if ($expr->isGlob() && $expr->getGlob()->isExpandable()) {
213 $expr = Expression
::create($expr->getGlob()->toRegex(false));
216 // Fixes 'not search' regex problems.
217 if ($expr->isRegex()) {
218 $regex = $expr->getRegex();
219 $regex->prepend($regex->hasStartFlag() ?
preg_quote($dir).DIRECTORY_SEPARATOR
: '.*')->setEndJoker(!$regex->hasEndFlag());
221 $expr->prepend('*')->append('*');
225 ->add($i > 0 ?
'-or' : null)
226 ->add($expr->isRegex()
227 ?
($expr->isCaseSensitive() ?
'-regex' : '-iregex')
228 : ($expr->isCaseSensitive() ?
'-path' : '-ipath')
230 ->arg($expr->renderPattern());
237 * @param Command $command
238 * @param NumberComparator[] $sizes
240 private function buildSizesFiltering(Command
$command, array $sizes)
242 foreach ($sizes as $i => $size) {
243 $command->add($i > 0 ?
'-and' : null);
245 switch ($size->getOperator()) {
247 $command->add('-size -'.($size->getTarget() +
1).'c');
250 $command->add('-size +'.($size->getTarget() - 1).'c');
253 $command->add('-size +'.$size->getTarget().'c');
256 $command->add('-size -'.$size->getTarget().'c');
257 $command->add('-size +'.$size->getTarget().'c');
261 $command->add('-size -'.$size->getTarget().'c');
267 * @param Command $command
268 * @param DateComparator[] $dates
270 private function buildDatesFiltering(Command
$command, array $dates)
272 foreach ($dates as $i => $date) {
273 $command->add($i > 0 ?
'-and' : null);
275 $mins = (int) round((time() - $date->getTarget()) / 60);
278 // mtime is in the future
279 $command->add(' -mmin -0');
280 // we will have no result so we don't need to continue
284 switch ($date->getOperator()) {
286 $command->add('-mmin +'.($mins - 1));
289 $command->add('-mmin -'.($mins +
1));
292 $command->add('-mmin -'.$mins);
295 $command->add('-mmin +'.$mins.' -or -mmin -'.$mins);
299 $command->add('-mmin +'.$mins);
305 * @param Command $command
306 * @param string $sort
308 * @throws \InvalidArgumentException
310 private function buildSorting(Command
$command, $sort)
312 $this->buildFormatSorting($command, $sort);
316 * @param Command $command
317 * @param string $sort
319 abstract protected function buildFormatSorting(Command
$command, $sort);
322 * @param Command $command
323 * @param array $contains
326 abstract protected function buildContentFiltering(Command
$command, array $contains, $not = false);