Failed Conditions
Push — master ( 1b60f6...3023cd )
by Bernhard
02:53
created

Glob::toRegExNonEscaped()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 35
Code Lines 17

Duplication

Lines 3
Ratio 8.57 %

Code Coverage

Tests 19
CRAP Score 4

Importance

Changes 6
Bugs 1 Features 4
Metric Value
c 6
b 1
f 4
dl 3
loc 35
ccs 19
cts 19
cp 1
rs 8.5806
cc 4
eloc 17
nc 4
nop 1
crap 4
1
<?php
2
3
/*
4
 * This file is part of the webmozart/glob package.
5
 *
6
 * (c) Bernhard Schussek <[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 Webmozart\Glob;
13
14
use InvalidArgumentException;
15
use Webmozart\Glob\Iterator\GlobIterator;
16
use Webmozart\PathUtil\Path;
17
18
/**
19
 * Searches and matches file paths using Ant-like globs.
20
 *
21
 * This class implements an Ant-like version of PHP's `glob()` function. The
22
 * wildcard "*" matches any number of characters except directory separators.
23
 * The double wildcard "**" matches any number of characters, including
24
 * directory separators.
25
 *
26
 * Use {@link glob()} to glob the filesystem for paths:
27
 *
28
 * ```php
29
 * foreach (Glob::glob('/project/**.twig') as $path) {
30
 *     // do something...
31
 * }
32
 * ```
33
 *
34
 * Use {@link match()} to match a file path against a glob:
35
 *
36
 * ```php
37
 * if (Glob::match('/project/views/index.html.twig', '/project/**.twig')) {
38
 *     // path matches
39
 * }
40
 * ```
41
 *
42
 * You can also filter an array of paths for all paths that match your glob with
43
 * {@link filter()}:
44
 *
45
 * ```php
46
 * $filteredPaths = Glob::filter($paths, '/project/**.twig');
47
 * ```
48
 *
49
 * Internally, the methods described above convert the glob into a regular
50
 * expression that is then matched against the matched paths. If you need to
51
 * match many paths against the same glob, you should convert the glob manually
52
 * and use {@link preg_match()} to test the paths:
53
 *
54
 * ```php
55
 * $staticPrefix = Glob::getStaticPrefix('/project/**.twig');
56
 * $regEx = Glob::toRegEx('/project/**.twig');
57
 *
58
 * if (0 !== strpos($path, $staticPrefix)) {
59
 *     // no match
60
 * }
61
 *
62
 * if (!preg_match($regEx, $path)) {
63
 *     // no match
64
 * }
65
 * ```
66
 *
67
 * The method {@link getStaticPrefix()} returns the part of the glob up to the
68
 * first wildcard "*". You should always test whether a path has this prefix
69
 * before calling the much more expensive {@link preg_match()}.
70
 *
71
 * @since  1.0
72
 *
73
 * @author Bernhard Schussek <[email protected]>
74
 */
75
final class Glob
76
{
77
    /**
78
     * Globs the file system paths matching the glob.
79
     *
80
     * The glob may contain the wildcard "*". This wildcard matches any number
81
     * of characters, *including* directory separators.
82
     *
83
     * ```php
84
     * foreach (Glob::glob('/project/**.twig') as $path) {
85
     *     // do something...
86
     * }
87
     * ```
88
     *
89
     * @param string $glob  The canonical glob. The glob should contain forward
90
     *                      slashes as directory separators only. It must not
91
     *                      contain any "." or ".." segments. Use the
92
     *                      "webmozart/path-util" utility to canonicalize globs
93
     *                      prior to calling this method.
94
     * @param int    $flags A bitwise combination of the flag constants in this
95
     *                      class.
96
     *
97
     * @return string[] The matching paths. The keys of the array are
98
     *                  incrementing integers.
99
     */
100 6
    public static function glob($glob, $flags = 0)
101
    {
102 6
        $results = iterator_to_array(new GlobIterator($glob, $flags));
103
104 5
        sort($results);
105
106 5
        return $results;
107
    }
108
109
    /**
110
     * Matches a path against a glob.
111
     *
112
     * ```php
113
     * if (Glob::match('/project/views/index.html.twig', '/project/**.twig')) {
114
     *     // path matches
115
     * }
116
     * ```
117
     *
118
     * @param string $path  The path to match.
119
     * @param string $glob  The canonical glob. The glob should contain forward
120
     *                      slashes as directory separators only. It must not
121
     *                      contain any "." or ".." segments. Use the
122
     *                      "webmozart/path-util" utility to canonicalize globs
123
     *                      prior to calling this method.
124
     * @param int    $flags A bitwise combination of the flag constants in
125
     *                      this class.
126
     *
127
     * @return bool Returns `true` if the path is matched by the glob.
128
     */
129 12
    public static function match($path, $glob, $flags = 0)
130
    {
131 12
        if (!self::isDynamic($glob)) {
132 1
            return $glob === $path;
133
        }
134
135 11
        if (0 !== strpos($path, self::getStaticPrefix($glob, $flags))) {
136 6
            return false;
137
        }
138
139 5
        if (!preg_match(self::toRegEx($glob, $flags), $path)) {
0 ignored issues
show
Unused Code introduced by
This if statement, and the following return statement can be replaced with return (bool) preg_match...$glob, $flags), $path);.
Loading history...
140
            return false;
141
        }
142
143 5
        return true;
144
    }
145
146
    /**
147
     * Filters an array for paths matching a glob.
148
     *
149
     * The filtered array is returned. This array preserves the keys of the
150
     * passed array.
151
     *
152
     * ```php
153
     * $filteredPaths = Glob::filter($paths, '/project/**.twig');
154
     * ```
155
     *
156
     * @param string[] $paths A list of paths.
157
     * @param string   $glob  The canonical glob. The glob should contain
158
     *                        forward slashes as directory separators only. It
159
     *                        must not contain any "." or ".." segments. Use the
160
     *                        "webmozart/path-util" utility to canonicalize
161
     *                        globs prior to calling this method.
162
     * @param int      $flags A bitwise combination of the flag constants in
163
     *                        this class.
164
     *
165
     * @return string[] The paths matching the glob indexed by their original
166
     *                  keys.
167
     */
168 4
    public static function filter(array $paths, $glob, $flags = 0)
169
    {
170 4
        if (!self::isDynamic($glob)) {
171 1
            if (false !== $key = array_search($glob, $paths)) {
172 1
                return array($key => $glob);
173
            }
174
175 1
            return array();
176
        }
177
178 3
        $staticPrefix = self::getStaticPrefix($glob, $flags);
179 2
        $regExp = self::toRegEx($glob, $flags);
180
181 2
        return array_filter($paths, function ($path) use ($staticPrefix, $regExp) {
182 2
            return 0 === strpos($path, $staticPrefix) && preg_match($regExp, $path);
183 2
        });
184
    }
185
186
    /**
187
     * Returns the base path of a glob.
188
     *
189
     * This method returns the most specific directory that contains all files
190
     * matched by the glob. If this directory does not exist on the file system,
191
     * it's not necessary to execute the glob algorithm.
192
     *
193
     * More specifically, the "base path" is the longest path trailed by a "/"
194
     * on the left of the first wildcard "*". If the glob does not contain
195
     * wildcards, the directory name of the glob is returned.
196
     *
197
     * ```php
198
     * Glob::getBasePath('/css/*.css');
199
     * // => /css
200
     *
201
     * Glob::getBasePath('/css/style.css');
202
     * // => /css
203
     *
204
     * Glob::getBasePath('/css/st*.css');
205
     * // => /css
206
     *
207
     * Glob::getBasePath('/*.css');
208
     * // => /
209
     * ```
210
     *
211
     * @param string $glob  The canonical glob. The glob should contain forward
212
     *                      slashes as directory separators only. It must not
213
     *                      contain any "." or ".." segments. Use the
214
     *                      "webmozart/path-util" utility to canonicalize globs
215
     *                      prior to calling this method.
216
     * @param int    $flags A bitwise combination of the flag constants in this
217
     *                      class.
218
     *
219
     * @return string The base path of the glob.
220
     */
221 40
    public static function getBasePath($glob, $flags = 0)
222
    {
223
        // Search the static prefix for the last "/"
224 40
        $staticPrefix = self::getStaticPrefix($glob, $flags);
225
226 38
        if (false !== ($pos = strrpos($staticPrefix, '/'))) {
227
            // Special case: Return "/" if the only slash is at the beginning
228
            // of the glob
229 38
            if (0 === $pos) {
230 2
                return '/';
231
            }
232
233
            // Special case: Include trailing slash of "scheme:///foo"
234 36
            if ($pos - 3 === strpos($glob, '://')) {
235 3
                return substr($staticPrefix, 0, $pos + 1);
236
            }
237
238 33
            return substr($staticPrefix, 0, $pos);
239
        }
240
241
        // Glob contains no slashes on the left of the wildcard
242
        // Return an empty string
243
        return '';
244
    }
245
246
    /**
247
     * Converts a glob to a regular expression.
248
     *
249
     * Use this method if you need to match many paths against a glob:
250
     *
251
     * ```php
252
     * $staticPrefix = Glob::getStaticPrefix('/project/**.twig');
253
     * $regEx = Glob::toRegEx('/project/**.twig');
254
     *
255
     * if (0 !== strpos($path, $staticPrefix)) {
256
     *     // no match
257
     * }
258
     *
259
     * if (!preg_match($regEx, $path)) {
260
     *     // no match
261
     * }
262
     * ```
263
     *
264
     * You should always test whether a path contains the static prefix of the
265
     * glob returned by {@link getStaticPrefix()} to reduce the number of calls
266
     * to the expensive {@link preg_match()}.
267
     *
268
     * @param string $glob  The canonical glob. The glob should contain forward
269
     *                      slashes as directory separators only. It must not
270
     *                      contain any "." or ".." segments. Use the
271
     *                      "webmozart/path-util" utility to canonicalize globs
272
     *                      prior to calling this method.
273
     * @param int    $flags A bitwise combination of the flag constants in this
274
     *                      class.
275
     *
276
     * @return string The regular expression for matching the glob.
277
     */
278 70
    public static function toRegEx($glob, $flags = 0, $delimiter = '~')
0 ignored issues
show
Unused Code introduced by
The parameter $flags is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
279
    {
280 70 View Code Duplication
        if (!Path::isAbsolute($glob) && false === strpos($glob, '://')) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
281 1
            throw new InvalidArgumentException(sprintf(
282 1
                'The glob "%s" is not absolute and not a URI.',
283
                $glob
284 1
            ));
285
        }
286
287 69
        $inSquare = false;
288 69
        $curlyLevels = 0;
289 69
        $regex = '';
290 69
        $length = strlen($glob);
291
292 69
        for ($i = 0; $i < $length; ++$i) {
293 69
            $c = $glob[$i];
294
295
            switch ($c) {
296 69
                case '.':
297 69
                case '(':
298 69
                case ')':
299 69
                case '|':
300 69
                case '+':
301 69
                case '^':
302 69
                case '$':
303 69
                case $delimiter:
304 67
                    $regex .= "\\$c";
0 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $c instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
305 67
                    break;
306
307 69 View Code Duplication
                case '/':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
308 69
                    if (isset($glob[$i + 3]) && '**/' === $glob[$i + 1].$glob[$i + 2].$glob[$i + 3]) {
309 22
                        $regex .= '/(.+/)*';
310 22
                        $i += 3;
311 22
                    } else {
312 68
                        $regex .= '/';
313
                    }
314 69
                    break;
315
316 69
                case '*':
317 35
                    $regex .= '[^/]*';
318 35
                    break;
319
320 69
                case '?':
321 3
                    $regex .= '.';
322 3
                    break;
323
324 69
                case '{':
325 7
                    $regex .= '(';
326 7
                    ++$curlyLevels;
327 7
                    break;
328
329 69
                case '}':
330 8
                    if ($curlyLevels > 0) {
331 6
                        $regex .= ')';
332 6
                        --$curlyLevels;
333 6
                    } else {
334 3
                        $regex .= '}';
335
                    }
336 8
                    break;
337
338 69
                case ',':
339 8
                    $regex .= $curlyLevels > 0 ? '|' : ',';
340 8
                    break;
341
342 69
                case '[':
343 11
                    $regex .= '[';
344 11
                    $inSquare = true;
345 11
                    if (isset($glob[$i + 1]) && '^' === $glob[$i + 1]) {
346 1
                        $regex .= '^';
347 1
                        ++$i;
348 1
                    }
349 11
                    break;
350
351 69
                case ']':
352 12
                    $regex .= $inSquare ? ']' : '\\]';
353 12
                    $inSquare = false;
354 12
                    break;
355
356 69
                case '-':
357 10
                    $regex .= $inSquare ? '-' : '\\-';
358 10
                    break;
359
360 69
                case '\\':
361 32
                    if (isset($glob[$i + 1])) {
362 32
                        switch ($glob[$i + 1]) {
363 32
                            case '*':
364 32
                            case '?':
365 32
                            case '{':
366 32
                            case '}':
367 32
                            case '[':
368 32
                            case ']':
369 32
                            case '-':
370 32
                            case '^':
371 32
                            case '\\':
372 32
                                $regex .= '\\'.$glob[$i + 1];
373 32
                                ++$i;
374 32
                                break;
375
376
                            default:
377
                                $regex .= '\\\\';
378 32
                        }
379 32
                    } else {
380
                        $regex .= '\\\\';
381
                    }
382 32
                    break;
383
384 69
                default:
385 69
                    $regex .= $c;
386 69
                    break;
387 69
            }
388 69
        }
389
390 69
        if ($inSquare) {
391 1
            throw new InvalidArgumentException(sprintf(
392 1
                'Invalid glob: missing ] in %s',
393
                $glob
394 1
            ));
395
        }
396
397 68
        if ($curlyLevels > 0) {
398 1
            throw new InvalidArgumentException(sprintf(
399 1
                'Invalid glob: missing } in %s',
400
                $glob
401 1
            ));
402
        }
403
404 67
        return $delimiter.'^'.$regex.'$'.$delimiter;
405
    }
406
407
    /**
408
     * Returns the static prefix of a glob.
409
     *
410
     * The "static prefix" is the part of the glob up to the first wildcard "*".
411
     * If the glob does not contain wildcards, the full glob is returned.
412
     *
413
     * @param string $glob  The canonical glob. The glob should contain forward
414
     *                      slashes as directory separators only. It must not
415
     *                      contain any "." or ".." segments. Use the
416
     *                      "webmozart/path-util" utility to canonicalize globs
417
     *                      prior to calling this method.
418
     * @param int    $flags A bitwise combination of the flag constants in this
419
     *                      class.
420
     *
421
     * @return string The static prefix of the glob.
422
     */
423 76
    public static function getStaticPrefix($glob, $flags = 0)
0 ignored issues
show
Unused Code introduced by
The parameter $flags is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
424
    {
425 76 View Code Duplication
        if (!Path::isAbsolute($glob) && false === strpos($glob, '://')) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
426 5
            throw new InvalidArgumentException(sprintf(
427 5
                'The glob "%s" is not absolute and not a URI.',
428
                $glob
429 5
            ));
430
        }
431
432 71
        $prefix = '';
433 71
        $length = strlen($glob);
434
435 71
        for ($i = 0; $i < $length; ++$i) {
436 71
            $c = $glob[$i];
437
438
            switch ($c) {
439 71 View Code Duplication
                case '/':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
440 71
                    $prefix .= '/';
441 71
                    if (isset($glob[$i + 3]) && '**/' === $glob[$i + 1].$glob[$i + 2].$glob[$i + 3]) {
442 18
                        break 2;
443
                    }
444 70
                    break;
445
446 70
                case '*':
447 70
                case '?':
448 70
                case '{':
449 70
                case '[':
450 35
                    break 2;
451
452 69
                case '\\':
453 29
                    if (isset($glob[$i + 1])) {
454 29
                        switch ($glob[$i + 1]) {
455 29
                            case '*':
456 29
                            case '?':
457 29
                            case '{':
458 29
                            case '[':
459 29
                            case '\\':
460 29
                                $prefix .= $glob[$i + 1];
461 29
                                ++$i;
462 29
                                break;
463
464 1
                            default:
465 1
                                $prefix .= '\\';
466 29
                        }
467 29
                    } else {
468
                        $prefix .= '\\';
469
                    }
470 29
                    break;
471
472 69
                default:
473 69
                    $prefix .= $c;
474 69
                    break;
475 69
            }
476 70
        }
477
478 71
        return $prefix;
479
    }
480
481
    /**
482
     * Returns whether the glob contains a dynamic part.
483
     *
484
     * The glob contains a dynamic part if it contains an unescaped "*" or
485
     * "{" character.
486
     *
487
     * @param string $glob The glob to test.
488
     *
489
     * @return bool Returns `true` if the glob contains a dynamic part and
490
     *              `false` otherwise.
491
     */
492 34
    public static function isDynamic($glob)
493
    {
494 34
        return false !== strpos($glob, '*') || false !== strpos($glob, '{') || false !== strpos($glob, '?') || false !== strpos($glob, '[');
495
    }
496
497
    private function __construct()
498
    {
499
    }
500
}
501