Skip to content

Commit 2401274

Browse files
committed
[Finder] Added bsd adapter (need tests).
1 parent f61457d commit 2401274

File tree

4 files changed

+411
-306
lines changed

4 files changed

+411
-306
lines changed
Lines changed: 352 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,352 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Finder\Adapter;
13+
14+
use Symfony\Component\Finder\Iterator;
15+
use Symfony\Component\Finder\Shell\Shell;
16+
use Symfony\Component\Finder\Expression\Expression;
17+
use Symfony\Component\Finder\Shell\Command;
18+
use Symfony\Component\Finder\Iterator\SortableIterator;
19+
use Symfony\Component\Finder\Comparator\NumberComparator;
20+
use Symfony\Component\Finder\Comparator\DateComparator;
21+
22+
/**
23+
* Shell engine implementation using GNU find command.
24+
*
25+
* @author Jean-François Simon <[email protected]>
26+
*/
27+
abstract class AbstractFindAdapter extends AbstractAdapter
28+
{
29+
/**
30+
* @var Shell
31+
*/
32+
protected $shell;
33+
34+
/**
35+
* Constructor.
36+
*/
37+
public function __construct()
38+
{
39+
$this->shell = new Shell();
40+
}
41+
42+
/**
43+
* {@inheritdoc}
44+
*/
45+
public function searchInDirectory($dir)
46+
{
47+
// having "/../" in path make find fail
48+
$dir = realpath($dir);
49+
50+
// searching directories containing or not containing strings leads to no result
51+
if (Iterator\FileTypeFilterIterator::ONLY_DIRECTORIES === $this->mode && ($this->contains || $this->notContains)) {
52+
return new Iterator\FilePathsIterator(array(), $dir);
53+
}
54+
55+
$command = Command::create();
56+
57+
$find = $command
58+
->ins('find')
59+
->add('find ')
60+
->arg($dir)
61+
->add('-noleaf') // -noleaf option is required for filesystems who doesn't follow '.' and '..' convention
62+
->add('-regextype posix-extended');
63+
64+
if ($this->followLinks) {
65+
$find->add('-follow');
66+
}
67+
68+
$find->add('-mindepth')->add($this->minDepth+1);
69+
// warning! INF < INF => true ; INF == INF => false ; INF === INF => true
70+
// https://bugs.php.net/bug.php?id=9118
71+
if (INF !== $this->maxDepth) {
72+
$find->add('-maxdepth')->add($this->maxDepth+1);
73+
}
74+
75+
if (Iterator\FileTypeFilterIterator::ONLY_DIRECTORIES === $this->mode) {
76+
$find->add('-type d');
77+
} elseif (Iterator\FileTypeFilterIterator::ONLY_FILES === $this->mode) {
78+
$find->add('-type f');
79+
}
80+
81+
$this->buildNamesFiltering($find, $this->names);
82+
$this->buildNamesFiltering($find, $this->notNames, true);
83+
$this->buildPathsFiltering($find, $dir, $this->paths);
84+
$this->buildPathsFiltering($find, $dir, $this->notPaths, true);
85+
$this->buildSizesFiltering($find, $this->sizes);
86+
$this->buildDatesFiltering($find, $this->dates);
87+
88+
$useGrep = $this->shell->testCommand('grep') && $this->shell->testCommand('xargs');
89+
$useSort = is_int($this->sort) && $this->shell->testCommand('sort') && $this->shell->testCommand('cut');
90+
91+
if ($useGrep && ($this->contains || $this->notContains)) {
92+
$grep = $command->ins('grep');
93+
$this->buildContentFiltering($grep, $this->contains);
94+
$this->buildContentFiltering($grep, $this->notContains, true);
95+
}
96+
97+
if ($useSort) {
98+
$this->buildSorting($command, $this->sort);
99+
}
100+
101+
$paths = $this->shell->testCommand('uniq') ? $command->add('| uniq')->execute() : array_unique($command->execute());
102+
$iterator = new Iterator\FilePathsIterator($paths, $dir);
103+
104+
if ($this->exclude) {
105+
$iterator = new Iterator\ExcludeDirectoryFilterIterator($iterator, $this->exclude);
106+
}
107+
108+
if (!$useGrep && ($this->contains || $this->notContains)) {
109+
$iterator = new Iterator\FilecontentFilterIterator($iterator, $this->contains, $this->notContains);
110+
}
111+
112+
if ($this->filters) {
113+
$iterator = new Iterator\CustomFilterIterator($iterator, $this->filters);
114+
}
115+
116+
if (!$useSort && $this->sort) {
117+
$iteratorAggregate = new Iterator\SortableIterator($iterator, $this->sort);
118+
$iterator = $iteratorAggregate->getIterator();
119+
}
120+
121+
return $iterator;
122+
}
123+
124+
/**
125+
* {@inheritdoc}
126+
*/
127+
public function isSupported()
128+
{
129+
return $this->shell->testCommand('find');
130+
}
131+
132+
/**
133+
* @param Command $command
134+
* @param string[] $names
135+
* @param bool $not
136+
*/
137+
private function buildNamesFiltering(Command $command, array $names, $not = false)
138+
{
139+
if (0 === count($names)) {
140+
return;
141+
}
142+
143+
$command->add($not ? '-not' : null)->cmd('(');
144+
145+
foreach ($names as $i => $name) {
146+
$expr = Expression::create($name);
147+
148+
// Fixes 'not search' and 'full path matching' regex problems.
149+
// - Jokers '.' are replaced by [^/].
150+
// - We add '[^/]*' before and after regex (if no ^|$ flags are present).
151+
if ($expr->isRegex()) {
152+
$regex = $expr->getRegex();
153+
$regex->prepend($regex->hasStartFlag() ? '/' : '/[^/]*')
154+
->setStartFlag(false)
155+
->setStartJoker(true)
156+
->replaceJokers('[^/]');
157+
if (!$regex->hasEndFlag() || $regex->hasEndJoker()) {
158+
$regex->setEndJoker(false)->append('[^/]*');
159+
}
160+
}
161+
162+
$command
163+
->add($i > 0 ? '-or' : null)
164+
->add($expr->isRegex()
165+
? ($expr->isCaseSensitive() ? '-regex' : '-iregex')
166+
: ($expr->isCaseSensitive() ? '-name' : '-iname')
167+
)
168+
->arg($expr->renderPattern());
169+
}
170+
171+
$command->cmd(')');
172+
}
173+
174+
/**
175+
* @param Command $command
176+
* @param string $dir
177+
* @param string[] $paths
178+
* @param bool $not
179+
* @return void
180+
*/
181+
private function buildPathsFiltering(Command $command, $dir, array $paths, $not = false)
182+
{
183+
if (0 === count($paths)) {
184+
return;
185+
}
186+
187+
$command->add($not ? '-not' : null)->cmd('(');
188+
189+
foreach ($paths as $i => $path) {
190+
$expr = Expression::create($path);
191+
192+
// Fixes 'not search' regex problems.
193+
if ($expr->isRegex()) {
194+
$regex = $expr->getRegex();
195+
$regex->prepend($regex->hasStartFlag() ? '' : '.*')->setEndJoker(!$regex->hasEndFlag());
196+
} else {
197+
$expr->prepend('*')->append('*');
198+
}
199+
200+
$command
201+
->add($i > 0 ? '-or' : null)
202+
->add($expr->isRegex()
203+
? ($expr->isCaseSensitive() ? '-regex' : '-iregex')
204+
: ($expr->isCaseSensitive() ? '-path' : '-ipath')
205+
)
206+
->arg($expr->prepend($dir.DIRECTORY_SEPARATOR)->renderPattern());
207+
}
208+
209+
$command->cmd(')');
210+
}
211+
212+
/**
213+
* @param Command $command
214+
* @param NumberComparator[] $sizes
215+
*/
216+
private function buildSizesFiltering(Command $command, array $sizes)
217+
{
218+
foreach ($sizes as $i => $size) {
219+
$command->add($i > 0 ? '-and' : null);
220+
221+
if ('<=' === $size->getOperator()) {
222+
$command->add('-size -'.($size->getTarget()+1).'c');
223+
continue;
224+
}
225+
226+
if ('<' === $size->getOperator()) {
227+
$command->add('-size -'.$size->getTarget().'c');
228+
continue;
229+
}
230+
231+
if ('>=' === $size->getOperator()) {
232+
$command->add('-size +'.($size->getTarget()-1).'c');
233+
continue;
234+
}
235+
236+
if ('>' === $size->getOperator()) {
237+
$command->add('-size +'.$size->getTarget().'c');
238+
continue;
239+
}
240+
241+
if ('!=' === $size->getOperator()) {
242+
$command->add('-size -'.$size->getTarget().'c');
243+
$command->add('-size +'.$size->getTarget().'c');
244+
continue;
245+
}
246+
247+
$command->add('-size '.$size->getTarget().'c');
248+
}
249+
}
250+
251+
/**
252+
* @param Command $command
253+
* @param DateComparator[] $dates
254+
*/
255+
private function buildDatesFiltering(Command $command, array $dates)
256+
{
257+
foreach ($dates as $i => $date) {
258+
$command->add($i > 0 ? '-and' : null);
259+
260+
$mins = (int) round((time()-$date->getTarget())/60);
261+
262+
if (0 > $mins) {
263+
// mtime is in the future
264+
$command->add(' -mmin -0');
265+
// we will have no result so we don't need to continue
266+
return;
267+
}
268+
269+
if ('<=' === $date->getOperator()) {
270+
$command->add('-mmin +'.($mins-1));
271+
continue;
272+
}
273+
274+
if ('<' === $date->getOperator()) {
275+
$command->add('-mmin +'.$mins);
276+
continue;
277+
}
278+
279+
if ('>=' === $date->getOperator()) {
280+
$command->add('-mmin -'.($mins+1));
281+
continue;
282+
}
283+
284+
if ('>' === $date->getOperator()) {
285+
$command->add('-mmin -'.$mins);
286+
continue;
287+
}
288+
289+
if ('!=' === $date->getOperator()) {
290+
$command->add('-mmin +'.$mins.' -or -mmin -'.$mins);
291+
continue;
292+
}
293+
294+
$command->add('-mmin '.$mins);
295+
}
296+
}
297+
298+
/**
299+
* @param Command $command
300+
* @param array $contains
301+
* @param bool $not
302+
*/
303+
private function buildContentFiltering(Command $command, array $contains, $not = false)
304+
{
305+
foreach ($contains as $contain) {
306+
$expr = Expression::create($contain);
307+
308+
// todo: avoid forking process for each $pattern by using multiple -e options
309+
$command
310+
->add('| xargs -r grep -I')
311+
->add($expr->isCaseSensitive() ? null : '-i')
312+
->add($not ? '-L' : '-l')
313+
->add('-Ee')->arg($expr->renderPattern());
314+
}
315+
}
316+
317+
/**
318+
* @param \Symfony\Component\Finder\Shell\Command $command
319+
* @param string $sort
320+
* @throws \InvalidArgumentException
321+
*/
322+
private function buildSorting(Command $command, $sort)
323+
{
324+
switch ($sort) {
325+
case SortableIterator::SORT_BY_NAME:
326+
$command->ins('sort')->add('| sort');
327+
return;
328+
case SortableIterator::SORT_BY_TYPE:
329+
$format = '%y';
330+
break;
331+
case SortableIterator::SORT_BY_ACCESSED_TIME:
332+
$format = '%A@';
333+
break;
334+
case SortableIterator::SORT_BY_CHANGED_TIME:
335+
$format = '%C@';
336+
break;
337+
case SortableIterator::SORT_BY_MODIFIED_TIME:
338+
$format = '%T@';
339+
break;
340+
default:
341+
throw new \InvalidArgumentException('Unknown sort options: '.$sort.'.');
342+
}
343+
344+
$this->buildFormatSorting($command, $format);
345+
}
346+
347+
/**
348+
* @param Command $command
349+
* @param string $format
350+
*/
351+
abstract protected function buildFormatSorting(Command $command, $format);
352+
}

0 commit comments

Comments
 (0)