Passed
Pull Request — master (#39)
by Alexander
06:59
created

WildcardPattern::match()   B

Complexity

Conditions 8
Paths 17

Size

Total Lines 41
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 8

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 8
eloc 25
c 1
b 0
f 0
nc 17
nop 1
dl 0
loc 41
ccs 17
cts 17
cp 1
crap 8
rs 8.4444
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Strings;
6
7
/**
8
 * A shell wildcard pattern to match strings against.
9
 *
10
 * - `\` escapes other special characters if usage of escape character is not turned off.
11
 * - `*` matches any string, including the empty string.
12
 * - `?` matches any single character.
13
 * - `[seq]` matches any character in seq.
14
 * - `[a-z]` matches any character from a to z.
15
 * - `[!seq]` matches any character not in seq.
16
 * - `[[:alnum:]]` matches POSIX style character classes,
17
 *   see {@see https://www.php.net/manual/en/regexp.reference.character-classes.php}.
18
 *
19
 * @see https://www.man7.org/linux/man-pages/man7/glob.7.html
20
 *
21
 * The class emulates {@see fnmatch()} using PCRE since it is not uniform across operating systems
22
 * and may not be available.
23
 */
24
final class WildcardPattern
25
{
26
    private bool $withoutEscape = false;
27
    private bool $matchSlashesExactly = false;
28
    private bool $matchLeadingPeriodExactly = false;
29
    private bool $ignoreCase = false;
30
    private string $pattern;
31
32
    /**
33
     * @param string $pattern The shell wildcard pattern to match against.
34
     */
35 60
    public function __construct(string $pattern)
36
    {
37 60
        $this->pattern = $pattern;
38 60
    }
39
40
    /**
41
     * Checks if the passed string would match the given shell wildcard pattern.
42
     *
43
     * @param string $string The tested string.
44
     * @return bool Whether the string matches pattern or not.
45
     */
46 59
    public function match(string $string): bool
47
    {
48 59
        if ($this->pattern === '*' && !$this->matchSlashesExactly && !$this->matchLeadingPeriodExactly) {
49 2
            return true;
50
        }
51
52 57
        $pattern = $this->pattern;
53
54 57
        if ($this->matchLeadingPeriodExactly) {
55 3
            $pattern = preg_replace('/^[*?]/', '[!.]', $pattern);
56
        }
57
58
        $replacements = [
59 57
            '\\\\\\\\' => '\\\\',
60
            '\\\\\\*' => '[*]',
61
            '\\\\\\?' => '[?]',
62
            '\*' => '.*',
63
            '\?' => '.',
64
            '\[\!' => '[^',
65
            '\[' => '[',
66
            '\]' => ']',
67
            '\-' => '-',
68
        ];
69
70 57
        if ($this->withoutEscape) {
71 5
            unset($replacements['\\\\\\\\'], $replacements['\\\\\\*'], $replacements['\\\\\\?']);
72
        }
73
74 57
        if ($this->matchSlashesExactly) {
75 10
            $replacements['\*'] = '[^/\\\\]*';
76 10
            $replacements['\?'] = '[^/\\\\]';
77
        }
78
79 57
        $pattern = strtr(preg_quote($pattern, '#'), $replacements);
80 57
        $pattern = '#^' . $pattern . '$#us';
81
82 57
        if ($this->ignoreCase) {
83 1
            $pattern .= 'i';
84
        }
85
86 57
        return preg_match($pattern, $string) === 1;
87
    }
88
89
    /**
90
     * Disables using `\` to escape following special character. `\` becomes regular character.
91
     * @return self
92
     */
93 6
    public function withoutEscape(): self
94
    {
95 6
        $new = clone $this;
96 6
        $new->withoutEscape = true;
97 6
        return $new;
98
    }
99
100
    /**
101
     * Do not match `/` character with wildcards. The only way to match `/` is with an explicit `/` in pattern.
102
     * Useful for matching file paths. Use with {@see withExactLeadingPeriod()}.
103
     * @return self
104
     */
105 11
    public function withExactSlashes(): self
106
    {
107 11
        $new = clone $this;
108 11
        $new->matchSlashesExactly = true;
109 11
        return $new;
110
    }
111
112
    /**
113
     * Make pattern case insensitive.
114
     * @return self
115
     */
116 2
    public function ignoreCase(): self
117
    {
118 2
        $new = clone $this;
119 2
        $new->ignoreCase = true;
120 2
        return $new;
121
    }
122
123
    /**
124
     * Do not match `.` character at the beginning of string with wildcards.
125
     * Useful for matching file paths. Use with {@see withExactSlashes()}.
126
     * @return self
127
     */
128 4
    public function withExactLeadingPeriod(): self
129
    {
130 4
        $new = clone $this;
131 4
        $new->matchLeadingPeriodExactly = true;
132 4
        return $new;
133
    }
134
}
135