Passed
Pull Request — master (#67)
by Alexander
02:11
created

WildcardPattern::withExactLeadingPeriod()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

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