Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

merged branch jfsimon/issue-4031 (PR #4061)

This PR was squashed before being merged into the master branch (closes #4061).

Commits
-------

32bb754 [2.2] [WIP] [Finder] Adding native finders implementations

Discussion
----------

[2.2] [WIP] [Finder] Adding native finders implementations

Work in progress...

Bug fix: no
Feature addition: no
Backwards compatibility break: no
Symfony2 tests pass: yes
Fixes the following tickets: #4031

This PR intends to add native finders implementation based on shell command execution.
Planned support concerns:
- GNU `find` command -> done
- MS `FINDSTR` command

---------------------------------------------------------------------------

by fabpot at 2012-05-15T06:19:50Z

@jfsimon What's the status of this PR?

---------------------------------------------------------------------------

by jfsimon at 2012-05-15T06:43:34Z

@fabpot 2 features missing for the GNU find adapter: sorting result with `sort` command and excluding directories; 1 bug (even if tests pass, which let me thing it needs more tests): regex matching is done on full path, not basename. Then I'll need to work on MS `FINDSTR` command adapter (I talked to Pierre Couzy, and he's OK to help when he will have time to). I'll try to push the sort and directory excluding features this week.

---------------------------------------------------------------------------

by jalliot at 2012-05-15T09:51:20Z

BTW @jfsimon, in the (quite specific) case where you don't precise filenames or other options but only `contains` or `notContains`, you could call `grep` directly without the `find`. That would speed things up a bit more :)

---------------------------------------------------------------------------

by fabpot at 2012-06-28T15:20:55Z

@jfsimon Would be nice to be able to include this PR before 2.1.0 beta2. Would you have time to finish the work soon?

---------------------------------------------------------------------------

by jfsimon at 2012-06-29T11:07:19Z

@fabpot I'd say next week for GNU part with some help from @smaftoul.

---------------------------------------------------------------------------

by jfsimon at 2012-07-10T08:20:44Z

It seems that I need to perform some benchmarks as find may not be so fast :/

---------------------------------------------------------------------------

by jfsimon at 2012-07-10T16:51:19Z

@fabpot @stof do you think I can add benchmark scripts inside the component, or should I create a new repository for that?

---------------------------------------------------------------------------

by fabpot at 2012-07-10T16:57:05Z

Then benchmark scripts won't be part of the repository in the end, so you should create a new repo for that.

---------------------------------------------------------------------------

by jfsimon at 2012-07-13T17:57:03Z

@fabpot @smaftoul Benchmark is ready (more cases to come): https://github.com/jfsimon/symfony-finder-benchmark
I'm glad to see that `gnu_find` adapter is really faster than the `php` one!

---------------------------------------------------------------------------

by stof at 2012-07-13T19:17:20Z

@jfsimon could you make a gist with the result of the benchmark ? I think many people will be lazy to run it themselves when looking at this ticket, and people using windows will probably be unable to run it at all :)

---------------------------------------------------------------------------

by jfsimon at 2012-07-13T21:37:50Z

First results: https://gist.github.com/3107676

---------------------------------------------------------------------------

by jfsimon at 2012-08-01T07:26:21Z

Sorry, I forgot `[Finder]` tag in 3 commits message... is it fixable?

---------------------------------------------------------------------------

by stof at 2012-08-01T08:58:28Z

@jfsimon you can edit the commit message whne doing an interactive rebase.

and btw, you will need to do a rebase anyway: the PR conflicts with master

---------------------------------------------------------------------------

by jfsimon at 2012-08-01T13:11:20Z

@stof Okay, I rebased origin/master. As you can see, above comments are now floating in the air :/

Strangely, rebase broke my tests... I need to fix them :(

---------------------------------------------------------------------------

by stof at 2012-08-01T13:14:11Z

Weird. github still tells me that the PR cannot be merged. Did you fetch the latest master before rebasing ?

---------------------------------------------------------------------------

by jfsimon at 2012-08-01T13:19:25Z

Weird, git fetch only fetched my own repository, I had to `git fetch origin`. I'm rebasing... again.

---------------------------------------------------------------------------

by jfsimon at 2012-08-01T14:50:02Z

@stof Rebase done, tests fixed :)

---------------------------------------------------------------------------

by stof at 2012-08-01T15:18:19Z

hmm, Travis does not seems to agree with the second statement :)

---------------------------------------------------------------------------

by jfsimon at 2012-08-01T17:33:55Z

Ouch, I'm really sorry, I was in the wrong tmux window when started tests :/
Good news, I have to fix my last problem (the regex tested against full path instead of basename) to fix the tests.
I'm on it.

---------------------------------------------------------------------------

by jfsimon at 2012-08-01T18:16:10Z

Grrr...  I didnt start full test suite, shame on me.

---------------------------------------------------------------------------

by jfsimon at 2012-08-01T19:10:02Z

Same bench than before, but with non empty files: https://gist.github.com/3229865

---------------------------------------------------------------------------

by jfsimon at 2012-08-01T19:23:32Z

It seems that searching files by their name with regex is really fatser than by glob with find: https://gist.github.com/3229911
@fabpot should I convert glob to regex when using `contains` and `notContains`?

---------------------------------------------------------------------------

by jfsimon at 2012-08-01T19:55:02Z

It seems that I'm an idiot, I used `contains` instead of `name`.
Real bench is here: https://gist.github.com/3230139
@fabpot sorry for the mess, I should go to bed :/
Results are still convincing!

---------------------------------------------------------------------------

by stof at 2012-08-01T20:04:42Z

They are, but the regex are not faster than glob anymore in these results

---------------------------------------------------------------------------

by jfsimon at 2012-08-01T21:17:25Z

@travisbot you failed, not me!

---------------------------------------------------------------------------

by jfsimon at 2012-08-01T21:18:28Z

Anyone to launch benchmark with php 5.4?
https://github.com/jfsimon/symfony-finder-benchmark

---------------------------------------------------------------------------

by lyrixx at 2012-08-01T22:25:08Z

Bench with php 5.4.5
https://gist.github.com/3231244
  • Loading branch information...
commit bcddd0e7463901f4b08e57d9532f8d23212591b1 2 parents 86f7d67 + 5259120
Fabien Potencier fabpot authored
Showing with 2,764 additions and 189 deletions.
  1. +174 −0 Adapter/AbstractAdapter.php
  2. +123 −0 Adapter/AdapterInterface.php
  3. +312 −0 Adapter/GnuFindAdapter.php
  4. +94 −0 Adapter/PhpAdapter.php
  5. +46 −0 Exception/AdapterFailureException.php
  6. +14 −0 Exception/ExceptionInterface.php
  7. +19 −0 Exception/OperationNotPermitedException.php
  8. +45 −0 Exception/ShellCommandFailureException.php
  9. +112 −0 Expression/Expression.php
  10. +126 −0 Expression/Glob.php
  11. +315 −0 Expression/Regex.php
  12. +46 −0 Expression/ValueInterface.php
  13. +100 −44 Finder.php
  14. +3 −23 Iterator/DepthRangeFilterIterator.php
  15. +123 −0 Iterator/FilePathsIterator.php
  16. +2 −2 Iterator/FilenameFilterIterator.php
  17. +3 −14 Iterator/MultiplePcreFilterIterator.php
  18. +245 −0 Shell/Command.php
  19. +86 −0 Shell/Shell.php
  20. +68 −0 Tests/Expression/ExpressionTest.php
  21. +4 −4 Tests/{ → Expression}/GlobTest.php
  22. +143 −0 Tests/Expression/RegexTest.php
  23. +58 −0 Tests/FakeAdapter/DummyAdapter.php
  24. +45 −0 Tests/FakeAdapter/FailingAdapter.php
  25. +57 −0 Tests/FakeAdapter/NamedAdapter.php
  26. +44 −0 Tests/FakeAdapter/UnsupportedAdapter.php
  27. +281 −94 Tests/FinderTest.php
  28. +7 −7 Tests/Iterator/DepthRangeIteratorTest.php
  29. +66 −0 Tests/Iterator/FilePathsIteratorTest.php
  30. +3 −1 Tests/Iterator/IteratorTestCase.php
174 Adapter/AbstractAdapter.php
View
@@ -0,0 +1,174 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Finder\Adapter;
+
+/**
+ * Interface for finder engine implementations.
+ *
+ * @author Jean-François Simon <contact@jfsimon.fr>
+ */
+abstract class AbstractAdapter implements AdapterInterface
+{
+ protected $followLinks = false;
+ protected $mode = 0;
+ protected $minDepth = 0;
+ protected $maxDepth = INF;
+ protected $exclude = array();
+ protected $names = array();
+ protected $notNames = array();
+ protected $contains = array();
+ protected $notContains = array();
+ protected $sizes = array();
+ protected $dates = array();
+ protected $filters = array();
+ protected $sort = false;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setFollowLinks($followLinks)
+ {
+ $this->followLinks = $followLinks;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setMode($mode)
+ {
+ $this->mode = $mode;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setDepths(array $depths)
+ {
+ $this->minDepth = 0;
+ $this->maxDepth = INF;
+
+ foreach ($depths as $comparator) {
+ switch ($comparator->getOperator()) {
+ case '>':
+ $this->minDepth = $comparator->getTarget() + 1;
+ break;
+ case '>=':
+ $this->minDepth = $comparator->getTarget();
+ break;
+ case '<':
+ $this->maxDepth = $comparator->getTarget() - 1;
+ break;
+ case '<=':
+ $this->maxDepth = $comparator->getTarget();
+ break;
+ default:
+ $this->minDepth = $this->maxDepth = $comparator->getTarget();
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setExclude(array $exclude)
+ {
+ $this->exclude = $exclude;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setNames(array $names)
+ {
+ $this->names = $names;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setNotNames(array $notNames)
+ {
+ $this->notNames = $notNames;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setContains(array $contains)
+ {
+ $this->contains = $contains;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setNotContains(array $notContains)
+ {
+ $this->notContains = $notContains;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setSizes(array $sizes)
+ {
+ $this->sizes = $sizes;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setDates(array $dates)
+ {
+ $this->dates = $dates;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setFilters(array $filters)
+ {
+ $this->filters = $filters;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setSort($sort)
+ {
+ $this->sort = $sort;
+
+ return $this;
+ }
+}
123 Adapter/AdapterInterface.php
View
@@ -0,0 +1,123 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Finder\Adapter;
+
+/**
+ * @author Jean-François Simon <contact@jfsimon.fr>
+ */
+interface AdapterInterface
+{
+ /**
+ * @param bool $followLinks
+ *
+ * @return AdapterInterface Current instance
+ */
+ function setFollowLinks($followLinks);
+
+ /**
+ * @param int $mode
+ *
+ * @return AdapterInterface Current instance
+ */
+ function setMode($mode);
+
+ /**
+ * @param array $exclude
+ *
+ * @return AdapterInterface Current instance
+ */
+ function setExclude(array $exclude);
+
+ /**
+ * @param array $depths
+ *
+ * @return AdapterInterface Current instance
+ */
+ function setDepths(array $depths);
+
+ /**
+ * @param array $names
+ *
+ * @return AdapterInterface Current instance
+ */
+ function setNames(array $names);
+
+ /**
+ * @param array $notNames
+ *
+ * @return AdapterInterface Current instance
+ */
+ function setNotNames(array $notNames);
+
+ /**
+ * @param array $contains
+ *
+ * @return AdapterInterface Current instance
+ */
+ function setContains(array $contains);
+
+ /**
+ * @param array $notContains
+ *
+ * @return AdapterInterface Current instance
+ */
+ function setNotContains(array $notContains);
+
+ /**
+ * @param array $sizes
+ *
+ * @return AdapterInterface Current instance
+ */
+ function setSizes(array $sizes);
+
+ /**
+ * @param array $dates
+ *
+ * @return AdapterInterface Current instance
+ */
+ function setDates(array $dates);
+
+ /**
+ * @param array $filters
+ *
+ * @return AdapterInterface Current instance
+ */
+ function setFilters(array $filters);
+
+ /**
+ * @param \Closure|int $sort
+ *
+ * @return AdapterInterface Current instance
+ */
+ function setSort($sort);
+
+ /**
+ * @param string $dir
+ *
+ * @return \Iterator Result iterator
+ */
+ function searchInDirectory($dir);
+
+ /**
+ * Tests adapter support for current platform.
+ *
+ * @return bool
+ */
+ function isSupported();
+
+ /**
+ * Returns adapter name.
+ *
+ * @return string
+ */
+ function getName();
+}
312 Adapter/GnuFindAdapter.php
View
@@ -0,0 +1,312 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Finder\Adapter;
+
+use Symfony\Component\Finder\Iterator;
+use Symfony\Component\Finder\Shell\Shell;
+use Symfony\Component\Finder\Expression\Expression;
+use Symfony\Component\Finder\Shell\Command;
+use Symfony\Component\Finder\Iterator\SortableIterator;
+use Symfony\Component\Finder\Comparator\NumberComparator;
+use Symfony\Component\Finder\Comparator\DateComparator;
+
+/**
+ * Shell engine implementation using GNU find command.
+ *
+ * @author Jean-François Simon <contact@jfsimon.fr>
+ */
+class GnuFindAdapter extends AbstractAdapter
+{
+ /**
+ * @var Shell
+ */
+ private $shell;
+
+ /**
+ * Constructor.
+ */
+ public function __construct()
+ {
+ $this->shell = new Shell();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function searchInDirectory($dir)
+ {
+ // having "/../" in path make find fail
+ $dir = realpath($dir);
+
+ // searching directories containing or not containing strings leads to no result
+ if (Iterator\FileTypeFilterIterator::ONLY_DIRECTORIES === $this->mode && ($this->contains || $this->notContains)) {
+ return new Iterator\FilePathsIterator(array(), $dir);
+ }
+
+ $command = Command::create();
+
+ $find = $command
+ ->ins('find')
+ ->add('find ')
+ ->arg($dir)
+ ->add('-noleaf') // -noleaf option is required for filesystems who doesn't follow '.' and '..' convention
+ ->add('-regextype posix-extended');
+
+ if ($this->followLinks) {
+ $find->add('-follow');
+ }
+
+ $find->add('-mindepth')->add($this->minDepth+1);
+ // warning! INF < INF => true ; INF == INF => false ; INF === INF => true
+ // https://bugs.php.net/bug.php?id=9118
+ if (INF !== $this->maxDepth) {
+ $find->add('-maxdepth')->add($this->maxDepth+1);
+ }
+
+ if (Iterator\FileTypeFilterIterator::ONLY_DIRECTORIES === $this->mode) {
+ $find->add('-type d');
+ } elseif (Iterator\FileTypeFilterIterator::ONLY_FILES === $this->mode) {
+ $find->add('-type f');
+ }
+
+ $this->buildNamesFiltering($find, $this->names);
+ $this->buildNamesFiltering($find, $this->notNames, true);
+ $this->buildSizesFiltering($find, $this->sizes);
+ $this->buildDatesFiltering($find, $this->dates);
+
+ $useGrep = $this->shell->testCommand('grep') && $this->shell->testCommand('xargs');
+ $useSort = is_int($this->sort) && $this->shell->testCommand('sort') && $this->shell->testCommand('awk');
+
+ if ($useGrep && ($this->contains || $this->notContains)) {
+ $grep = $command->ins('grep');
+ $this->buildContentFiltering($grep, $this->contains);
+ $this->buildContentFiltering($grep, $this->notContains, true);
+ }
+
+ if ($useSort) {
+ $this->buildSorting($command, $this->sort);
+ }
+
+ $paths = $this->shell->testCommand('uniq') ? $command->add('| uniq')->execute() : array_unique($command->execute());
+ $iterator = new Iterator\FilePathsIterator($paths, $dir);
+
+ if ($this->exclude) {
+ $iterator = new Iterator\ExcludeDirectoryFilterIterator($iterator, $this->exclude);
+ }
+
+ if (!$useGrep && ($this->contains || $this->notContains)) {
+ $iterator = new Iterator\FilecontentFilterIterator($iterator, $this->contains, $this->notContains);
+ }
+
+ if ($this->filters) {
+ $iterator = new Iterator\CustomFilterIterator($iterator, $this->filters);
+ }
+
+ if (!$useSort && $this->sort) {
+ $iteratorAggregate = new Iterator\SortableIterator($iterator, $this->sort);
+ $iterator = $iteratorAggregate->getIterator();
+ }
+
+ return $iterator;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isSupported()
+ {
+ return $this->shell->getType() !== Shell::TYPE_WINDOWS
+ && $this->shell->testCommand('find');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return 'gnu_find';
+ }
+
+ /**
+ * @param Command $command
+ * @param string[] $names
+ * @param bool $not
+ */
+ private function buildNamesFiltering(Command $command, array $names, $not = false)
+ {
+ if (0 === count($names)) {
+ return;
+ }
+
+ $command->add($not ? '-not' : null)->cmd('(');
+
+ foreach ($names as $i => $name) {
+ $expr = Expression::create($name);
+
+ // Fixes 'not search' and 'fuls path matching' regex problems.
+ // - Jokers '.' are replaced by [^/].
+ // - We add '[^/]*' before and after regex (if no ^|$ flags are present).
+ if ($expr->isRegex()) {
+ $regex = $expr->getRegex();
+ $regex->prepend($regex->hasStartFlag() ? '/' : '/[^/]*')
+ ->setStartFlag(false)
+ ->setStartJoker(true)
+ ->replaceJokers('[^/]');
+ if (!$regex->hasEndFlag() || $regex->hasEndJoker()) {
+ $regex->setEndJoker(false)->append('[^/]*');
+ }
+ }
+
+ $command
+ ->add($i > 0 ? '-or' : null)
+ ->add($expr->isRegex()
+ ? ($expr->isCaseSensitive() ? '-regex' : '-iregex')
+ : ($expr->isCaseSensitive() ? '-name' : '-iname')
+ )
+ ->arg($expr->renderPattern());
+ }
+
+ $command->cmd(')');
+ }
+
+ /**
+ * @param Command $command
+ * @param NumberComparator[] $sizes
+ */
+ private function buildSizesFiltering(Command $command, array $sizes)
+ {
+ foreach ($sizes as $i => $size) {
+ $command->add($i > 0 ? '-and' : null);
+
+ if ('<=' === $size->getOperator()) {
+ $command->add('-size -'.($size->getTarget()+1).'c');
+ continue;
+ }
+
+ if ('<' === $size->getOperator()) {
+ $command->add('-size -'.$size->getTarget().'c');
+ continue;
+ }
+
+ if ('>=' === $size->getOperator()) {
+ $command->add('-size +'.($size->getTarget()-1).'c');
+ continue;
+ }
+
+ if ('>' === $size->getOperator()) {
+ $command->add('-size +'.$size->getTarget().'c');
+ continue;
+ }
+
+ if ('!=' === $size->getOperator()) {
+ $command->add('-size -'.$size->getTarget().'c');
+ $command->add('-size +'.$size->getTarget().'c');
+ continue;
+ }
+
+ $command->add('-size '.$size->getTarget().'c');
+ }
+ }
+
+ /**
+ * @param Command $command
+ * @param DateComparator[] $dates
+ */
+ private function buildDatesFiltering(Command $command, array $dates)
+ {
+ foreach ($dates as $i => $date) {
+ $command->add($i > 0 ? '-and' : null);
+
+ $mins = (int) round((time()-$date->getTarget())/60);
+
+ if (0 > $mins) {
+ // mtime is in the future
+ $command->add(' -mmin -0');
+ // we will have no result so we dont need to continue
+ return;
+ }
+
+ if ('<=' === $date->getOperator()) {
+ $command->add('-mmin +'.($mins-1));
+ continue;
+ }
+
+ if ('<' === $date->getOperator()) {
+ $command->add('-mmin +'.$mins);
+ continue;
+ }
+
+ if ('>=' === $date->getOperator()) {
+ $command->add('-mmin -'.($mins+1));
+ continue;
+ }
+
+ if ('>' === $date->getOperator()) {
+ $command->add('-mmin -'.$mins);
+ continue;
+ }
+
+ if ('!=' === $date->getOperator()) {
+ $command->add('-mmin +'.$mins.' -or -mmin -'.$mins);
+ continue;
+ }
+
+ $command->add('-mmin '.$mins);
+ }
+ }
+
+ /**
+ * @param Command $command
+ * @param array $contains
+ * @param bool $not
+ */
+ private function buildContentFiltering(Command $command, array $contains, $not = false)
+ {
+ foreach ($contains as $contain) {
+ $expr = Expression::create($contain);
+
+ // todo: avoid forking process for each $pattern by using multiple -e options
+ $command
+ ->add('| xargs -r grep -I')
+ ->add($expr->isCaseSensitive() ? null : '-i')
+ ->add($not ? '-L' : '-l')
+ ->add('-Ee')->arg($expr->renderPattern());
+ }
+ }
+
+ private function buildSorting(Command $command, $sort)
+ {
+ switch ($sort) {
+ case SortableIterator::SORT_BY_NAME:
+ $format = null;
+ break;
+ case SortableIterator::SORT_BY_TYPE:
+ $format = '%y';
+ break;
+ case SortableIterator::SORT_BY_ACCESSED_TIME:
+ $format = '%A@';
+ break;
+ case SortableIterator::SORT_BY_CHANGED_TIME:
+ $format = '%C@';
+ break;
+ case SortableIterator::SORT_BY_MODIFIED_TIME:
+ $format = '%T@';
+ break;
+ default:
+ throw new \InvalidArgumentException('Unknown sort options: '.$sort.'.');
+ }
+
+ $command->get('find')->add('-printf')->arg($format.' %h/%f\\n');
+ $command->ins('sort')->add('| sort');
+ $command->ins('awk')->add('| awk')->arg('{ print $'.(null === $format ? '1' : '2').' }');
+ }
+}
94 Adapter/PhpAdapter.php
View
@@ -0,0 +1,94 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Finder\Adapter;
+
+use Symfony\Component\Finder\Iterator;
+
+/**
+ * PHP finder engine implementation.
+ *
+ * @author Jean-François Simon <contact@jfsimon.fr>
+ */
+class PhpAdapter extends AbstractAdapter
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function searchInDirectory($dir)
+ {
+ $flags = \RecursiveDirectoryIterator::SKIP_DOTS;
+
+ if ($this->followLinks) {
+ $flags |= \RecursiveDirectoryIterator::FOLLOW_SYMLINKS;
+ }
+
+ $iterator = new \RecursiveIteratorIterator(
+ new Iterator\RecursiveDirectoryIterator($dir, $flags),
+ \RecursiveIteratorIterator::SELF_FIRST
+ );
+
+ if ($this->minDepth > 0 || $this->maxDepth < INF) {
+ $iterator = new Iterator\DepthRangeFilterIterator($iterator, $this->minDepth, $this->maxDepth);
+ }
+
+ if ($this->mode) {
+ $iterator = new Iterator\FileTypeFilterIterator($iterator, $this->mode);
+ }
+
+ if ($this->exclude) {
+ $iterator = new Iterator\ExcludeDirectoryFilterIterator($iterator, $this->exclude);
+ }
+
+ if ($this->names || $this->notNames) {
+ $iterator = new Iterator\FilenameFilterIterator($iterator, $this->names, $this->notNames);
+ }
+
+ if ($this->contains || $this->notContains) {
+ $iterator = new Iterator\FilecontentFilterIterator($iterator, $this->contains, $this->notContains);
+ }
+
+ if ($this->sizes) {
+ $iterator = new Iterator\SizeRangeFilterIterator($iterator, $this->sizes);
+ }
+
+ if ($this->dates) {
+ $iterator = new Iterator\DateRangeFilterIterator($iterator, $this->dates);
+ }
+
+ if ($this->filters) {
+ $iterator = new Iterator\CustomFilterIterator($iterator, $this->filters);
+ }
+
+ if ($this->sort) {
+ $iteratorAggregate = new Iterator\SortableIterator($iterator, $this->sort);
+ $iterator = $iteratorAggregate->getIterator();
+ }
+
+ return $iterator;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isSupported()
+ {
+ return true;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return 'php';
+ }
+}
46 Exception/AdapterFailureException.php
View
@@ -0,0 +1,46 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Finder\Exception;
+
+use Symfony\Component\Finder\Adapter\AdapterInterface;
+
+/**
+ * Base exception for all adapter failures.
+ *
+ * @author Jean-François Simon <contact@jfsimon.fr>
+ */
+class AdapterFailureException extends \RuntimeException implements ExceptionInterface
+{
+ /**
+ * @var \Symfony\Component\Finder\Adapter\AdapterInterface
+ */
+ private $adapter;
+
+ /**
+ * @param AdapterInterface $adapter
+ * @param string|null $message
+ * @param \Exception|null $previous
+ */
+ public function __construct(AdapterInterface $adapter, $message = null, \Exception $previous = null)
+ {
+ $this->adapter = $adapter;
+ parent::__construct($message ?: 'Search failed with "'.$adapter->getName().'" adapter.', $previous);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getAdapter()
+ {
+ return $this->adapter;
+ }
+}
14 Exception/ExceptionInterface.php
View
@@ -0,0 +1,14 @@
+<?php
+
+namespace Symfony\Component\Finder\Exception;
+
+/**
+ * @author Jean-François Simon <contact@jfsimon.fr>
+ */
+interface ExceptionInterface
+{
+ /**
+ * @return \Symfony\Component\Finder\Adapter\AdapterInterface
+ */
+ function getAdapter();
+}
19 Exception/OperationNotPermitedException.php
View
@@ -0,0 +1,19 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Finder\Exception;
+
+/**
+ * @author Jean-François Simon <contact@jfsimon.fr>
+ */
+class OperationNotPermitedException extends AdapterFailureException
+{
+}
45 Exception/ShellCommandFailureException.php
View
@@ -0,0 +1,45 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Finder\Exception;
+
+use Symfony\Component\Finder\Adapter\AdapterInterface;
+use Symfony\Component\Finder\Shell\Command;
+
+/**
+ * @author Jean-François Simon <contact@jfsimon.fr>
+ */
+class ShellCommandFailureException extends AdapterFailureException
+{
+ /**
+ * @var Command
+ */
+ private $command;
+
+ /**
+ * @param AdapterInterface $adapter
+ * @param Command $command
+ * @param \Exception|null $previous
+ */
+ public function __construct(AdapterInterface $adapter, Command $command, \Exception $previous = null)
+ {
+ $this->command = $command;
+ parent::__construct($adapter, 'Shell command failed: "'.$command->join().'".', $previous);
+ }
+
+ /**
+ * @return Command
+ */
+ public function getCommand()
+ {
+ return $this->command;
+ }
+}
112 Expression/Expression.php
View
@@ -0,0 +1,112 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Finder\Expression;
+
+/**
+ * @author Jean-François Simon <contact@jfsimon.fr>
+ */
+class Expression implements ValueInterface
+{
+ const TYPE_REGEX = 1;
+ const TYPE_GLOB = 2;
+
+ /**
+ * @var ValueInterface
+ */
+ private $value;
+
+ /**
+ * @param string $expr
+ *
+ * @return Expression
+ */
+ public static function create($expr)
+ {
+ return new self($expr);
+ }
+
+ /**
+ * @param string $expr
+ */
+ public function __construct($expr)
+ {
+ try {
+ $this->value = Regex::create($expr);
+ } catch(\InvalidArgumentException $e) {
+ $this->value = new Glob($expr);
+ }
+ }
+
+ /**
+ * @return string
+ */
+ public function __toString()
+ {
+ return $this->render();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function render()
+ {
+ return $this->value->render();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function renderPattern()
+ {
+ return $this->value->renderPattern();
+ }
+
+ /**
+ * @return bool
+ */
+ public function isCaseSensitive()
+ {
+ return $this->value->isCaseSensitive();
+ }
+
+ /**
+ * @return int
+ */
+ public function getType()
+ {
+ return $this->value->getType();
+ }
+
+ /**
+ * @return bool
+ */
+ public function isRegex()
+ {
+ return self::TYPE_REGEX === $this->value->getType();
+ }
+
+ /**
+ * @return bool
+ */
+ public function isGlob()
+ {
+ return self::TYPE_GLOB === $this->value->getType();
+ }
+
+ /**
+ * @return Regex
+ */
+ public function getRegex()
+ {
+ return self::TYPE_REGEX === $this->value->getType() ? $this->value : $this->value->toRegex();
+ }
+}
126 Expression/Glob.php
View
@@ -0,0 +1,126 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Finder\Expression;
+
+/**
+ * @author Jean-François Simon <contact@jfsimon.fr>
+ */
+class Glob implements ValueInterface
+{
+ /**
+ * @var string
+ */
+ private $pattern;
+
+ /**
+ * @param string $pattern
+ */
+ public function __construct($pattern)
+ {
+ $this->pattern = $pattern;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function render()
+ {
+ return $this->pattern;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function renderPattern()
+ {
+ return $this->pattern;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getType()
+ {
+ return Expression::TYPE_GLOB;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isCaseSensitive()
+ {
+ return true;
+ }
+
+ /**
+ * @param bool $strictLeadingDot
+ * @param bool $strictWildcardSlash
+ *
+ * @return Regex
+ */
+ public function toRegex($strictLeadingDot = true, $strictWildcardSlash = true)
+ {
+ $firstByte = true;
+ $escaping = false;
+ $inCurlies = 0;
+ $regex = '';
+ $sizeGlob = strlen($this->pattern);
+ for ($i = 0; $i < $sizeGlob; $i++) {
+ $car = $this->pattern[$i];
+ if ($firstByte) {
+ if ($strictLeadingDot && '.' !== $car) {
+ $regex .= '(?=[^\.])';
+ }
+
+ $firstByte = false;
+ }
+
+ if ('/' === $car) {
+ $firstByte = true;
+ }
+
+ if ('.' === $car || '(' === $car || ')' === $car || '|' === $car || '+' === $car || '^' === $car || '$' === $car) {
+ $regex .= "\\$car";
+ } elseif ('*' === $car) {
+ $regex .= $escaping ? '\\*' : ($strictWildcardSlash ? '[^/]*' : '.*');
+ } elseif ('?' === $car) {
+ $regex .= $escaping ? '\\?' : ($strictWildcardSlash ? '[^/]' : '.');
+ } elseif ('{' === $car) {
+ $regex .= $escaping ? '\\{' : '(';
+ if (!$escaping) {
+ ++$inCurlies;
+ }
+ } elseif ('}' === $car && $inCurlies) {
+ $regex .= $escaping ? '}' : ')';
+ if (!$escaping) {
+ --$inCurlies;
+ }
+ } elseif (',' === $car && $inCurlies) {
+ $regex .= $escaping ? ',' : '|';
+ } elseif ('\\' === $car) {
+ if ($escaping) {
+ $regex .= '\\\\';
+ $escaping = false;
+ } else {
+ $escaping = true;
+ }
+
+ continue;
+ } else {
+ $regex .= $car;
+ }
+ $escaping = false;
+ }
+
+ return new Regex('^'.$regex.'$');
+ }
+}
315 Expression/Regex.php
View
@@ -0,0 +1,315 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Finder\Expression;
+
+/**
+ * @author Jean-François Simon <contact@jfsimon.fr>
+ */
+class Regex implements ValueInterface
+{
+ const START_FLAG = '^';
+ const END_FLAG = '$';
+ const BOUNDARY = '~';
+ const JOKER = '.*';
+ const ESCAPING = '\\';
+
+ /**
+ * @var string
+ */
+ private $pattern;
+
+ /**
+ * @var array
+ */
+ private $options;
+
+ /**
+ * @var bool
+ */
+ private $startFlag;
+
+ /**
+ * @var bool
+ */
+ private $endFlag;
+
+ /**
+ * @var bool
+ */
+ private $startJoker;
+
+ /**
+ * @var bool
+ */
+ private $endJoker;
+
+ /**
+ * @param string $expr
+ *
+ * @return Regex
+ *
+ * @throws \InvalidArgumentException
+ */
+ public static function create($expr)
+ {
+ if (preg_match('/^(.{3,}?)([imsxuADU]*)$/', $expr, $m)) {
+ $start = substr($m[1], 0, 1);
+ $end = substr($m[1], -1);
+
+ if (($start === $end && !preg_match('/[*?[:alnum:] \\\\]/', $start)) || ($start === '{' && $end === '}')) {
+ return new self(substr($m[1], 1, -1), $m[2]);
+ }
+ }
+
+ throw new \InvalidArgumentException('Given expression is not a regex.');
+ }
+
+ /**
+ * @param string $pattern
+ * @param string $options
+ */
+ public function __construct($pattern, $options = '')
+ {
+ $this->parsePattern($pattern);
+ $this->options = $options;
+ }
+
+ /**
+ * @return string
+ */
+ public function __toString()
+ {
+ return $this->render();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function render()
+ {
+ return self::BOUNDARY
+ .$this->renderPattern()
+ .self::BOUNDARY
+ .$this->options;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function renderPattern()
+ {
+ return ($this->startFlag ? self::START_FLAG : '')
+ .($this->startJoker ? self::JOKER : '')
+ .$this->pattern
+ .($this->endJoker ? self::JOKER : '')
+ .($this->endFlag ? self::END_FLAG : '');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isCaseSensitive()
+ {
+ return !$this->hasOption('i');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getType()
+ {
+ return Expression::TYPE_REGEX;
+ }
+
+ /**
+ * @param string $option
+ *
+ * @return bool
+ */
+ public function hasOption($option)
+ {
+ return false !== strpos($this->options, $option);
+ }
+
+ /**
+ * @param string $option
+ *
+ * @return Regex
+ */
+ public function addOption($option)
+ {
+ if (!$this->hasOption($option)) {
+ $this->options.= $option;
+ }
+
+ return $this;
+ }
+
+ /**
+ * @param string $option
+ *
+ * @return Regex
+ */
+ public function removeOption($option)
+ {
+ $this->options = str_replace($option, '', $this->options);
+
+ return $this;
+ }
+
+ /**
+ * @param bool $startFlag
+ *
+ * @return Regex
+ */
+ public function setStartFlag($startFlag)
+ {
+ $this->startFlag = $startFlag;
+
+ return $this;
+ }
+
+ /**
+ * @return bool
+ */
+ public function hasStartFlag()
+ {
+ return $this->startFlag;
+ }
+
+ /**
+ * @param bool $endFlag
+ *
+ * @return Regex
+ */
+ public function setEndFlag($endFlag)
+ {
+ $this->endFlag = (bool) $endFlag;
+
+ return $this;
+ }
+
+ /**
+ * @return bool
+ */
+ public function hasEndFlag()
+ {
+ return $this->endFlag;
+ }
+
+ /**
+ * @param bool $startJoker
+ *
+ * @return Regex
+ */
+ public function setStartJoker($startJoker)
+ {
+ $this->startJoker = $startJoker;
+
+ return $this;
+ }
+
+ /**
+ * @return bool
+ */
+ public function hasStartJoker()
+ {
+ return $this->startJoker;
+ }
+
+ /**
+ * @param bool $endJoker
+ *
+ * @return Regex
+ */
+ public function setEndJoker($endJoker)
+ {
+ $this->endJoker = (bool) $endJoker;
+
+ return $this;
+ }
+
+ /**
+ * @return bool
+ */
+ public function hasEndJoker()
+ {
+ return $this->endJoker;
+ }
+
+ /**
+ * @param string $expr
+ *
+ * @return Regex
+ */
+ public function prepend($expr)
+ {
+ $this->pattern = $expr.$this->pattern;
+
+ return $this;
+ }
+
+ /**
+ * @param string $expr
+ *
+ * @return Regex
+ */
+ public function append($expr)
+ {
+ $this->pattern .= $expr;
+
+ return $this;
+ }
+
+ /**
+ * @param array $replacements
+ *
+ * @return Regex
+ */
+ public function replaceJokers($replacement)
+ {
+ $replace = function ($subject) use ($replacement) {
+ $subject = $subject[0];
+ $replace = 0 === substr_count($subject, '\\') % 2;
+
+ return $replace ? str_replace('.', $replacement, $subject) : $subject;
+ };
+
+ $this->pattern = preg_replace_callback('~[\\\\]*\\.~', $replace, $this->pattern);
+
+ return $this;
+ }
+
+ /**
+ * @param string $pattern
+ */
+ private function parsePattern($pattern)
+ {
+ if ($this->startFlag = self::START_FLAG === substr($pattern, 0, 1)) {
+ $pattern = substr($pattern, 1);
+ }
+
+ if ($this->startJoker = self::JOKER === substr($pattern, 0, 2)) {
+ $pattern = substr($pattern, 2);
+ }
+
+ if ($this->endFlag = (self::END_FLAG === substr($pattern, -1) && self::ESCAPING !== substr($pattern, -2, -1))) {
+ $pattern = substr($pattern, 0, -1);
+ }
+
+ if ($this->endJoker = (self::JOKER === substr($pattern, -2) && self::ESCAPING !== substr($pattern, -3, -2))) {
+ $pattern = substr($pattern, 0, -2);
+ }
+
+ $this->pattern = $pattern;
+ }
+}
46 Expression/ValueInterface.php
View
@@ -0,0 +1,46 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Finder\Expression;
+
+/**
+ * @author Jean-François Simon <contact@jfsimon.fr>
+ */
+interface ValueInterface
+{
+ /**
+ * Renders string representation of expression.
+ *
+ * @return string
+ */
+ function render();
+
+ /**
+ * Renders string representation of pattern.
+ *
+ * @return string
+ */
+ function renderPattern();
+
+ /**
+ * Returns value case sensitivity.
+ *
+ * @return bool
+ */
+ function isCaseSensitive();
+
+ /**
+ * Returns expression type.
+ *
+ * @return int
+ */
+ function getType();
+}
144 Finder.php
View
@@ -11,6 +11,11 @@
namespace Symfony\Component\Finder;
+use Symfony\Component\Finder\Adapter\AdapterInterface;
+use Symfony\Component\Finder\Adapter\GnuFindAdapter;
+use Symfony\Component\Finder\Adapter\PhpAdapter;
+use Symfony\Component\Finder\Exception\ExceptionInterface;
+
/**
* Finder allows to build rules to find files and directories.
*
@@ -46,6 +51,7 @@ class Finder implements \IteratorAggregate, \Countable
private $iterators = array();
private $contains = array();
private $notContains = array();
+ private $adapters = array();
private static $vcsPatterns = array('.svn', '_svn', 'CVS', '_darcs', '.arch-params', '.monotone', '.bzr', '.git', '.hg');
@@ -55,6 +61,9 @@ class Finder implements \IteratorAggregate, \Countable
public function __construct()
{
$this->ignore = static::IGNORE_VCS_FILES | static::IGNORE_DOT_FILES;
+
+ $this->register(new GnuFindAdapter());
+ $this->register(new PhpAdapter(), -50);
}
/**
@@ -70,6 +79,48 @@ public static function create()
}
/**
+ * Registers a finder engine implementation.
+ *
+ * @param AdapterInterface $adapter An adapter instance
+ * @param int $priority Highest is selected first
+ *
+ * @return Finder The current Finder instance
+ */
+ public function register(Adapter\AdapterInterface $adapter, $priority = 0)
+ {
+ $this->adapters[$adapter->getName()] = array(
+ 'adapter' => $adapter,
+ 'priority' => $priority,
+ );
+
+ return $this->sortAdapters();
+ }
+
+ /**
+ * Removes all adapters registered in the finder.
+ *
+ * @return Finder The current Finder instance
+ */
+ public function removeAdapters()
+ {
+ $this->adapters = array();
+
+ return $this;
+ }
+
+ /**
+ * Returns registered adapters ordered by priority without extra information.
+ *
+ * @return AdapterInterface[]
+ */
+ public function getAdapters()
+ {
+ return array_values(array_map(function(array $adapter) {
+ return $adapter['adapter'];
+ }, $this->adapters));
+ }
+
+ /**
* Restricts the matching to directories only.
*
* @return Finder The current Finder instance
@@ -568,27 +619,25 @@ public function count()
return iterator_count($this->getIterator());
}
- private function searchInDirectory($dir)
+ /*
+ * @return Finder The current Finder instance
+ */
+ private function sortAdapters()
{
- $flags = \RecursiveDirectoryIterator::SKIP_DOTS;
-
- if ($this->followLinks) {
- $flags |= \RecursiveDirectoryIterator::FOLLOW_SYMLINKS;
- }
-
- $iterator = new \RecursiveIteratorIterator(
- new Iterator\RecursiveDirectoryIterator($dir, $flags),
- \RecursiveIteratorIterator::SELF_FIRST
- );
-
- if ($this->depths) {
- $iterator = new Iterator\DepthRangeFilterIterator($iterator, $this->depths);
- }
+ uasort($this->adapters, function (array $a, array $b) {
+ return $a['priority'] > $b['priority'] ? -1 : 1;
+ });
- if ($this->mode) {
- $iterator = new Iterator\FileTypeFilterIterator($iterator, $this->mode);
- }
+ return $this;
+ }
+ /**
+ * @param $dir
+ *
+ * @return \Iterator
+ */
+ private function searchInDirectory($dir)
+ {
if (static::IGNORE_VCS_FILES === (static::IGNORE_VCS_FILES & $this->ignore)) {
$this->exclude = array_merge($this->exclude, self::$vcsPatterns);
}
@@ -597,35 +646,42 @@ private function searchInDirectory($dir)
$this->notNames[] = '/^\..+/';
}
- if ($this->exclude) {
- $iterator = new Iterator\ExcludeDirectoryFilterIterator($iterator, $this->exclude);
- }
-
- if ($this->names || $this->notNames) {
- $iterator = new Iterator\FilenameFilterIterator($iterator, $this->names, $this->notNames);
- }
-
- if ($this->contains || $this->notContains) {
- $iterator = new Iterator\FilecontentFilterIterator($iterator, $this->contains, $this->notContains);
- }
-
- if ($this->sizes) {
- $iterator = new Iterator\SizeRangeFilterIterator($iterator, $this->sizes);
- }
-
- if ($this->dates) {
- $iterator = new Iterator\DateRangeFilterIterator($iterator, $this->dates);
- }
+ foreach ($this->adapters as $adapter) {
+ if (!$adapter['adapter']->isSupported()) {
+ continue;
+ }
- if ($this->filters) {
- $iterator = new Iterator\CustomFilterIterator($iterator, $this->filters);
+ try {
+ return $this
+ ->buildAdapter($adapter['adapter'])
+ ->searchInDirectory($dir);
+ } catch(ExceptionInterface $e) {
+ continue;
+ }
}
- if ($this->sort) {
- $iteratorAggregate = new Iterator\SortableIterator($iterator, $this->sort);
- $iterator = $iteratorAggregate->getIterator();
- }
+ throw new \RuntimeException('No supported adapter found.');
+ }
- return $iterator;
+ /**
+ * @param AdapterInterface $adapter
+ *
+ * @return AdapterInterface
+ */
+ private function buildAdapter(AdapterInterface $adapter)
+ {
+ return $adapter
+ ->setFollowLinks($this->followLinks)
+ ->setDepths($this->depths)
+ ->setMode($this->mode)
+ ->setExclude($this->exclude)
+ ->setNames($this->names)
+ ->setNotNames($this->notNames)
+ ->setContains($this->contains)
+ ->setNotContains($this->notContains)
+ ->setSizes($this->sizes)
+ ->setDates($this->dates)
+ ->setFilters($this->filters)
+ ->setSort($this->sort);
}
}
26 Iterator/DepthRangeFilterIterator.php
View
@@ -24,31 +24,11 @@ class DepthRangeFilterIterator extends FilterIterator
* Constructor.
*
* @param \RecursiveIteratorIterator $iterator The Iterator to filter
- * @param array $comparators An array of \NumberComparator instances
+ * @param int $minDepth The min depth
+ * @param int $maxDepth The max depth
*/
- public function __construct(\RecursiveIteratorIterator $iterator, array $comparators)
+ public function __construct(\RecursiveIteratorIterator $iterator, $minDepth = 0, $maxDepth = INF)
{
- $minDepth = 0;
- $maxDepth = INF;
- foreach ($comparators as $comparator) {
- switch ($comparator->getOperator()) {
- case '>':
- $minDepth = $comparator->getTarget() + 1;
- break;
- case '>=':
- $minDepth = $comparator->getTarget();
- break;
- case '<':
- $maxDepth = $comparator->getTarget() - 1;
- break;
- case '<=':
- $maxDepth = $comparator->getTarget();
- break;
- default:
- $minDepth = $maxDepth = $comparator->getTarget();
- }
- }
-
$this->minDepth = $minDepth;
$iterator->setMaxDepth(INF === $maxDepth ? -1 : $maxDepth);
123 Iterator/FilePathsIterator.php
View
@@ -0,0 +1,123 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Finder\Iterator;
+
+use Symfony\Component\Finder\SplFileInfo;
+
+/**
+ * Iterate over shell command result.
+ *
+ * @author Jean-François Simon <contact@jfsimon.fr>
+ */
+class FilePathsIterator extends \ArrayIterator
+{
+ /**
+ * @var string
+ */
+ private $baseDir;
+
+ /**
+ * @var int
+ */
+ private $baseDirLength;
+
+ /**
+ * @var string
+ */
+ private $subPath;
+
+ /**
+ * @var string
+ */
+ private $subPathname;
+
+ /**
+ * @param array $paths List of paths returned by shell command
+ * @param string $baseDir Base dir for relative path building
+ */
+ public function __construct(array $paths, $baseDir)
+ {
+ $this->baseDir = $baseDir;
+ $this->baseDirLength = strlen($baseDir);
+
+ parent::__construct($paths);
+ }
+
+ /**
+ * @param string $name
+ * @param array $arguments
+ *
+ * @return mixed
+ */
+ public function __call($name, array $arguments)
+ {
+ return call_user_func_array(array($this->current(), $name), $arguments);
+ }
+
+ /**
+ * Return an instance of SplFileInfo with support for relative paths.
+ *
+ * @return SplFileInfo File information
+ */
+ public function current()
+ {
+ return new SplFileInfo(parent::current(), $this->subPath, $this->subPathname);
+ }
+
+ public function next()
+ {
+ parent::next();
+
+ $this->buildSubPath();
+ }
+
+ public function rewind()
+ {
+ parent::rewind();
+
+ $this->buildSubPath();
+ }
+
+ /**
+ * @return string
+ */
+ public function getSubPath()
+ {
+ return $this->subPath;
+ }
+
+ /**
+ * @return string
+ */
+ public function getSubPathname()
+ {
+ return $this->subPathname;
+ }
+
+ /**
+ * @param string $absolutePath
+ *
+ * @return null|string
+ */
+ private function buildSubPath()
+ {
+ $absolutePath = parent::current();
+
+ if ($this->baseDir === substr($absolutePath, 0, $this->baseDirLength)) {
+ $this->subPathname = ltrim(substr($absolutePath, $this->baseDirLength), '/\\');
+ $dir = dirname($this->subPathname);
+ $this->subPath = '.' === $dir ? '' : $dir;
+ } else {
+ $this->subPath = $this->subPathname = '';
+ }
+ }
+}
4 Iterator/FilenameFilterIterator.php
View
@@ -11,7 +11,7 @@
namespace Symfony\Component\Finder\Iterator;
-use Symfony\Component\Finder\Glob;
+use Symfony\Component\Finder\Expression\Expression;
/**
* FilenameFilterIterator filters files by patterns (a regexp, a glob, or a string).
@@ -63,6 +63,6 @@ public function accept()
*/
protected function toRegex($str)
{
- return $this->isRegex($str) ? $str : Glob::toRegex($str);
+ return Expression::create($str)->getRegex()->render();
}
}
17 Iterator/MultiplePcreFilterIterator.php
View
@@ -11,6 +11,8 @@
namespace Symfony\Component\Finder\Iterator;
+use Symfony\Component\Finder\Expression\Expression;
+
/**
* MultiplePcreFilterIterator filters files using patterns (regexps, globs or strings).
*
@@ -52,20 +54,7 @@ public function __construct(\Iterator $iterator, array $matchPatterns, array $no
*/
protected function isRegex($str)
{
- if (preg_match('/^(.{3,}?)[imsxuADU]*$/', $str, $m)) {
- $start = substr($m[1], 0, 1);
- $end = substr($m[1], -1);
-
- if ($start === $end) {
- return !preg_match('/[*?[:alnum:] \\\\]/', $start);
- }
-
- if ($start === '{' && $end === '}') {
- return true;
- }
- }
-
- return false;
+ return Expression::create($str)->isRegex();
}
/**
245 Shell/Command.php
View
@@ -0,0 +1,245 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Finder\Shell;
+
+/**
+ * @author Jean-François Simon <contact@jfsimon.fr>
+ */
+class Command
+{
+ /**
+ * @var Command|null
+ */
+ private $parent;
+
+ /**
+ * @var array
+ */
+ private $bits;
+
+ /**
+ * @var array
+ */
+ private $labels;
+
+ /**
+ * Constructor.
+ *
+ * @param Command $parent Parent command
+ */
+ public function __construct(Command $parent = null)
+ {
+ $this->parent = $parent;
+ $this->bits = array();
+ $this->labels = array();
+ }
+
+ /**
+ * Returns command as string.
+ *
+ * @return string
+ */
+ public function __toString()
+ {
+ return $this->join();
+ }
+
+ /**
+ * Creates a new Command instance.
+ *
+ * @param Command $parent Parent command
+ *
+ * @return Command New Command instance
+ */
+ public static function create(Command $parent = null)
+ {
+ return new self($parent);
+ }
+
+ /**
+ * Escapes special chars from input.
+ *
+ * @param string $input A string to escape
+ *
+ * @return string The escaped string
+ */
+ public static function escape($input)
+ {
+ return escapeshellcmd($input);
+ }
+
+ /**
+ * Quotes input.
+ *
+ * @param string $input An argument string
+ *
+ * @return string The quoted string
+ */
+ public static function quote($input)
+ {
+ return escapeshellarg($input);
+ }
+
+ /**
+ * Appends a string or a Command instance.
+ *
+ * @param string|Command $bit
+ *
+ * @return Command The current Command instance
+ */
+ public function add($bit)
+ {
+ $this->bits[] = $bit;
+
+ return $this;
+ }
+
+ /**
+ * Prepends a string or a command instance.
+ *
+ * @param string|Command $bit
+ *
+ * @return Command The current Command instance
+ */
+ public function top($bit)
+ {
+ array_unshift($this->bits, $bit);
+
+ foreach ($this->labels as $label => $index) {
+ $this->labels[$label] += 1;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Appends an argument, will be quoted.
+ *
+ * @param string $arg
+ *
+ * @return Command The current Command instance
+ */
+ public function arg($arg)
+ {
+ $this->bits[] = self::quote($arg);
+
+ return $this;
+ }
+
+ /**
+ * Appends escaped special command chars.
+ *
+ * @param string $esc
+ *
+ * @return Command The current Command instance
+ */
+ public function cmd($esc)
+ {
+ $this->bits[] = self::escape($esc);
+
+ return $this;
+ }
+
+ /**
+ * Inserts a labeled command to feed later.
+ *
+ * @param string $label The unique label
+ *
+ * @return Command The current Command instance
+ *
+ * @throws \RuntimeException If label already exists
+ */
+ public function ins($label)
+ {
+ if (isset($this->labels[$label])) {
+ throw new \RuntimeException('Label "'.$label.'" already exists.');
+ }
+
+ $this->bits[] = self::create($this);
+ $this->labels[$label] = count($this->bits)-1;
+
+ return $this->bits[$this->labels[$label]];
+ }
+
+ /**
+ * Retrieves a previously labeled command.
+ *
+ * @param string $label
+ *
+ * @return Command The labeled command
+ */
+ public function get($label)
+ {
+ if (!isset($this->labels[$label])) {
+ throw new \RuntimeException('Label "'.$label.'" does not exists.');
+ }
+
+ return $this->bits[$this->labels[$label]];
+ }
+
+ /**
+ * Returns parent command (if any).
+ *
+ * @return Command Parent command
+ *
+ * @throws \RuntimeException If command has no parent
+ */
+ public function end()
+ {
+ if (null === $this->parent) {
+ throw new \RuntimeException('Calling end on root command dont makes sense.');
+ }
+
+ return $this->parent;
+ }
+
+ /**
+ * Counts bits stored in command.
+ *
+ * @return int The bits count
+ */
+ public function length()
+ {
+ return count($this->bits);
+ }
+
+ /**
+ * Executes current command.
+ *
+ * @return array The command result
+ */
+ public function execute()
+ {
+ exec($this->join(), $output, $code);
+
+ if (0 !== $code) {
+ throw new \RuntimeException('Execution failed with return code: '.$code.'.');
+ }
+
+ return $output ?: array();
+ }
+
+ /**
+ * Joins bits.
+ *
+ * @return string
+ */
+ public function join()
+ {
+ return implode(' ', array_filter(
+ array_map(function($bit) {
+ return $bit instanceof Command ? $bit->join() : ($bit ?: null);
+ }, $this->bits),
+ function($bit) { return null !== $bit; }
+ ));
+ }
+}
86 Shell/Shell.php
View
@@ -0,0 +1,86 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Finder\Shell;
+
+/**
+ * @author Jean-François Simon <contact@jfsimon.fr>
+ */
+class Shell
+{
+ const TYPE_UNIX = 1;
+ const TYPE_DARWIN = 2;
+ const TYPE_CYGWIN = 3;
+ const TYPE_WINDOWS = 4;
+
+ /**
+ * @var string|null
+ */
+ private $type;
+
+ /**
+ * Returns guessed OS type.
+ *
+ * @return int
+ */
+ public function getType()
+ {
+ if (null === $this->type) {
+ $this->type = $this->guessType();
+ }
+
+ return $this->type;
+ }
+
+ /**
+ * Tests if a command is available.
+ *
+ * @param string $command
+ *
+ * @return bool
+ */
+ public function testCommand($command)
+ {
+ if (self::TYPE_WINDOWS === $this->type) {
+ // todo: find a way to test if windows command exists
+ return true;
+ }
+
+ // todo: find a better way (command could not be available)
+ exec('command -v '.$command, $output, $code);
+
+ return 0 === $code && count($output) > 0;
+ }
+
+ /**
+ * Guesses OS type.
+ *
+ * @return int
+ */
+ private function guessType()
+ {
+ $os = strtolower(PHP_OS);
+
+ if (false !== strpos($os, 'cygwin')) {
+ return self::TYPE_CYGWIN;
+ }
+
+ if (false !== strpos($os, 'darwin')) {
+ return self::TYPE_DARWIN;
+ }
+
+ if (0 === strpos($os, 'win')) {
+ return self::TYPE_WINDOWS;
+ }
+
+ return self::TYPE_UNIX;
+ }
+}
68 Tests/Expression/ExpressionTest.php
View
@@ -0,0 +1,68 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Finder\Tests;
+
+use Symfony\Component\Finder\Expression\Expression;
+
+class ExpressionTest extends \PHPUnit_Framework_TestCase
+{
+ /**
+ * @dataProvider getTypeGuesserData
+ */
+ public function testTypeGuesser($expr, $type)
+ {
+ $this->assertEquals($type, Expression::create($expr)->getType());
+ }
+
+ /**
+ * @dataProvider getCaseSensitiveData
+ */
+ public function testCaseSensitive($expr, $isCaseSensitive)
+ {
+ $this->assertEquals($isCaseSensitive, Expression::create($expr)->isCaseSensitive());
+ }
+
+ /**
+ * @dataProvider getRegexRenderingData
+ */
+ public function testRegexRendering($expr, $body)
+ {
+ $this->assertEquals($body, Expression::create($expr)->renderPattern());
+ }
+
+ public function getTypeGuesserData()
+ {
+ return array(
+ array('{foo}', Expression::TYPE_REGEX),
+ array('/foo/', Expression::TYPE_REGEX),
+ array('foo', Expression::TYPE_GLOB),
+ array('foo*', Expression::TYPE_GLOB),
+ );
+ }
+
+ public function getCaseSensitiveData()
+ {
+ return array(
+ array('{foo}m', true),
+ array('/foo/i', false),
+ array('foo*', true),
+ );
+ }
+
+ public function getRegexRenderingData()
+ {
+ return array(
+ array('{foo}m', 'foo'),
+ array('/foo/i', 'foo'),
+ );
+ }
+}
8 Tests/GlobTest.php → Tests/Expression/GlobTest.php
View
@@ -11,21 +11,21 @@
namespace Symfony\Component\Finder\Tests;
-use Symfony\Component\Finder\Glob;
+use Symfony\Component\Finder\Expression\Expression;
class GlobTest extends \PHPUnit_Framework_TestCase
{
/**
* @dataProvider getToRegexData
*/
- public function testToRegex($glob, $match, $noMatch)
+ public function testGlobToRegex($glob, $match, $noMatch)
{
foreach ($match as $m) {
- $this->assertRegExp(Glob::toRegex($glob), $m, '::toRegex() converts a glob to a regexp');
+ $this->assertRegExp(Expression::create($glob)->getRegex()->render(), $m, '::toRegex() converts a glob to a regexp');
}
foreach ($noMatch as $m) {
- $this->assertNotRegExp(Glob::toRegex($glob), $m, '::toRegex() converts a glob to a regexp');
+ $this->assertNotRegExp(Expression::create($glob)->getRegex()->render(), $m, '::toRegex() converts a glob to a regexp');
}
}
143 Tests/Expression/RegexTest.php
View
@@ -0,0 +1,143 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Finder\Tests;
+
+use Symfony\Component\Finder\Expression\Expression;
+
+class RegexTest extends \PHPUnit_Framework_TestCase
+{
+ /**
+ * @dataProvider getHasFlagsData
+ */
+ public function testHasFlags($regex, $start, $end)
+ {
+ $expr = new Expression($regex);
+
+ $this->assertEquals($start, $expr->getRegex()->hasStartFlag());
+ $this->assertEquals($end, $expr->getRegex()->hasEndFlag());
+ }
+
+ /**
+ * @dataProvider getHasJokersData
+ */
+ public function testHasJokers($regex, $start, $end)
+ {
+ $expr = new Expression($regex);
+
+ $this->assertEquals($start, $expr->getRegex()->hasStartJoker());
+ $this->assertEquals($end, $expr->getRegex()->hasEndJoker());
+ }
+
+ /**
+ * @dataProvider getSetFlagsData
+ */
+ public function testSetFlags($regex, $start, $end, $expected)
+ {
+ $expr = new Expression($regex);
+ $expr->getRegex()->setStartFlag($start)->setEndFlag($end);
+
+ $this->assertEquals($expected, $expr->render());
+ }
+
+ /**
+ * @dataProvider getSetJokersData
+ */
+ public function testSetJokers($regex, $start, $end, $expected)
+ {
+ $expr = new Expression($regex);
+ $expr->getRegex()->setStartJoker($start)-