Glob support
thekid opened this issue · 5 comments
Should something along the lines of the following be part of io.FolderEntries
?
class Glob implements IteratorAggregate {
public function __construct(private Path $base, private string $pattern) { }
public function getIterator(): Traversable {
foreach (glob($this->base.$this->pattern, GLOB_NOSORT | GLOB_BRACE) as $file) {
yield new Path($file);
}
}
}
Usage idea:
$f= new Folder($argv[1]);
foreach ($f->entries()->matching('*.jpg') as $image) {
// ...
}
Implementation:
diff --git a/src/main/php/io/FolderEntries.class.php b/src/main/php/io/FolderEntries.class.php
index c1718b3a3..3b0d77abd 100755
--- a/src/main/php/io/FolderEntries.class.php
+++ b/src/main/php/io/FolderEntries.class.php
@@ -31,6 +31,17 @@ class FolderEntries implements IteratorAggregate {
return new Path($this->base, $name);
}
+ /**
+ * Iterates over all entries matching a given pattern
+ *
+ * @see https://www.php.net/glob
+ */
+ public function matching(string $pattern): iterable {
+ foreach (glob(rtrim($this->base, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.$pattern, GLOB_NOSORT | GLOB_BRACE) as $match) {
+ yield basename($match) => new Path($match);
+ }
+ }
+
/** Iterate over all entries */
public function getIterator(): Traversable {
if (null === $this->handle) {
diff --git a/src/test/php/net/xp_framework/unittest/io/FolderEntriesTest.class.php b/src/test/php/net/xp_framework/unittest/io/FolderEntriesTest.class.php
index 3a2dea37d..36c332023 100755
--- a/src/test/php/net/xp_framework/unittest/io/FolderEntriesTest.class.php
+++ b/src/test/php/net/xp_framework/unittest/io/FolderEntriesTest.class.php
@@ -105,4 +105,41 @@ class FolderEntriesTest extends \unittest\TestCase {
public function named_dot() {
$this->assertEquals(new Path($this->folder), (new FolderEntries($this->folder))->named('.'));
}
+
+ #[Test]
+ public function entries_matching() {
+ (new File($this->folder, 'a.txt'))->touch();
+ (new File($this->folder, 'b.txt'))->touch();
+ (new File($this->folder, 'not-found'))->touch();
+
+ $this->assertEquals(
+ ['a.txt' => new Path($this->folder, 'a.txt'), 'b.txt' => new Path($this->folder, 'b.txt')],
+ iterator_to_array((new FolderEntries($this->folder))->matching('*.txt'))
+ );
+ }
+
+ #[Test]
+ public function entries_matching_ignores_hidden_files() {
+ (new File($this->folder, '.hidden.txt'))->touch();
+
+ $this->assertEquals([], iterator_to_array((new FolderEntries($this->folder))->matching('*.txt')));
+ }
+
+ #[Test]
+ public function entries_matching_is_case_sensitive() {
+ (new File($this->folder, 'C.TXT'))->touch();
+
+ $this->assertEquals([], iterator_to_array((new FolderEntries($this->folder))->matching('*.txt')));
+ }
+
+ #[Test]
+ public function entries_matching_supports_braces() {
+ (new File($this->folder, 'a.txt'))->touch();
+ (new File($this->folder, 'C.TXT'))->touch();
+
+ $this->assertEquals(
+ ['a.txt' => new Path($this->folder, 'a.txt'), 'C.TXT' => new Path($this->folder, 'C.TXT')],
+ iterator_to_array((new FolderEntries($this->folder))->matching('*.{txt,TXT}'))
+ );
+ }
}
\ No newline at end of file
https://gist.github.com/thekid/795c35bec603c0f217cb5445cc901ded could be simplified if this was implemented:
- class Glob implements IteratorAggregate {
-
- public function __construct(private Path $base, private string $pattern) { }
-
- public function getIterator(): Traversable {
- foreach (glob($this->base.$this->pattern, GLOB_NOSORT | GLOB_BRACE) as $file) {
- yield new Path($file);
- }
- }
- }
@@ ... @@
- foreach ($p->isFile() ? [$p] : new Glob($p, 'thumb*.webp') as $entry) { ... }
+ foreach ($p->isFile() ? [$p] : $p->entries()->matching('thumb*.webp') as $entry) { ... }
Should there also be io.Path::match()
which uses https://www.php.net/fnmatch? - including a replacement for non-POSIX compliant systems except Windows where this function is not available.
Should there also be
io.Path::match()
which uses https://www.php.net/fnmatch? - including a replacement for non-POSIX compliant systems except Windows where this function is not available.
Using fnmatch()
means an inconsistency with glob()
, the first doesn't support braces. Here's a replacement:
class Path {
// ...
public function matches($pattern): bool {
$regex= '';
$o= 0;
$l= strlen($pattern);
do {
$s= strcspn($pattern, '.*?[]{}\\', $o);
$regex.= substr($pattern, $o, $s);
$o+= $s;
switch ($pattern[$o] ?? null) {
case null: break;
case '.': $regex.= '\\.'; break;
case '*': $regex.= '.*'; break;
case '\\': $regex.= '\\'.$pattern[++$o]; break;
case '{':
$s= strcspn($pattern, '}', $o);
$regex.= '('.strtr(substr($pattern, $o + 1, $s - 1), ',', '|').')';
$o+= $s;
break;
case '[':
$s= strcspn($pattern, ']', $o);
$regex.= '['.('!' === $pattern[$o + 1] ? '^' : $pattern[$o + 1]).substr($pattern, $o + 2, $s - 2).']';
$o+= $s;
break;
}
} while (++$o < $l);
return preg_match('~'.$regex.'$~', $this->path);
}
}
Alternatively, move this to a separate library, which also supersedes https://github.com/xp-framework/io-collections in Sequence
-style interface:
Files::in($folder)
->matching('thumb*.webp')
->toMap(fn($file) => yield $file->name => $file->size())
;