Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add **, match anything including /, to WildcardPattern #67

Merged
merged 21 commits into from
Feb 10, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Remove withExactSlashes()
  • Loading branch information
samdark committed Feb 10, 2021
commit 38fd39d8e2609e6c9e7441be0e0b3a7006b9b1e6
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Yii Strings Change Log


## 1.2.1 under development
## 2.0.0 under development

- Enh: Add `**`, match anything including `/`, to `WildcardPattern` (samdark)
- Enh #67: Add `**`, match anything including `/`, to `WildcardPattern`, remove `withExactSlashes()` (samdark)

## 1.2.0 January 22, 2021

Expand Down
45 changes: 7 additions & 38 deletions src/WildcardPattern.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,21 @@
namespace Yiisoft\Strings;

/**
* A shell wildcard pattern to match strings against.
* A wildcard pattern to match strings against.
*
* - `\` escapes other special characters if usage of escape character is not turned off.
* - `*` matches any string, including the empty string.
* Does not match slashes if {@see WildcardPattern::withExactSlashes()} is used.
* - `**` always matches any string, including the empty string and slashes.
* - `*` matches any string including the empty string. Slashes do not match.
* - `**` matches any string including the empty string and slashes.
* - `?` matches any single character.
* - `[seq]` matches any character in seq.
* - `[a-z]` matches any character from a to z.
* - `[!seq]` matches any character not in seq.
* - `[[:alnum:]]` matches POSIX style character classes,
* see {@see https://www.php.net/manual/en/regexp.reference.character-classes.php}.
*
* @see https://www.man7.org/linux/man-pages/man7/glob.7.html
*
* The class emulates {@see fnmatch()} using PCRE since it is not uniform across operating systems
* and may not be available.
*/
final class WildcardPattern
{
private bool $withoutEscape = false;
private bool $matchSlashesExactly = false;
private bool $matchLeadingPeriodExactly = false;
private bool $ignoreCase = false;
private bool $matchEnding = false;
Expand All @@ -53,23 +46,19 @@ public function match(string $string): bool
return true;
}

if ($this->pattern === '*' && !$this->matchSlashesExactly && !$this->matchLeadingPeriodExactly) {
return true;
}

$pattern = $this->pattern;

if ($this->matchLeadingPeriodExactly) {
$pattern = preg_replace('/^[*?]/', '[!.]', $pattern);
}

$replacements = [
'\*\*' => '[^\\\\]*',
'\*\*' => '.*',
'\\\\\\\\' => '\\\\',
'\\\\\\*' => '[*]',
'\\\\\\?' => '[?]',
'\*' => '.*',
'\?' => '.',
'\*' => '[^/\\\\]*',
'\?' => '[^/\\\\]',
'\[\!' => '[^',
'\[' => '[',
'\]' => ']',
Expand All @@ -80,11 +69,6 @@ public function match(string $string): bool
unset($replacements['\\\\\\\\'], $replacements['\\\\\\*'], $replacements['\\\\\\?']);
}

if ($this->matchSlashesExactly) {
$replacements['\*'] = '[^/\\\\]*';
$replacements['\?'] = '[^/\\\\]';
}

$pattern = strtr(preg_quote($pattern, '#'), $replacements);
$pattern = '#' . ($this->matchEnding ? '' : '^') . $pattern . '$#us';

Expand All @@ -109,21 +93,6 @@ public function withoutEscape(bool $flag = true): self
return $new;
}

/**
* Do not match `/` character with wildcards. The only way to match `/` is with an explicit `/` in pattern.
* Useful for matching file paths. Use with {@see withExactLeadingPeriod()}.
*
* @param bool $flag
*
* @return self
*/
public function withExactSlashes(bool $flag = true): self
{
$new = clone $this;
$new->matchSlashesExactly = $flag;
return $new;
}

/**
* Make pattern case insensitive.
*
Expand All @@ -140,7 +109,7 @@ public function ignoreCase(bool $flag = true): self

/**
* Do not match `.` character at the beginning of string with wildcards.
* Useful for matching file paths. Use with {@see withExactSlashes()}.
* Useful for matching file paths.
*
* @param bool $flag
*
Expand Down
60 changes: 24 additions & 36 deletions tests/WildcardPatternTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
final class WildcardPatternTest extends TestCase
{
/**
* Data provider for [[testMatchWildcard()]]
* Data provider for {@see testMatchWildcard()}.
*
* @return array test data.
*/
Expand All @@ -28,6 +28,16 @@ public function dataProviderMatchWildcard(): array
['begin*', 'begin-end', true],
['begin*', 'end', false],
['begin*', 'before-begin', false],
// * with slashes
['begin/*/end', 'begin/middle/end', true],
['begin/*/end', 'begin/two/steps/end', false],
['begin/*/end', 'begin/end', false],
['begin\\\\*\\\\end', 'begin\middle\end', true],
['begin\\\\*\\\\end', 'begin\two\steps\end', false],
['begin\\\\*\\\\end', 'begin\end', false],
// **
['from/**/b', 'from/a/to/b', true],
['from\\\\**\\\\b', 'from\a\to\b', true],
// ?
['begin?end', 'begin1end', true],
['begin?end', 'beginend', false],
Expand All @@ -50,13 +60,6 @@ public function dataProviderMatchWildcard(): array
// -
['a-z', 'a-z', true],
['a-z', 'a-c', false],
// slashes
['begin/*/end', 'begin/middle/end', true],
['begin/*/end', 'begin/two/steps/end', true],
['begin/*/end', 'begin/end', false],
['begin\\\\*\\\\end', 'begin\middle\end', true],
['begin\\\\*\\\\end', 'begin\two\steps\end', true],
['begin\\\\*\\\\end', 'begin\end', false],
// dots
['begin.*.end', 'begin.middle.end', true],
['begin.*.end', 'begin.two.steps.end', true],
Expand All @@ -71,29 +74,29 @@ public function dataProviderMatchWildcard(): array
['begin*end', 'BEGIN-middle-END', false],
['begin*end', 'BEGIN-middle-END', true, ['caseSensitive' => false]],
// file path
['begin/*/end', 'begin/middle/end', true, ['filePath' => true]],
['begin/*/end', 'begin/two/steps/end', false, ['filePath' => true]],
['begin\\\\*\\\\end', 'begin\middle\end', true, ['filePath' => true]],
['begin\\\\*\\\\end', 'begin\two\steps\end', false, ['filePath' => true]],
['*', 'any', true, ['filePath' => true]],
['*', 'any/path', false, ['filePath' => true]],
['[.-0]', 'any/path', false, ['filePath' => true]],
['*', '.dotenv', true, ['filePath' => true]],
['begin/*/end', 'begin/middle/end', true],
['begin/*/end', 'begin/two/steps/end', false],
['begin\\\\*\\\\end', 'begin\middle\end', true],
['begin\\\\*\\\\end', 'begin\two\steps\end', false],
['*', 'any', true],
['*', 'any/path', false],
['[.-0]', 'any/path', false],
['*', '.dotenv', true],
// escaping
['\*\?', '*?', true],
['\*\?', 'zz', false],
['begin\*\end', 'begin\middle\end', true, ['escape' => false]],
['begin\*\end', 'begin\two\steps\end', true, ['escape' => false]],
['begin\*\end', 'begin\two\steps\end', false, ['escape' => false]],
['begin\*\end', 'begin\end', false, ['escape' => false]],
['begin\*\end', 'begin\middle\end', true, ['filePath' => true, 'escape' => false]],
['begin\*\end', 'begin\two\steps\end', false, ['filePath' => true, 'escape' => false]],
// ending
['i/*.jpg', 'i/hello.jpg', true, ['ending' => true]],
['i/*.jpg', 'i/hello.jpg', true, ['ending' => true, 'filePath' => true]],
['i/*.jpg', 'i/h/hello.jpg', true, ['ending' => true]],
['i/*.jpg', 'i/h/hello.jpg', false, ['ending' => true, 'filePath' => true]],
['i/*.jpg', 'i/hello.jpg', true, ['ending' => true]],
['i/*.jpg', 'i/h/hello.jpg', false, ['ending' => true]],
['i/*.jpg', 'i/h/hello.jpg', false, ['ending' => true]],
['i/*.jpg', 'path/to/i/hello.jpg', true, ['ending' => true]],
['i/*.jpg', 'path/to/i/hello.jpg', true, ['ending' => true]],
['i/*.jpg', 'path/to/i/hello.jpg', true, ['ending' => true, 'filePath' => true]],
];
}

Expand All @@ -117,9 +120,6 @@ private function getWildcardPattern(string $pattern, array $options): WildcardPa
if (isset($options['caseSensitive']) && $options['caseSensitive'] === false) {
$wildcardPattern = $wildcardPattern->ignoreCase();
}
if (isset($options['filePath']) && $options['filePath'] === true) {
$wildcardPattern = $wildcardPattern->withExactSlashes();
}
if (isset($options['escape']) && $options['escape'] === false) {
$wildcardPattern = $wildcardPattern->withoutEscape();
}
Expand All @@ -145,11 +145,6 @@ public function testDisableOptions(): void
->withoutEscape(false);
$this->assertTrue($wildcardPattern->match('*42'));

$wildcardPattern = (new WildcardPattern('/*/42'))
->withExactSlashes()
->withExactSlashes(false);
$this->assertTrue($wildcardPattern->match('/a/b/c/42'));

$wildcardPattern = (new WildcardPattern('*/42'))
->withExactLeadingPeriod()
->withExactLeadingPeriod(false);
Expand All @@ -166,14 +161,7 @@ public function testImmutability(): void
$original = new WildcardPattern('*');
$this->assertNotSame($original, $original->withExactLeadingPeriod());
$this->assertNotSame($original, $original->ignoreCase());
$this->assertNotSame($original, $original->withExactSlashes());
$this->assertNotSame($original, $original->withoutEscape());
$this->assertNotSame($original, $original->withEnding());
}

public function testDoubleStar(): void
{
$pattern = (new WildcardPattern('from/**/b'))->withExactSlashes(true);
$this->assertTrue($pattern->match('from/a/to/b'));
}
}