From 32bb754fbe44aefaa665c576d6fbdbda15f205cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Simon?= Date: Sat, 21 Apr 2012 21:31:44 +0200 Subject: [PATCH] [2.2] [WIP] [Finder] Adding native finders implementations --- .../Finder/Adapter/AbstractAdapter.php | 174 ++++++++ .../Finder/Adapter/AdapterInterface.php | 123 ++++++ .../Finder/Adapter/GnuFindAdapter.php | 312 +++++++++++++++ .../Component/Finder/Adapter/PhpAdapter.php | 94 +++++ .../Exception/AdapterFailureException.php | 46 +++ .../Finder/Exception/ExceptionInterface.php | 14 + .../OperationNotPermitedException.php | 19 + .../ShellCommandFailureException.php | 45 +++ .../Finder/Expression/Expression.php | 112 ++++++ .../Component/Finder/Expression/Glob.php | 126 ++++++ .../Component/Finder/Expression/Regex.php | 315 +++++++++++++++ .../Finder/Expression/ValueInterface.php | 46 +++ src/Symfony/Component/Finder/Finder.php | 144 +++++-- .../Iterator/DepthRangeFilterIterator.php | 26 +- .../Finder/Iterator/FilePathsIterator.php | 123 ++++++ .../Iterator/FilenameFilterIterator.php | 4 +- .../Iterator/MultiplePcreFilterIterator.php | 17 +- .../Component/Finder/Shell/Command.php | 245 ++++++++++++ src/Symfony/Component/Finder/Shell/Shell.php | 86 ++++ .../Tests/Expression/ExpressionTest.php | 68 ++++ .../Tests/{ => Expression}/GlobTest.php | 8 +- .../Finder/Tests/Expression/RegexTest.php | 143 +++++++ .../Finder/Tests/FakeAdapter/DummyAdapter.php | 58 +++ .../Tests/FakeAdapter/FailingAdapter.php | 45 +++ .../Finder/Tests/FakeAdapter/NamedAdapter.php | 57 +++ .../Tests/FakeAdapter/UnsupportedAdapter.php | 44 ++ .../Component/Finder/Tests/FinderTest.php | 375 +++++++++++++----- .../Tests/Iterator/DepthRangeIteratorTest.php | 14 +- .../Tests/Iterator/FilePathsIteratorTest.php | 66 +++ .../Tests/Iterator/IteratorTestCase.php | 4 +- 30 files changed, 2764 insertions(+), 189 deletions(-) create mode 100644 src/Symfony/Component/Finder/Adapter/AbstractAdapter.php create mode 100644 src/Symfony/Component/Finder/Adapter/AdapterInterface.php create mode 100644 src/Symfony/Component/Finder/Adapter/GnuFindAdapter.php create mode 100644 src/Symfony/Component/Finder/Adapter/PhpAdapter.php create mode 100644 src/Symfony/Component/Finder/Exception/AdapterFailureException.php create mode 100644 src/Symfony/Component/Finder/Exception/ExceptionInterface.php create mode 100644 src/Symfony/Component/Finder/Exception/OperationNotPermitedException.php create mode 100644 src/Symfony/Component/Finder/Exception/ShellCommandFailureException.php create mode 100644 src/Symfony/Component/Finder/Expression/Expression.php create mode 100644 src/Symfony/Component/Finder/Expression/Glob.php create mode 100644 src/Symfony/Component/Finder/Expression/Regex.php create mode 100644 src/Symfony/Component/Finder/Expression/ValueInterface.php create mode 100644 src/Symfony/Component/Finder/Iterator/FilePathsIterator.php create mode 100644 src/Symfony/Component/Finder/Shell/Command.php create mode 100644 src/Symfony/Component/Finder/Shell/Shell.php create mode 100644 src/Symfony/Component/Finder/Tests/Expression/ExpressionTest.php rename src/Symfony/Component/Finder/Tests/{ => Expression}/GlobTest.php (77%) create mode 100644 src/Symfony/Component/Finder/Tests/Expression/RegexTest.php create mode 100644 src/Symfony/Component/Finder/Tests/FakeAdapter/DummyAdapter.php create mode 100644 src/Symfony/Component/Finder/Tests/FakeAdapter/FailingAdapter.php create mode 100644 src/Symfony/Component/Finder/Tests/FakeAdapter/NamedAdapter.php create mode 100644 src/Symfony/Component/Finder/Tests/FakeAdapter/UnsupportedAdapter.php create mode 100644 src/Symfony/Component/Finder/Tests/Iterator/FilePathsIteratorTest.php diff --git a/src/Symfony/Component/Finder/Adapter/AbstractAdapter.php b/src/Symfony/Component/Finder/Adapter/AbstractAdapter.php new file mode 100644 index 000000000000..8958571bdff7 --- /dev/null +++ b/src/Symfony/Component/Finder/Adapter/AbstractAdapter.php @@ -0,0 +1,174 @@ + + * + * 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 + */ +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; + } +} diff --git a/src/Symfony/Component/Finder/Adapter/AdapterInterface.php b/src/Symfony/Component/Finder/Adapter/AdapterInterface.php new file mode 100644 index 000000000000..246a26cb06ad --- /dev/null +++ b/src/Symfony/Component/Finder/Adapter/AdapterInterface.php @@ -0,0 +1,123 @@ + + * + * 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 + */ +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(); +} diff --git a/src/Symfony/Component/Finder/Adapter/GnuFindAdapter.php b/src/Symfony/Component/Finder/Adapter/GnuFindAdapter.php new file mode 100644 index 000000000000..98ac30f9b2d0 --- /dev/null +++ b/src/Symfony/Component/Finder/Adapter/GnuFindAdapter.php @@ -0,0 +1,312 @@ + + * + * 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 + */ +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').' }'); + } +} diff --git a/src/Symfony/Component/Finder/Adapter/PhpAdapter.php b/src/Symfony/Component/Finder/Adapter/PhpAdapter.php new file mode 100644 index 000000000000..2dbae76a565f --- /dev/null +++ b/src/Symfony/Component/Finder/Adapter/PhpAdapter.php @@ -0,0 +1,94 @@ + + * + * 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 + */ +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'; + } +} diff --git a/src/Symfony/Component/Finder/Exception/AdapterFailureException.php b/src/Symfony/Component/Finder/Exception/AdapterFailureException.php new file mode 100644 index 000000000000..15fa22147d83 --- /dev/null +++ b/src/Symfony/Component/Finder/Exception/AdapterFailureException.php @@ -0,0 +1,46 @@ + + * + * 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 + */ +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; + } +} diff --git a/src/Symfony/Component/Finder/Exception/ExceptionInterface.php b/src/Symfony/Component/Finder/Exception/ExceptionInterface.php new file mode 100644 index 000000000000..86d391b91e5c --- /dev/null +++ b/src/Symfony/Component/Finder/Exception/ExceptionInterface.php @@ -0,0 +1,14 @@ + + */ +interface ExceptionInterface +{ + /** + * @return \Symfony\Component\Finder\Adapter\AdapterInterface + */ + function getAdapter(); +} diff --git a/src/Symfony/Component/Finder/Exception/OperationNotPermitedException.php b/src/Symfony/Component/Finder/Exception/OperationNotPermitedException.php new file mode 100644 index 000000000000..3663112259c4 --- /dev/null +++ b/src/Symfony/Component/Finder/Exception/OperationNotPermitedException.php @@ -0,0 +1,19 @@ + + * + * 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 + */ +class OperationNotPermitedException extends AdapterFailureException +{ +} diff --git a/src/Symfony/Component/Finder/Exception/ShellCommandFailureException.php b/src/Symfony/Component/Finder/Exception/ShellCommandFailureException.php new file mode 100644 index 000000000000..2658f6a508fb --- /dev/null +++ b/src/Symfony/Component/Finder/Exception/ShellCommandFailureException.php @@ -0,0 +1,45 @@ + + * + * 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 + */ +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; + } +} diff --git a/src/Symfony/Component/Finder/Expression/Expression.php b/src/Symfony/Component/Finder/Expression/Expression.php new file mode 100644 index 000000000000..b8124124269d --- /dev/null +++ b/src/Symfony/Component/Finder/Expression/Expression.php @@ -0,0 +1,112 @@ + + * + * 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 + */ +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(); + } +} diff --git a/src/Symfony/Component/Finder/Expression/Glob.php b/src/Symfony/Component/Finder/Expression/Glob.php new file mode 100644 index 000000000000..5e7b2d01127f --- /dev/null +++ b/src/Symfony/Component/Finder/Expression/Glob.php @@ -0,0 +1,126 @@ + + * + * 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 + */ +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.'$'); + } +} diff --git a/src/Symfony/Component/Finder/Expression/Regex.php b/src/Symfony/Component/Finder/Expression/Regex.php new file mode 100644 index 000000000000..40d776394a89 --- /dev/null +++ b/src/Symfony/Component/Finder/Expression/Regex.php @@ -0,0 +1,315 @@ + + * + * 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 + */ +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; + } +} diff --git a/src/Symfony/Component/Finder/Expression/ValueInterface.php b/src/Symfony/Component/Finder/Expression/ValueInterface.php new file mode 100644 index 000000000000..aca28f4bad89 --- /dev/null +++ b/src/Symfony/Component/Finder/Expression/ValueInterface.php @@ -0,0 +1,46 @@ + + * + * 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 + */ +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(); +} diff --git a/src/Symfony/Component/Finder/Finder.php b/src/Symfony/Component/Finder/Finder.php index 489a6f8b9a98..63fa6e70f7f7 100644 --- a/src/Symfony/Component/Finder/Finder.php +++ b/src/Symfony/Component/Finder/Finder.php @@ -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); } /** @@ -69,6 +78,48 @@ public static function create() return new static(); } + /** + * 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. * @@ -569,27 +620,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); } @@ -598,35 +647,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); } } diff --git a/src/Symfony/Component/Finder/Iterator/DepthRangeFilterIterator.php b/src/Symfony/Component/Finder/Iterator/DepthRangeFilterIterator.php index 832125393f1a..77a9f45f2dfa 100644 --- a/src/Symfony/Component/Finder/Iterator/DepthRangeFilterIterator.php +++ b/src/Symfony/Component/Finder/Iterator/DepthRangeFilterIterator.php @@ -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); diff --git a/src/Symfony/Component/Finder/Iterator/FilePathsIterator.php b/src/Symfony/Component/Finder/Iterator/FilePathsIterator.php new file mode 100644 index 000000000000..5153ed301aea --- /dev/null +++ b/src/Symfony/Component/Finder/Iterator/FilePathsIterator.php @@ -0,0 +1,123 @@ + + * + * 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 + */ +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 = ''; + } + } +} diff --git a/src/Symfony/Component/Finder/Iterator/FilenameFilterIterator.php b/src/Symfony/Component/Finder/Iterator/FilenameFilterIterator.php index 109b5f00294b..3c0f3aa70ef7 100644 --- a/src/Symfony/Component/Finder/Iterator/FilenameFilterIterator.php +++ b/src/Symfony/Component/Finder/Iterator/FilenameFilterIterator.php @@ -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(); } } diff --git a/src/Symfony/Component/Finder/Iterator/MultiplePcreFilterIterator.php b/src/Symfony/Component/Finder/Iterator/MultiplePcreFilterIterator.php index 12584b186e00..3a9dd5558285 100644 --- a/src/Symfony/Component/Finder/Iterator/MultiplePcreFilterIterator.php +++ b/src/Symfony/Component/Finder/Iterator/MultiplePcreFilterIterator.php @@ -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(); } /** diff --git a/src/Symfony/Component/Finder/Shell/Command.php b/src/Symfony/Component/Finder/Shell/Command.php new file mode 100644 index 000000000000..477c664e111f --- /dev/null +++ b/src/Symfony/Component/Finder/Shell/Command.php @@ -0,0 +1,245 @@ + + * + * 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 + */ +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; } + )); + } +} diff --git a/src/Symfony/Component/Finder/Shell/Shell.php b/src/Symfony/Component/Finder/Shell/Shell.php new file mode 100644 index 000000000000..0c8bc7ef03e3 --- /dev/null +++ b/src/Symfony/Component/Finder/Shell/Shell.php @@ -0,0 +1,86 @@ + + * + * 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 + */ +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; + } +} diff --git a/src/Symfony/Component/Finder/Tests/Expression/ExpressionTest.php b/src/Symfony/Component/Finder/Tests/Expression/ExpressionTest.php new file mode 100644 index 000000000000..c907d6a8d4d4 --- /dev/null +++ b/src/Symfony/Component/Finder/Tests/Expression/ExpressionTest.php @@ -0,0 +1,68 @@ + + * + * 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'), + ); + } +} diff --git a/src/Symfony/Component/Finder/Tests/GlobTest.php b/src/Symfony/Component/Finder/Tests/Expression/GlobTest.php similarity index 77% rename from src/Symfony/Component/Finder/Tests/GlobTest.php rename to src/Symfony/Component/Finder/Tests/Expression/GlobTest.php index 56077a10687c..fbaeb0e237c8 100644 --- a/src/Symfony/Component/Finder/Tests/GlobTest.php +++ b/src/Symfony/Component/Finder/Tests/Expression/GlobTest.php @@ -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'); } } diff --git a/src/Symfony/Component/Finder/Tests/Expression/RegexTest.php b/src/Symfony/Component/Finder/Tests/Expression/RegexTest.php new file mode 100644 index 000000000000..f252696a7625 --- /dev/null +++ b/src/Symfony/Component/Finder/Tests/Expression/RegexTest.php @@ -0,0 +1,143 @@ + + * + * 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)->setEndJoker($end); + + $this->assertEquals($expected, $expr->render()); + } + + public function testOptions() + { + $expr = new Expression('~abc~is'); + $expr->getRegex()->removeOption('i')->addOption('m'); + + $this->assertEquals('~abc~sm', $expr->render()); + } + + public function testMixFlagsAndJokers() + { + $expr = new Expression('~^.*abc.*$~is'); + + $expr->getRegex()->setStartFlag(false)->setEndFlag(false)->setStartJoker(false)->setEndJoker(false); + $this->assertEquals('~abc~is', $expr->render()); + + $expr->getRegex()->setStartFlag(true)->setEndFlag(true)->setStartJoker(true)->setEndJoker(true); + $this->assertEquals('~^.*abc.*$~is', $expr->render()); + } + + /** + * @dataProvider getReplaceJokersTestData + */ + public function testReplaceJokers($regex, $expected) + { + $expr = new Expression($regex); + $expr = $expr->getRegex()->replaceJokers('@'); + + $this->assertEquals($expected, $expr->renderPattern()); + } + + public function getHasFlagsData() + { + return array( + array('~^abc~', true, false), + array('~abc$~', false, true), + array('~abc~', false, false), + array('~^abc$~', true, true), + array('~^abc\\$~', true, false), + ); + } + + public function getHasJokersData() + { + return array( + array('~.*abc~', true, false), + array('~abc.*~', false, true), + array('~abc~', false, false), + array('~.*abc.*~', true, true), + array('~.*abc\\.*~', true, false), + ); + } + + public function getSetFlagsData() + { + return array( + array('~abc~', true, false, '~^abc~'), + array('~abc~', false, true, '~abc$~'), + array('~abc~', false, false, '~abc~'), + array('~abc~', true, true, '~^abc$~'), + ); + } + + public function getSetJokersData() + { + return array( + array('~abc~', true, false, '~.*abc~'), + array('~abc~', false, true, '~abc.*~'), + array('~abc~', false, false, '~abc~'), + array('~abc~', true, true, '~.*abc.*~'), + ); + } + + public function getReplaceJokersTestData() + { + return array( + array('~.abc~', '@abc'), + array('~\\.abc~', '\\.abc'), + array('~\\\\.abc~', '\\\\@abc'), + array('~\\\\\\.abc~', '\\\\\\.abc'), + ); + } +} diff --git a/src/Symfony/Component/Finder/Tests/FakeAdapter/DummyAdapter.php b/src/Symfony/Component/Finder/Tests/FakeAdapter/DummyAdapter.php new file mode 100644 index 000000000000..26361f803612 --- /dev/null +++ b/src/Symfony/Component/Finder/Tests/FakeAdapter/DummyAdapter.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Finder\Tests\FakeAdapter; + +use Symfony\Component\Finder\Adapter\AbstractAdapter; +use Symfony\Component\Finder\Exception\AdapterFailureException; + +/** + * @author Jean-François Simon + */ +class DummyAdapter extends AbstractAdapter +{ + /** + * @var \Iterator + */ + private $iterator; + + /** + * @param \Iterator $iterator + */ + public function __construct(\Iterator $iterator) + { + $this->iterator = $iterator; + } + + /** + * {@inheritdoc} + */ + public function searchInDirectory($dir) + { + return $this->iterator; + } + + /** + * {@inheritdoc} + */ + public function isSupported() + { + return true; + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'yes'; + } +} diff --git a/src/Symfony/Component/Finder/Tests/FakeAdapter/FailingAdapter.php b/src/Symfony/Component/Finder/Tests/FakeAdapter/FailingAdapter.php new file mode 100644 index 000000000000..00fc022fdf76 --- /dev/null +++ b/src/Symfony/Component/Finder/Tests/FakeAdapter/FailingAdapter.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Finder\Tests\FakeAdapter; + +use Symfony\Component\Finder\Adapter\AbstractAdapter; +use Symfony\Component\Finder\Exception\AdapterFailureException; + +/** + * @author Jean-François Simon + */ +class FailingAdapter extends AbstractAdapter +{ + /** + * {@inheritdoc} + */ + public function searchInDirectory($dir) + { + throw new AdapterFailureException($this); + } + + /** + * {@inheritdoc} + */ + public function isSupported() + { + return true; + } + + /** + * {@inheritdoc} + */ + function getName() + { + return 'failing'; + } +} diff --git a/src/Symfony/Component/Finder/Tests/FakeAdapter/NamedAdapter.php b/src/Symfony/Component/Finder/Tests/FakeAdapter/NamedAdapter.php new file mode 100644 index 000000000000..2321195e94b0 --- /dev/null +++ b/src/Symfony/Component/Finder/Tests/FakeAdapter/NamedAdapter.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Finder\Tests\FakeAdapter; + +use Symfony\Component\Finder\Adapter\AbstractAdapter; + +/** + * @author Jean-François Simon + */ +class NamedAdapter extends AbstractAdapter +{ + /** + * @var string + */ + private $name; + + /** + * @param string $name + */ + public function __construct($name) + { + $this->name = $name; + } + + /** + * {@inheritdoc} + */ + public function searchInDirectory($dir) + { + return new \ArrayIterator(array()); + } + + /** + * {@inheritdoc} + */ + public function isSupported() + { + return true; + } + + /** + * {@inheritdoc} + */ + function getName() + { + return $this->name; + } +} diff --git a/src/Symfony/Component/Finder/Tests/FakeAdapter/UnsupportedAdapter.php b/src/Symfony/Component/Finder/Tests/FakeAdapter/UnsupportedAdapter.php new file mode 100644 index 000000000000..d02a13e0328b --- /dev/null +++ b/src/Symfony/Component/Finder/Tests/FakeAdapter/UnsupportedAdapter.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Finder\Tests\FakeAdapter; + +use Symfony\Component\Finder\Adapter\AbstractAdapter; + +/** + * @author Jean-François Simon + */ +class UnsupportedAdapter extends AbstractAdapter +{ + /** + * {@inheritdoc} + */ + public function searchInDirectory($dir) + { + return new \ArrayIterator(array()); + } + + /** + * {@inheritdoc} + */ + public function isSupported() + { + return false; + } + + /** + * {@inheritdoc} + */ + function getName() + { + return 'unsupported'; + } +} diff --git a/src/Symfony/Component/Finder/Tests/FinderTest.php b/src/Symfony/Component/Finder/Tests/FinderTest.php index ec7db1ec0684..1baf457f7db6 100644 --- a/src/Symfony/Component/Finder/Tests/FinderTest.php +++ b/src/Symfony/Component/Finder/Tests/FinderTest.php @@ -12,6 +12,8 @@ namespace Symfony\Component\Finder\Tests; use Symfony\Component\Finder\Finder; +use Symfony\Component\Finder\Adapter; +use Symfony\Component\Finder\Tests\FakeAdapter; class FinderTest extends Iterator\RealIteratorTestCase { @@ -24,80 +26,106 @@ public static function setUpBeforeClass() self::$tmpDir = sys_get_temp_dir().'/symfony2_finder'; } - public function testCreate() + /** + * @dataProvider getAdaptersTestData + */ + public function testCreate($adapter) { $this->assertInstanceOf('Symfony\Component\Finder\Finder', Finder::create()); } - public function testDirectories() + /** + * @dataProvider getAdaptersTestData + */ + public function testDirectories($adapter) { - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $this->assertSame($finder, $finder->directories()); $this->assertIterator($this->toAbsolute(array('foo', 'toto')), $finder->in(self::$tmpDir)->getIterator()); - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $finder->directories(); $finder->files(); $finder->directories(); $this->assertIterator($this->toAbsolute(array('foo', 'toto')), $finder->in(self::$tmpDir)->getIterator()); } - public function testFiles() + /** + * @dataProvider getAdaptersTestData + */ + public function testFiles($adapter) { - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $this->assertSame($finder, $finder->files()); $this->assertIterator($this->toAbsolute(array('foo/bar.tmp', 'test.php', 'test.py')), $finder->in(self::$tmpDir)->getIterator()); - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $finder->files(); $finder->directories(); $finder->files(); $this->assertIterator($this->toAbsolute(array('foo/bar.tmp', 'test.php', 'test.py')), $finder->in(self::$tmpDir)->getIterator()); } - public function testDepth() + /** + * @dataProvider getAdaptersTestData + */ + public function testDepth($adapter) { - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $this->assertSame($finder, $finder->depth('< 1')); $this->assertIterator($this->toAbsolute(array('foo', 'test.php', 'test.py', 'toto')), $finder->in(self::$tmpDir)->getIterator()); - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $this->assertSame($finder, $finder->depth('<= 0')); $this->assertIterator($this->toAbsolute(array('foo', 'test.php', 'test.py', 'toto')), $finder->in(self::$tmpDir)->getIterator()); - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $this->assertSame($finder, $finder->depth('>= 1')); $this->assertIterator($this->toAbsolute(array('foo/bar.tmp')), $finder->in(self::$tmpDir)->getIterator()); - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $finder->depth('< 1')->depth('>= 1'); $this->assertIterator(array(), $finder->in(self::$tmpDir)->getIterator()); } - public function testName() + /** + * @dataProvider getAdaptersTestData + */ + public function testName($adapter) { - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $this->assertSame($finder, $finder->name('*.php')); $this->assertIterator($this->toAbsolute(array('test.php')), $finder->in(self::$tmpDir)->getIterator()); - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $finder->name('test.ph*'); $finder->name('test.py'); $this->assertIterator($this->toAbsolute(array('test.php', 'test.py')), $finder->in(self::$tmpDir)->getIterator()); + + $finder = $this->buildFinder($adapter); + $finder->name('~^test~i'); + $this->assertIterator($this->toAbsolute(array('test.php', 'test.py')), $finder->in(self::$tmpDir)->getIterator()); + + $finder = $this->buildFinder($adapter); + $finder->name('~\\.php$~i'); + $this->assertIterator($this->toAbsolute(array('test.php')), $finder->in(self::$tmpDir)->getIterator()); } - public function testNotName() + /** + * @dataProvider getAdaptersTestData + */ + public function testNotName($adapter) { - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $this->assertSame($finder, $finder->notName('*.php')); $this->assertIterator($this->toAbsolute(array('foo', 'foo/bar.tmp', 'test.py', 'toto')), $finder->in(self::$tmpDir)->getIterator()); - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $finder->notName('*.php'); $finder->notName('*.py'); $this->assertIterator($this->toAbsolute(array('foo', 'foo/bar.tmp', 'toto')), $finder->in(self::$tmpDir)->getIterator()); - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $finder->name('test.ph*'); $finder->name('test.py'); $finder->notName('*.php'); @@ -105,46 +133,72 @@ public function testNotName() $this->assertIterator(array(), $finder->in(self::$tmpDir)->getIterator()); } - public function testSize() + /** + * @dataProvider getRegexNameTestData + * + * @group regexName + */ + public function testRegexName($adapter, $regex) { - $finder = new Finder(); + $finder = $this->buildFinder($adapter); + $finder->name($regex); + $this->assertIterator($this->toAbsolute(array('test.py', 'test.php')), $finder->in(self::$tmpDir)->getIterator()); + } + + /** + * @dataProvider getAdaptersTestData + */ + public function testSize($adapter) + { + $finder = $this->buildFinder($adapter); $this->assertSame($finder, $finder->files()->size('< 1K')->size('> 500')); $this->assertIterator($this->toAbsolute(array('test.php')), $finder->in(self::$tmpDir)->getIterator()); } - public function testDate() + /** + * @dataProvider getAdaptersTestData + */ + public function testDate($adapter) { - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $this->assertSame($finder, $finder->files()->date('until last month')); $this->assertIterator($this->toAbsolute(array('foo/bar.tmp', 'test.php')), $finder->in(self::$tmpDir)->getIterator()); } - public function testExclude() + /** + * @dataProvider getAdaptersTestData + */ + public function testExclude($adapter) { - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $this->assertSame($finder, $finder->exclude('foo')); $this->assertIterator($this->toAbsolute(array('test.php', 'test.py', 'toto')), $finder->in(self::$tmpDir)->getIterator()); } - public function testIgnoreVCS() + /** + * @dataProvider getAdaptersTestData + */ + public function testIgnoreVCS($adapter) { - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $this->assertSame($finder, $finder->ignoreVCS(false)->ignoreDotFiles(false)); $this->assertIterator($this->toAbsolute(array('.git', 'foo', 'foo/bar.tmp', 'test.php', 'test.py', 'toto', '.bar', '.foo', '.foo/.bar')), $finder->in(self::$tmpDir)->getIterator()); - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $finder->ignoreVCS(false)->ignoreVCS(false)->ignoreDotFiles(false); $this->assertIterator($this->toAbsolute(array('.git', 'foo', 'foo/bar.tmp', 'test.php', 'test.py', 'toto', '.bar', '.foo', '.foo/.bar')), $finder->in(self::$tmpDir)->getIterator()); - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $this->assertSame($finder, $finder->ignoreVCS(true)->ignoreDotFiles(false)); $this->assertIterator($this->toAbsolute(array('foo', 'foo/bar.tmp', 'test.php', 'test.py', 'toto', '.bar', '.foo', '.foo/.bar')), $finder->in(self::$tmpDir)->getIterator()); - } - public function testIgnoreDotFiles() + /** + * @dataProvider getAdaptersTestData + */ + public function testIgnoreDotFiles($adapter) { - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $this->assertSame($finder, $finder->ignoreDotFiles(false)->ignoreVCS(false)); $this->assertIterator($this->toAbsolute(array('.git', '.bar', '.foo', '.foo/.bar', 'foo', 'foo/bar.tmp', 'test.php', 'test.py', 'toto')), $finder->in(self::$tmpDir)->getIterator()); @@ -152,75 +206,102 @@ public function testIgnoreDotFiles() $finder->ignoreDotFiles(false)->ignoreDotFiles(false)->ignoreVCS(false); $this->assertIterator($this->toAbsolute(array('.git', '.bar', '.foo', '.foo/.bar', 'foo', 'foo/bar.tmp', 'test.php', 'test.py', 'toto')), $finder->in(self::$tmpDir)->getIterator()); - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $this->assertSame($finder, $finder->ignoreDotFiles(true)->ignoreVCS(false)); $this->assertIterator($this->toAbsolute(array('foo', 'foo/bar.tmp', 'test.php', 'test.py', 'toto')), $finder->in(self::$tmpDir)->getIterator()); } - public function testSortByName() + /** + * @dataProvider getAdaptersTestData + */ + public function testSortByName($adapter) { - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $this->assertSame($finder, $finder->sortByName()); $this->assertIterator($this->toAbsolute(array('foo', 'foo/bar.tmp', 'test.php', 'test.py', 'toto')), $finder->in(self::$tmpDir)->getIterator()); } - public function testSortByType() + /** + * @dataProvider getAdaptersTestData + */ + public function testSortByType($adapter) { - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $this->assertSame($finder, $finder->sortByType()); $this->assertIterator($this->toAbsolute(array('foo', 'toto', 'foo/bar.tmp', 'test.php', 'test.py')), $finder->in(self::$tmpDir)->getIterator()); } - public function testSortByAccessedTime() + /** + * @dataProvider getAdaptersTestData + */ + public function testSortByAccessedTime($adapter) { - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $this->assertSame($finder, $finder->sortByAccessedTime()); $this->assertIterator($this->toAbsolute(array('foo/bar.tmp', 'test.php', 'toto', 'test.py', 'foo')), $finder->in(self::$tmpDir)->getIterator()); } - public function testSortByChangedTime() + /** + * @dataProvider getAdaptersTestData + */ + public function testSortByChangedTime($adapter) { - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $this->assertSame($finder, $finder->sortByChangedTime()); $this->assertIterator($this->toAbsolute(array('toto', 'test.py', 'test.php', 'foo/bar.tmp', 'foo')), $finder->in(self::$tmpDir)->getIterator()); } - public function testSortByModifiedTime() + /** + * @dataProvider getAdaptersTestData + */ + public function testSortByModifiedTime($adapter) { - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $this->assertSame($finder, $finder->sortByModifiedTime()); $this->assertIterator($this->toAbsolute(array('foo/bar.tmp', 'test.php', 'toto', 'test.py', 'foo')), $finder->in(self::$tmpDir)->getIterator()); } - public function testSort() + /** + * @dataProvider getAdaptersTestData + */ + public function testSort($adapter) { - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $this->assertSame($finder, $finder->sort(function (\SplFileInfo $a, \SplFileInfo $b) { return strcmp($a->getRealpath(), $b->getRealpath()); })); $this->assertIterator($this->toAbsolute(array('foo', 'foo/bar.tmp', 'test.php', 'test.py', 'toto')), $finder->in(self::$tmpDir)->getIterator()); } - public function testFilter() + /** + * @dataProvider getAdaptersTestData + */ + public function testFilter($adapter) { - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $this->assertSame($finder, $finder->filter(function (\SplFileInfo $f) { return preg_match('/test/', $f) > 0; })); $this->assertIterator($this->toAbsolute(array('test.php', 'test.py')), $finder->in(self::$tmpDir)->getIterator()); } - public function testFollowLinks() + /** + * @dataProvider getAdaptersTestData + */ + public function testFollowLinks($adapter) { if ('\\' == DIRECTORY_SEPARATOR) { return; } - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $this->assertSame($finder, $finder->followLinks()); $this->assertIterator($this->toAbsolute(array('foo', 'foo/bar.tmp', 'test.php', 'test.py', 'toto')), $finder->in(self::$tmpDir)->getIterator()); } - public function testIn() + /** + * @dataProvider getAdaptersTestData + */ + public function testIn($adapter) { - $finder = new Finder(); + $finder = $this->buildFinder($adapter); try { $finder->in('foobar'); $this->fail('->in() throws a \InvalidArgumentException if the directory does not exist'); @@ -228,15 +309,18 @@ public function testIn() $this->assertInstanceOf('InvalidArgumentException', $e, '->in() throws a \InvalidArgumentException if the directory does not exist'); } - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $iterator = $finder->files()->name('*.php')->depth('< 1')->in(array(self::$tmpDir, __DIR__))->getIterator(); - $this->assertIterator(array(self::$tmpDir.DIRECTORY_SEPARATOR.'test.php', __DIR__.DIRECTORY_SEPARATOR.'FinderTest.php', __DIR__.DIRECTORY_SEPARATOR.'bootstrap.php', __DIR__.DIRECTORY_SEPARATOR.'GlobTest.php'), $iterator); + $this->assertIterator(array(self::$tmpDir.DIRECTORY_SEPARATOR.'test.php', __DIR__.DIRECTORY_SEPARATOR.'FinderTest.php', __DIR__.DIRECTORY_SEPARATOR.'bootstrap.php'), $iterator); } - public function testGetIterator() + /** + * @dataProvider getAdaptersTestData + */ + public function testGetIterator($adapter) { - $finder = new Finder(); + $finder = $this->buildFinder($adapter); try { $finder->getIterator(); $this->fail('->getIterator() throws a \LogicException if the in() method has not been called'); @@ -244,7 +328,7 @@ public function testGetIterator() $this->assertInstanceOf('LogicException', $e, '->getIterator() throws a \LogicException if the in() method has not been called'); } - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $dirs = array(); foreach ($finder->directories()->in(self::$tmpDir) as $dir) { $dirs[] = (string) $dir; @@ -257,19 +341,22 @@ public function testGetIterator() $this->assertEquals($expected, $dirs, 'implements the \IteratorAggregate interface'); - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $this->assertEquals(2, iterator_count($finder->directories()->in(self::$tmpDir)), 'implements the \IteratorAggregate interface'); - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $a = iterator_to_array($finder->directories()->in(self::$tmpDir)); $a = array_values(array_map(function ($a) { return (string) $a; }, $a)); sort($a); $this->assertEquals($expected, $a, 'implements the \IteratorAggregate interface'); } - public function testRelativePath() + /** + * @dataProvider getAdaptersTestData + */ + public function testRelativePath($adapter) { - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $finder->in(self::$tmpDir); @@ -284,12 +371,15 @@ public function testRelativePath() sort($ref); sort($paths); - $this->assertEquals($paths, $ref); + $this->assertEquals($ref, $paths); } - public function testRelativePathname() + /** + * @dataProvider getAdaptersTestData + */ + public function testRelativePathname($adapter) { - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $finder->in(self::$tmpDir)->sortByName(); @@ -304,15 +394,18 @@ public function testRelativePathname() sort($paths); sort($ref); - $this->assertEquals($paths, $ref); + $this->assertEquals($ref, $paths); } - public function testAppendWithAFinder() + /** + * @dataProvider getAdaptersTestData + */ + public function testAppendWithAFinder($adapter) { - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $finder->files()->in(self::$tmpDir.DIRECTORY_SEPARATOR.'foo'); - $finder1 = new Finder(); + $finder1 = $this->buildFinder($adapter); $finder1->directories()->in(self::$tmpDir); $finder->append($finder1); @@ -320,9 +413,12 @@ public function testAppendWithAFinder() $this->assertIterator($this->toAbsolute(array('foo', 'foo/bar.tmp', 'toto')), $finder->getIterator()); } - public function testAppendWithAnArray() + /** + * @dataProvider getAdaptersTestData + */ + public function testAppendWithAnArray($adapter) { - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $finder->files()->in(self::$tmpDir.DIRECTORY_SEPARATOR.'foo'); $finder->append($this->toAbsolute(array('foo', 'toto'))); @@ -391,10 +487,11 @@ protected function toAbsoluteFixtures($files) /** * @dataProvider getContainsTestData + * @group grep */ - public function testContains($matchPatterns, $noMatchPatterns, $expected) + public function testContains($adapter, $matchPatterns, $noMatchPatterns, $expected) { - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $finder->in(__DIR__.DIRECTORY_SEPARATOR.'Fixtures') ->name('*.txt')->sortByName() ->contains($matchPatterns) @@ -403,26 +500,12 @@ public function testContains($matchPatterns, $noMatchPatterns, $expected) $this->assertIterator($this->toAbsoluteFixtures($expected), $finder); } - public function getContainsTestData() - { - return array( - array('', '', array()), - array('foo', 'bar', array()), - array('', 'foobar', array('dolor.txt', 'ipsum.txt', 'lorem.txt')), - array('lorem ipsum dolor sit amet', 'foobar', array('lorem.txt')), - array('sit', 'bar', array('dolor.txt', 'ipsum.txt', 'lorem.txt')), - array('dolor sit amet', '@^L@m', array('dolor.txt', 'ipsum.txt')), - array('/^lorem ipsum dolor sit amet$/m', 'foobar', array('lorem.txt')), - array('lorem', 'foobar', array('lorem.txt')), - - array('', 'lorem', array('dolor.txt', 'ipsum.txt')), - array('ipsum dolor sit amet', '/^IPSUM/m', array('lorem.txt')), - ); - } - - public function testContainsOnDirectory() + /** + * @dataProvider getAdaptersTestData + */ + public function testContainsOnDirectory(Adapter\AdapterInterface $adapter) { - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $finder->in(__DIR__) ->directories() ->name('Fixtures') @@ -430,9 +513,12 @@ public function testContainsOnDirectory() $this->assertIterator(array(), $finder); } - public function testNotContainsOnDirectory() + /** + * @dataProvider getAdaptersTestData + */ + public function testNotContainsOnDirectory(Adapter\AdapterInterface $adapter) { - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $finder->in(__DIR__) ->directories() ->name('Fixtures') @@ -445,8 +531,10 @@ public function testNotContainsOnDirectory() * with inner FilesystemIterator in an ivalid state. * * @see https://bugs.php.net/bug.php?id=49104 + * + * @dataProvider getAdaptersTestData */ - public function testMultipleLocations() + public function testMultipleLocations(Adapter\AdapterInterface $adapter) { $locations = array( self::$tmpDir.'/', @@ -454,9 +542,108 @@ public function testMultipleLocations() ); // it is expected that there are test.py test.php in the tmpDir - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $finder->in($locations)->depth('< 1')->name('test.php'); $this->assertEquals(1, count($finder)); } + + public function testAdaptersOrdering() + { + $finder = Finder::create() + ->removeAdapters() + ->register(new FakeAdapter\NamedAdapter('a'), 0) + ->register(new FakeAdapter\NamedAdapter('b'), -50) + ->register(new FakeAdapter\NamedAdapter('c'), 50) + ->register(new FakeAdapter\NamedAdapter('d'), -25) + ->register(new FakeAdapter\NamedAdapter('e'), 25); + + $this->assertEquals( + array('c', 'e', 'a', 'd', 'b'), + array_map(function(Adapter\AdapterInterface $adapter) { + return $adapter->getName(); + }, $finder->getAdapters()) + ); + } + + public function testAdaptersChaining() + { + $iterator = new \ArrayIterator(array()); + $filenames = $this->toAbsolute(array('foo', 'foo/bar.tmp', 'test.php', 'test.py', 'toto')); + foreach ($filenames as $file) { + $iterator->append(new \Symfony\Component\Finder\SplFileInfo($file, null, null)); + } + + $finder = Finder::create() + ->removeAdapters() + ->register(new FakeAdapter\UnsupportedAdapter(), 3) + ->register(new FakeAdapter\FailingAdapter(), 2) + ->register(new FakeAdapter\DummyAdapter($iterator), 1); + + $this->assertIterator($filenames, $finder->in(sys_get_temp_dir())->getIterator()); + } + + public function getAdaptersTestData() + { + return array_map( + function ($adapter) { return array($adapter); }, + $this->getValidAdapters() + ); + } + + public function getContainsTestData() + { + $tests = array( + array('', '', array()), + array('foo', 'bar', array()), + array('', 'foobar', array('dolor.txt', 'ipsum.txt', 'lorem.txt')), + array('lorem ipsum dolor sit amet', 'foobar', array('lorem.txt')), + array('sit', 'bar', array('dolor.txt', 'ipsum.txt', 'lorem.txt')), + array('dolor sit amet', '@^L@m', array('dolor.txt', 'ipsum.txt')), + array('/^lorem ipsum dolor sit amet$/m', 'foobar', array('lorem.txt')), + array('lorem', 'foobar', array('lorem.txt')), + + array('', 'lorem', array('dolor.txt', 'ipsum.txt')), + array('ipsum dolor sit amet', '/^IPSUM/m', array('lorem.txt')), + ); + + return $this->buildTestData($tests); + } + + public function getRegexNameTestData() + { + $tests = array( + array('~.+\\.p.+~i'), + array('~t.*s~i'), + ); + + return $this->buildTestData($tests); + } + + private function buildFinder(Adapter\AdapterInterface $adapter) + { + return Finder::create() + ->removeAdapters() + ->register($adapter); + } + + private function getValidAdapters() + { + return array_filter( + array(new Adapter\GnuFindAdapter(), new Adapter\PhpAdapter()), + function (Adapter\AdapterInterface $adapter) { return $adapter->isSupported(); } + ); + } + + private function buildTestData(array $tests) + { + $data = array(); + foreach ($this->getValidAdapters() as $adapter) { + foreach ($tests as $test) { + $data[] = array_merge(array($adapter), $test); + } + } + + return $data; + } } diff --git a/src/Symfony/Component/Finder/Tests/Iterator/DepthRangeIteratorTest.php b/src/Symfony/Component/Finder/Tests/Iterator/DepthRangeIteratorTest.php index 43ad4599496c..68c81e1168a2 100644 --- a/src/Symfony/Component/Finder/Tests/Iterator/DepthRangeIteratorTest.php +++ b/src/Symfony/Component/Finder/Tests/Iterator/DepthRangeIteratorTest.php @@ -19,11 +19,11 @@ class DepthRangeFilterIteratorTest extends RealIteratorTestCase /** * @dataProvider getAcceptData */ - public function testAccept($size, $expected) + public function testAccept($minDepth, $maxDepth, $expected) { $inner = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($this->getAbsolutePath(''), \FilesystemIterator::SKIP_DOTS), \RecursiveIteratorIterator::SELF_FIRST); - $iterator = new DepthRangeFilterIterator($inner, $size); + $iterator = new DepthRangeFilterIterator($inner, $minDepth, $maxDepth); $actual = array_keys(iterator_to_array($iterator)); sort($expected); @@ -34,11 +34,11 @@ public function testAccept($size, $expected) public function getAcceptData() { return array( - array(array(new NumberComparator('< 1')), array($this->getAbsolutePath('/.git'), $this->getAbsolutePath('/test.py'), $this->getAbsolutePath('/foo'), $this->getAbsolutePath('/test.php'), $this->getAbsolutePath('/toto'), $this->getAbsolutePath('/.foo'), $this->getAbsolutePath('/.bar'))), - array(array(new NumberComparator('<= 1')), array($this->getAbsolutePath('/.git'), $this->getAbsolutePath('/test.py'), $this->getAbsolutePath('/foo'), $this->getAbsolutePath('/foo/bar.tmp'), $this->getAbsolutePath('/test.php'), $this->getAbsolutePath('/toto'), $this->getAbsolutePath('/.foo'), $this->getAbsolutePath('/.foo/.bar'), $this->getAbsolutePath('/.bar'))), - array(array(new NumberComparator('> 1')), array()), - array(array(new NumberComparator('>= 1')), array($this->getAbsolutePath('/foo/bar.tmp'), $this->getAbsolutePath('/.foo/.bar'))), - array(array(new NumberComparator('1')), array($this->getAbsolutePath('/foo/bar.tmp'), $this->getAbsolutePath('/.foo/.bar'))), + array(0, 0, array($this->getAbsolutePath('/.git'), $this->getAbsolutePath('/test.py'), $this->getAbsolutePath('/foo'), $this->getAbsolutePath('/test.php'), $this->getAbsolutePath('/toto'), $this->getAbsolutePath('/.foo'), $this->getAbsolutePath('/.bar'))), + array(0, 1, array($this->getAbsolutePath('/.git'), $this->getAbsolutePath('/test.py'), $this->getAbsolutePath('/foo'), $this->getAbsolutePath('/foo/bar.tmp'), $this->getAbsolutePath('/test.php'), $this->getAbsolutePath('/toto'), $this->getAbsolutePath('/.foo'), $this->getAbsolutePath('/.foo/.bar'), $this->getAbsolutePath('/.bar'))), + array(2, INF, array()), + array(1, INF, array($this->getAbsolutePath('/foo/bar.tmp'), $this->getAbsolutePath('/.foo/.bar'))), + array(1, 1, array($this->getAbsolutePath('/foo/bar.tmp'), $this->getAbsolutePath('/.foo/.bar'))), ); } diff --git a/src/Symfony/Component/Finder/Tests/Iterator/FilePathsIteratorTest.php b/src/Symfony/Component/Finder/Tests/Iterator/FilePathsIteratorTest.php new file mode 100644 index 000000000000..c60a90656a55 --- /dev/null +++ b/src/Symfony/Component/Finder/Tests/Iterator/FilePathsIteratorTest.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Finder\Tests\Iterator; + +use Symfony\Component\Finder\Iterator\FilePathsIterator; + +class FilePathsIteratorTest extends RealIteratorTestCase +{ + /** + * @dataProvider getSubPathData + */ + public function testSubPath($baseDir, array $paths, array $subPaths, array $subPathnames) + { + $iterator = new FilePathsIterator($paths, $baseDir); + + foreach ($iterator as $index => $file) { + $this->assertEquals($paths[$index], $file->getPathname()); + $this->assertEquals($subPaths[$index], $iterator->getSubPath()); + $this->assertEquals($subPathnames[$index], $iterator->getSubPathname()); + } + } + + public function getSubPathData() + { + $tmpDir = sys_get_temp_dir().'/symfony2_finder'; + + return array( + array( + $tmpDir, + array( // paths + $tmpDir.DIRECTORY_SEPARATOR.'.git', + $tmpDir.DIRECTORY_SEPARATOR.'test.py', + $tmpDir.DIRECTORY_SEPARATOR.'foo', + $tmpDir.DIRECTORY_SEPARATOR.'foo'.DIRECTORY_SEPARATOR.'bar.tmp', + $tmpDir.DIRECTORY_SEPARATOR.'test.php', + $tmpDir.DIRECTORY_SEPARATOR.'toto' + ), + array( // subPaths + '', + '', + '', + 'foo', + '', + '' + ), + array( // subPathnames + '.git', + 'test.py', + 'foo', + 'foo'.DIRECTORY_SEPARATOR.'bar.tmp', + 'test.php', + 'toto' + ), + ), + ); + } +} diff --git a/src/Symfony/Component/Finder/Tests/Iterator/IteratorTestCase.php b/src/Symfony/Component/Finder/Tests/Iterator/IteratorTestCase.php index 8810ce757d25..226469b4e7a9 100644 --- a/src/Symfony/Component/Finder/Tests/Iterator/IteratorTestCase.php +++ b/src/Symfony/Component/Finder/Tests/Iterator/IteratorTestCase.php @@ -15,7 +15,9 @@ abstract class IteratorTestCase extends \PHPUnit_Framework_TestCase { protected function assertIterator($expected, \Traversable $iterator) { - $values = array_map(function (\SplFileInfo $fileinfo) { return $fileinfo->getPathname(); }, iterator_to_array($iterator)); + // set iterator_to_array $use_key to false to avoid values merge + // this made FinderTest::testAppendWithAnArray() failed with GnuFinderAdapter + $values = array_map(function (\SplFileInfo $fileinfo) { return $fileinfo->getPathname(); }, iterator_to_array($iterator, false)); sort($values); sort($expected);