Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/base/common/glob.ts
5222 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { equals } from './arrays.js';
7
import { isThenable } from './async.js';
8
import { CharCode } from './charCode.js';
9
import { isEqualOrParent } from './extpath.js';
10
import { LRUCache } from './map.js';
11
import { basename, extname, posix, sep } from './path.js';
12
import { isLinux } from './platform.js';
13
import { endsWithIgnoreCase, equalsIgnoreCase, escapeRegExpCharacters, ltrim } from './strings.js';
14
15
export interface IRelativePattern {
16
17
/**
18
* A base file path to which this pattern will be matched against relatively.
19
*/
20
readonly base: string;
21
22
/**
23
* A file glob pattern like `*.{ts,js}` that will be matched on file paths
24
* relative to the base path.
25
*
26
* Example: Given a base of `/home/work/folder` and a file path of `/home/work/folder/index.js`,
27
* the file glob pattern will match on `index.js`.
28
*/
29
readonly pattern: string;
30
}
31
32
export interface IExpression {
33
[pattern: string]: boolean | SiblingClause;
34
}
35
36
export function getEmptyExpression(): IExpression {
37
return Object.create(null);
38
}
39
40
interface SiblingClause {
41
when: string;
42
}
43
44
export const GLOBSTAR = '**';
45
export const GLOB_SPLIT = '/';
46
47
const PATH_REGEX = '[/\\\\]'; // any slash or backslash
48
const NO_PATH_REGEX = '[^/\\\\]'; // any non-slash and non-backslash
49
const ALL_FORWARD_SLASHES = /\//g;
50
51
function starsToRegExp(starCount: number, isLastPattern?: boolean): string {
52
switch (starCount) {
53
case 0:
54
return '';
55
case 1:
56
return `${NO_PATH_REGEX}*?`; // 1 star matches any number of characters except path separator (/ and \) - non greedy (?)
57
default:
58
// Matches: (Path Sep OR Path Val followed by Path Sep) 0-many times except when it's the last pattern
59
// in which case also matches (Path Sep followed by Path Val)
60
// Group is non capturing because we don't need to capture at all (?:...)
61
// Overall we use non-greedy matching because it could be that we match too much
62
return `(?:${PATH_REGEX}|${NO_PATH_REGEX}+${PATH_REGEX}${isLastPattern ? `|${PATH_REGEX}${NO_PATH_REGEX}+` : ''})*?`;
63
}
64
}
65
66
export function splitGlobAware(pattern: string, splitChar: string): string[] {
67
if (!pattern) {
68
return [];
69
}
70
71
const segments: string[] = [];
72
73
let inBraces = false;
74
let inBrackets = false;
75
76
let curVal = '';
77
for (const char of pattern) {
78
switch (char) {
79
case splitChar:
80
if (!inBraces && !inBrackets) {
81
segments.push(curVal);
82
curVal = '';
83
84
continue;
85
}
86
break;
87
case '{':
88
inBraces = true;
89
break;
90
case '}':
91
inBraces = false;
92
break;
93
case '[':
94
inBrackets = true;
95
break;
96
case ']':
97
inBrackets = false;
98
break;
99
}
100
101
curVal += char;
102
}
103
104
// Tail
105
if (curVal) {
106
segments.push(curVal);
107
}
108
109
return segments;
110
}
111
112
function parseRegExp(pattern: string): string {
113
if (!pattern) {
114
return '';
115
}
116
117
let regEx = '';
118
119
// Split up into segments for each slash found
120
const segments = splitGlobAware(pattern, GLOB_SPLIT);
121
122
// Special case where we only have globstars
123
if (segments.every(segment => segment === GLOBSTAR)) {
124
regEx = '.*';
125
}
126
127
// Build regex over segments
128
else {
129
let previousSegmentWasGlobStar = false;
130
segments.forEach((segment, index) => {
131
132
// Treat globstar specially
133
if (segment === GLOBSTAR) {
134
135
// if we have more than one globstar after another, just ignore it
136
if (previousSegmentWasGlobStar) {
137
return;
138
}
139
140
regEx += starsToRegExp(2, index === segments.length - 1);
141
}
142
143
// Anything else, not globstar
144
else {
145
146
// States
147
let inBraces = false;
148
let braceVal = '';
149
150
let inBrackets = false;
151
let bracketVal = '';
152
153
for (const char of segment) {
154
155
// Support brace expansion
156
if (char !== '}' && inBraces) {
157
braceVal += char;
158
continue;
159
}
160
161
// Support brackets
162
if (inBrackets && (char !== ']' || !bracketVal) /* ] is literally only allowed as first character in brackets to match it */) {
163
let res: string;
164
165
// range operator
166
if (char === '-') {
167
res = char;
168
}
169
170
// negation operator (only valid on first index in bracket)
171
else if ((char === '^' || char === '!') && !bracketVal) {
172
res = '^';
173
}
174
175
// glob split matching is not allowed within character ranges
176
// see http://man7.org/linux/man-pages/man7/glob.7.html
177
else if (char === GLOB_SPLIT) {
178
res = '';
179
}
180
181
// anything else gets escaped
182
else {
183
res = escapeRegExpCharacters(char);
184
}
185
186
bracketVal += res;
187
continue;
188
}
189
190
switch (char) {
191
case '{':
192
inBraces = true;
193
continue;
194
195
case '[':
196
inBrackets = true;
197
continue;
198
199
case '}': {
200
const choices = splitGlobAware(braceVal, ',');
201
202
// Converts {foo,bar} => [foo|bar]
203
const braceRegExp = `(?:${choices.map(choice => parseRegExp(choice)).join('|')})`;
204
205
regEx += braceRegExp;
206
207
inBraces = false;
208
braceVal = '';
209
210
break;
211
}
212
213
case ']': {
214
regEx += ('[' + bracketVal + ']');
215
216
inBrackets = false;
217
bracketVal = '';
218
219
break;
220
}
221
222
case '?':
223
regEx += NO_PATH_REGEX; // 1 ? matches any single character except path separator (/ and \)
224
continue;
225
226
case '*':
227
regEx += starsToRegExp(1);
228
continue;
229
230
default:
231
regEx += escapeRegExpCharacters(char);
232
}
233
}
234
235
// Tail: Add the slash we had split on if there is more to
236
// come and the remaining pattern is not a globstar
237
// For example if pattern: some/**/*.js we want the "/" after
238
// some to be included in the RegEx to prevent a folder called
239
// "something" to match as well.
240
if (
241
index < segments.length - 1 && // more segments to come after this
242
(
243
segments[index + 1] !== GLOBSTAR || // next segment is not **, or...
244
index + 2 < segments.length // ...next segment is ** but there is more segments after that
245
)
246
) {
247
regEx += PATH_REGEX;
248
}
249
}
250
251
// update globstar state
252
previousSegmentWasGlobStar = (segment === GLOBSTAR);
253
});
254
}
255
256
return regEx;
257
}
258
259
// regexes to check for trivial glob patterns that just check for String#endsWith
260
const T1 = /^\*\*\/\*\.[\w\.-]+$/; // **/*.something
261
const T2 = /^\*\*\/([\w\.-]+)\/?$/; // **/something
262
const T3 = /^{\*\*\/\*?[\w\.-]+\/?(,\*\*\/\*?[\w\.-]+\/?)*}$/; // {**/*.something,**/*.else} or {**/package.json,**/project.json}
263
const T3_2 = /^{\*\*\/\*?[\w\.-]+(\/(\*\*)?)?(,\*\*\/\*?[\w\.-]+(\/(\*\*)?)?)*}$/; // Like T3, with optional trailing /**
264
const T4 = /^\*\*((\/[\w\.-]+)+)\/?$/; // **/something/else
265
const T5 = /^([\w\.-]+(\/[\w\.-]+)*)\/?$/; // something/else
266
267
export type ParsedPattern = (path: string, basename?: string) => boolean;
268
269
// The `ParsedExpression` returns a `Promise`
270
// iff `hasSibling` returns a `Promise`.
271
export type ParsedExpression = (path: string, basename?: string, hasSibling?: (name: string) => boolean | Promise<boolean>) => string | null | Promise<string | null> /* the matching pattern */;
272
273
export interface IGlobOptions {
274
275
/**
276
* Simplify patterns for use as exclusion filters during
277
* tree traversal to skip entire subtrees. Cannot be used
278
* outside of a tree traversal.
279
*/
280
trimForExclusions?: boolean;
281
282
/**
283
* Whether glob pattern matching should be case insensitive.
284
*/
285
ignoreCase?: boolean;
286
}
287
288
interface IGlobOptionsInternal extends IGlobOptions {
289
equals: (a: string, b: string) => boolean;
290
endsWith: (str: string, candidate: string) => boolean;
291
isEqualOrParent: (base: string, candidate: string) => boolean;
292
}
293
294
interface ParsedStringPattern {
295
(path: string, basename?: string): string | null | Promise<string | null> /* the matching pattern */;
296
basenames?: string[];
297
patterns?: string[];
298
allBasenames?: string[];
299
allPaths?: string[];
300
}
301
302
interface ParsedExpressionPattern {
303
(path: string, basename?: string, name?: string, hasSibling?: (name: string) => boolean | Promise<boolean>): string | null | Promise<string | null> /* the matching pattern */;
304
requiresSiblings?: boolean;
305
allBasenames?: string[];
306
allPaths?: string[];
307
}
308
309
const CACHE = new LRUCache<string, ParsedStringPattern>(10000); // bounded to 10000 elements
310
311
const FALSE = function () {
312
return false;
313
};
314
315
const NULL = function (): string | null {
316
return null;
317
};
318
319
/**
320
* Check if a provided parsed pattern or expression
321
* is empty - hence it won't ever match anything.
322
*
323
* See {@link FALSE} and {@link NULL}.
324
*/
325
export function isEmptyPattern(pattern: ParsedPattern | ParsedExpression): pattern is (typeof FALSE | typeof NULL) {
326
if (pattern === FALSE) {
327
return true;
328
}
329
330
if (pattern === NULL) {
331
return true;
332
}
333
334
return false;
335
}
336
337
function parsePattern(arg1: string | IRelativePattern, options: IGlobOptions): ParsedStringPattern {
338
if (!arg1) {
339
return NULL;
340
}
341
342
// Handle relative patterns
343
let pattern: string;
344
if (typeof arg1 !== 'string') {
345
pattern = arg1.pattern;
346
} else {
347
pattern = arg1;
348
}
349
350
// Whitespace trimming
351
pattern = pattern.trim();
352
353
const ignoreCase = options.ignoreCase ?? false;
354
const internalOptions = {
355
...options,
356
equals: ignoreCase ? equalsIgnoreCase : (a: string, b: string) => a === b,
357
endsWith: ignoreCase ? endsWithIgnoreCase : (str: string, candidate: string) => str.endsWith(candidate),
358
// TODO: the '!isLinux' part below is to keep current behavior unchanged, but it should probably be removed
359
// in favor of passing correct options from the caller.
360
isEqualOrParent: (base: string, candidate: string) => isEqualOrParent(base, candidate, !isLinux || ignoreCase)
361
};
362
363
// Check cache
364
const patternKey = `${ignoreCase ? pattern.toLowerCase() : pattern}_${!!options.trimForExclusions}_${ignoreCase}`;
365
let parsedPattern = CACHE.get(patternKey);
366
if (parsedPattern) {
367
return wrapRelativePattern(parsedPattern, arg1, internalOptions);
368
}
369
370
// Check for Trivials
371
let match: RegExpExecArray | null;
372
if (T1.test(pattern)) {
373
parsedPattern = trivia1(pattern.substring(4), pattern, internalOptions); // common pattern: **/*.txt just need endsWith check
374
} else if (match = T2.exec(trimForExclusions(pattern, internalOptions))) { // common pattern: **/some.txt just need basename check
375
parsedPattern = trivia2(match[1], pattern, internalOptions);
376
} else if ((options.trimForExclusions ? T3_2 : T3).test(pattern)) { // repetition of common patterns (see above) {**/*.txt,**/*.png}
377
parsedPattern = trivia3(pattern, internalOptions);
378
} else if (match = T4.exec(trimForExclusions(pattern, internalOptions))) { // common pattern: **/something/else just need endsWith check
379
parsedPattern = trivia4and5(match[1].substring(1), pattern, true, internalOptions);
380
} else if (match = T5.exec(trimForExclusions(pattern, internalOptions))) { // common pattern: something/else just need equals check
381
parsedPattern = trivia4and5(match[1], pattern, false, internalOptions);
382
}
383
384
// Otherwise convert to pattern
385
else {
386
parsedPattern = toRegExp(pattern, internalOptions);
387
}
388
389
// Cache
390
CACHE.set(patternKey, parsedPattern);
391
392
return wrapRelativePattern(parsedPattern, arg1, internalOptions);
393
}
394
395
function wrapRelativePattern(parsedPattern: ParsedStringPattern, arg2: string | IRelativePattern, options: IGlobOptionsInternal): ParsedStringPattern {
396
if (typeof arg2 === 'string') {
397
return parsedPattern;
398
}
399
400
const wrappedPattern: ParsedStringPattern = function (path, basename) {
401
if (!options.isEqualOrParent(path, arg2.base)) {
402
// skip glob matching if `base` is not a parent of `path`
403
return null;
404
}
405
406
// Given we have checked `base` being a parent of `path`,
407
// we can now remove the `base` portion of the `path`
408
// and only match on the remaining path components
409
// For that we try to extract the portion of the `path`
410
// that comes after the `base` portion. We have to account
411
// for the fact that `base` might end in a path separator
412
// (https://github.com/microsoft/vscode/issues/162498)
413
414
return parsedPattern(ltrim(path.substring(arg2.base.length), sep), basename);
415
};
416
417
// Make sure to preserve associated metadata
418
wrappedPattern.allBasenames = parsedPattern.allBasenames;
419
wrappedPattern.allPaths = parsedPattern.allPaths;
420
wrappedPattern.basenames = parsedPattern.basenames;
421
wrappedPattern.patterns = parsedPattern.patterns;
422
423
return wrappedPattern;
424
}
425
426
function trimForExclusions(pattern: string, options: IGlobOptions): string {
427
return options.trimForExclusions && pattern.endsWith('/**') ? pattern.substring(0, pattern.length - 2) : pattern; // dropping **, tailing / is dropped later
428
}
429
430
// common pattern: **/*.txt just need endsWith check
431
function trivia1(base: string, pattern: string, options: IGlobOptionsInternal): ParsedStringPattern {
432
return function (path: string, basename?: string) {
433
return typeof path === 'string' && options.endsWith(path, base) ? pattern : null;
434
};
435
}
436
437
// common pattern: **/some.txt just need basename check
438
function trivia2(base: string, pattern: string, options: IGlobOptionsInternal): ParsedStringPattern {
439
const slashBase = `/${base}`;
440
const backslashBase = `\\${base}`;
441
442
const parsedPattern: ParsedStringPattern = function (path: string, basename?: string) {
443
if (typeof path !== 'string') {
444
return null;
445
}
446
447
if (basename) {
448
return options.equals(basename, base) ? pattern : null;
449
}
450
451
return options.equals(path, base) || options.endsWith(path, slashBase) || options.endsWith(path, backslashBase) ? pattern : null;
452
};
453
454
const basenames = [base];
455
parsedPattern.basenames = basenames;
456
parsedPattern.patterns = [pattern];
457
parsedPattern.allBasenames = basenames;
458
459
return parsedPattern;
460
}
461
462
// repetition of common patterns (see above) {**/*.txt,**/*.png}
463
function trivia3(pattern: string, options: IGlobOptionsInternal): ParsedStringPattern {
464
const parsedPatterns = aggregateBasenameMatches(pattern.slice(1, -1)
465
.split(',')
466
.map(pattern => parsePattern(pattern, options))
467
.filter(pattern => pattern !== NULL), pattern);
468
469
const patternsLength = parsedPatterns.length;
470
if (!patternsLength) {
471
return NULL;
472
}
473
474
if (patternsLength === 1) {
475
return parsedPatterns[0];
476
}
477
478
const parsedPattern: ParsedStringPattern = function (path: string, basename?: string) {
479
for (let i = 0, n = parsedPatterns.length; i < n; i++) {
480
if (parsedPatterns[i](path, basename)) {
481
return pattern;
482
}
483
}
484
485
return null;
486
};
487
488
const withBasenames = parsedPatterns.find(pattern => !!pattern.allBasenames);
489
if (withBasenames) {
490
parsedPattern.allBasenames = withBasenames.allBasenames;
491
}
492
493
const allPaths = parsedPatterns.reduce((all, current) => current.allPaths ? all.concat(current.allPaths) : all, [] as string[]);
494
if (allPaths.length) {
495
parsedPattern.allPaths = allPaths;
496
}
497
498
return parsedPattern;
499
}
500
501
// common patterns: **/something/else just need endsWith check, something/else just needs and equals check
502
function trivia4and5(targetPath: string, pattern: string, matchPathEnds: boolean, options: IGlobOptionsInternal): ParsedStringPattern {
503
const usingPosixSep = sep === posix.sep;
504
const nativePath = usingPosixSep ? targetPath : targetPath.replace(ALL_FORWARD_SLASHES, sep);
505
const nativePathEnd = sep + nativePath;
506
const targetPathEnd = posix.sep + targetPath;
507
508
let parsedPattern: ParsedStringPattern;
509
if (matchPathEnds) {
510
parsedPattern = function (path: string, basename?: string) {
511
return typeof path === 'string' && (
512
(options.equals(path, nativePath) || options.endsWith(path, nativePathEnd)) ||
513
!usingPosixSep && (options.equals(path, targetPath) || options.endsWith(path, targetPathEnd))
514
) ? pattern : null;
515
};
516
} else {
517
parsedPattern = function (path: string, basename?: string) {
518
return typeof path === 'string' && (options.equals(path, nativePath) || (!usingPosixSep && options.equals(path, targetPath))) ? pattern : null;
519
};
520
}
521
522
parsedPattern.allPaths = [(matchPathEnds ? '*/' : './') + targetPath];
523
524
return parsedPattern;
525
}
526
527
function toRegExp(pattern: string, options: IGlobOptions): ParsedStringPattern {
528
try {
529
const regExp = new RegExp(`^${parseRegExp(pattern)}$`, options.ignoreCase ? 'i' : undefined);
530
return function (path: string) {
531
regExp.lastIndex = 0; // reset RegExp to its initial state to reuse it!
532
533
return typeof path === 'string' && regExp.test(path) ? pattern : null;
534
};
535
} catch {
536
return NULL;
537
}
538
}
539
540
/**
541
* Simplified glob matching. Supports a subset of glob patterns:
542
* * `*` to match zero or more characters in a path segment
543
* * `?` to match on one character in a path segment
544
* * `**` to match any number of path segments, including none
545
* * `{}` to group conditions (e.g. *.{ts,js} matches all TypeScript and JavaScript files)
546
* * `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …)
547
* * `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`)
548
*/
549
export function match(pattern: string | IRelativePattern, path: string, options?: IGlobOptions): boolean;
550
export function match(expression: IExpression, path: string, options?: IGlobOptions): boolean;
551
export function match(arg1: string | IExpression | IRelativePattern, path: string, options?: IGlobOptions): boolean {
552
if (!arg1 || typeof path !== 'string') {
553
return false;
554
}
555
556
return parse(arg1, options)(path) as boolean;
557
}
558
559
/**
560
* Simplified glob matching. Supports a subset of glob patterns:
561
* * `*` to match zero or more characters in a path segment
562
* * `?` to match on one character in a path segment
563
* * `**` to match any number of path segments, including none
564
* * `{}` to group conditions (e.g. *.{ts,js} matches all TypeScript and JavaScript files)
565
* * `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …)
566
* * `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`)
567
*/
568
export function parse(pattern: string | IRelativePattern, options?: IGlobOptions): ParsedPattern;
569
export function parse(expression: IExpression, options?: IGlobOptions): ParsedExpression;
570
export function parse(arg1: string | IExpression | IRelativePattern, options?: IGlobOptions): ParsedPattern | ParsedExpression;
571
export function parse(arg1: string | IExpression | IRelativePattern, options: IGlobOptions = {}): ParsedPattern | ParsedExpression {
572
if (!arg1) {
573
return FALSE;
574
}
575
576
// Glob with String
577
if (typeof arg1 === 'string' || isRelativePattern(arg1)) {
578
const parsedPattern = parsePattern(arg1, options);
579
if (parsedPattern === NULL) {
580
return FALSE;
581
}
582
583
const resultPattern: ParsedPattern & { allBasenames?: string[]; allPaths?: string[] } = function (path: string, basename?: string) {
584
return !!parsedPattern(path, basename);
585
};
586
587
if (parsedPattern.allBasenames) {
588
resultPattern.allBasenames = parsedPattern.allBasenames;
589
}
590
591
if (parsedPattern.allPaths) {
592
resultPattern.allPaths = parsedPattern.allPaths;
593
}
594
595
return resultPattern;
596
}
597
598
// Glob with Expression
599
return parsedExpression(arg1, options);
600
}
601
602
export function isRelativePattern(obj: unknown): obj is IRelativePattern {
603
const rp = obj as IRelativePattern | undefined | null;
604
if (!rp) {
605
return false;
606
}
607
608
return typeof rp.base === 'string' && typeof rp.pattern === 'string';
609
}
610
611
export function getBasenameTerms(patternOrExpression: ParsedPattern | ParsedExpression): string[] {
612
return (<ParsedStringPattern>patternOrExpression).allBasenames || [];
613
}
614
615
export function getPathTerms(patternOrExpression: ParsedPattern | ParsedExpression): string[] {
616
return (<ParsedStringPattern>patternOrExpression).allPaths || [];
617
}
618
619
function parsedExpression(expression: IExpression, options: IGlobOptions): ParsedExpression {
620
const parsedPatterns = aggregateBasenameMatches(Object.getOwnPropertyNames(expression)
621
.map(pattern => parseExpressionPattern(pattern, expression[pattern], options))
622
.filter(pattern => pattern !== NULL));
623
624
const patternsLength = parsedPatterns.length;
625
if (!patternsLength) {
626
return NULL;
627
}
628
629
if (!parsedPatterns.some(parsedPattern => !!(<ParsedExpressionPattern>parsedPattern).requiresSiblings)) {
630
if (patternsLength === 1) {
631
return parsedPatterns[0] as ParsedStringPattern;
632
}
633
634
const resultExpression: ParsedStringPattern = function (path: string, basename?: string) {
635
let resultPromises: Promise<string | null>[] | undefined = undefined;
636
637
for (let i = 0, n = parsedPatterns.length; i < n; i++) {
638
const result = parsedPatterns[i](path, basename);
639
if (typeof result === 'string') {
640
return result; // immediately return as soon as the first expression matches
641
}
642
643
// If the result is a promise, we have to keep it for
644
// later processing and await the result properly.
645
if (isThenable(result)) {
646
if (!resultPromises) {
647
resultPromises = [];
648
}
649
650
resultPromises.push(result);
651
}
652
}
653
654
// With result promises, we have to loop over each and
655
// await the result before we can return any result.
656
if (resultPromises) {
657
return (async () => {
658
for (const resultPromise of resultPromises) {
659
const result = await resultPromise;
660
if (typeof result === 'string') {
661
return result;
662
}
663
}
664
665
return null;
666
})();
667
}
668
669
return null;
670
};
671
672
const withBasenames = parsedPatterns.find(pattern => !!pattern.allBasenames);
673
if (withBasenames) {
674
resultExpression.allBasenames = withBasenames.allBasenames;
675
}
676
677
const allPaths = parsedPatterns.reduce((all, current) => current.allPaths ? all.concat(current.allPaths) : all, [] as string[]);
678
if (allPaths.length) {
679
resultExpression.allPaths = allPaths;
680
}
681
682
return resultExpression;
683
}
684
685
const resultExpression: ParsedStringPattern = function (path: string, base?: string, hasSibling?: (name: string) => boolean | Promise<boolean>) {
686
let name: string | undefined = undefined;
687
let resultPromises: Promise<string | null>[] | undefined = undefined;
688
689
for (let i = 0, n = parsedPatterns.length; i < n; i++) {
690
691
// Pattern matches path
692
const parsedPattern = (<ParsedExpressionPattern>parsedPatterns[i]);
693
if (parsedPattern.requiresSiblings && hasSibling) {
694
if (!base) {
695
base = basename(path);
696
}
697
698
if (!name) {
699
name = base.substring(0, base.length - extname(path).length);
700
}
701
}
702
703
const result = parsedPattern(path, base, name, hasSibling);
704
if (typeof result === 'string') {
705
return result; // immediately return as soon as the first expression matches
706
}
707
708
// If the result is a promise, we have to keep it for
709
// later processing and await the result properly.
710
if (isThenable(result)) {
711
if (!resultPromises) {
712
resultPromises = [];
713
}
714
715
resultPromises.push(result);
716
}
717
}
718
719
// With result promises, we have to loop over each and
720
// await the result before we can return any result.
721
if (resultPromises) {
722
return (async () => {
723
for (const resultPromise of resultPromises) {
724
const result = await resultPromise;
725
if (typeof result === 'string') {
726
return result;
727
}
728
}
729
730
return null;
731
})();
732
}
733
734
return null;
735
};
736
737
const withBasenames = parsedPatterns.find(pattern => !!pattern.allBasenames);
738
if (withBasenames) {
739
resultExpression.allBasenames = withBasenames.allBasenames;
740
}
741
742
const allPaths = parsedPatterns.reduce((all, current) => current.allPaths ? all.concat(current.allPaths) : all, [] as string[]);
743
if (allPaths.length) {
744
resultExpression.allPaths = allPaths;
745
}
746
747
return resultExpression;
748
}
749
750
function parseExpressionPattern(pattern: string, value: boolean | SiblingClause, options: IGlobOptions): (ParsedStringPattern | ParsedExpressionPattern) {
751
if (value === false) {
752
return NULL; // pattern is disabled
753
}
754
755
const parsedPattern = parsePattern(pattern, options);
756
if (parsedPattern === NULL) {
757
return NULL;
758
}
759
760
// Expression Pattern is <boolean>
761
if (typeof value === 'boolean') {
762
return parsedPattern;
763
}
764
765
// Expression Pattern is <SiblingClause>
766
if (value) {
767
const when = value.when;
768
if (typeof when === 'string') {
769
const result: ParsedExpressionPattern = (path: string, basename?: string, name?: string, hasSibling?: (name: string) => boolean | Promise<boolean>) => {
770
if (!hasSibling || !parsedPattern(path, basename)) {
771
return null;
772
}
773
774
const clausePattern = when.replace('$(basename)', () => name!);
775
const matched = hasSibling(clausePattern);
776
return isThenable(matched) ?
777
matched.then(match => match ? pattern : null) :
778
matched ? pattern : null;
779
};
780
781
result.requiresSiblings = true;
782
783
return result;
784
}
785
}
786
787
// Expression is anything
788
return parsedPattern;
789
}
790
791
function aggregateBasenameMatches(parsedPatterns: Array<ParsedStringPattern | ParsedExpressionPattern>, result?: string): Array<ParsedStringPattern | ParsedExpressionPattern> {
792
const basenamePatterns = parsedPatterns.filter(parsedPattern => !!(<ParsedStringPattern>parsedPattern).basenames);
793
if (basenamePatterns.length < 2) {
794
return parsedPatterns;
795
}
796
797
const basenames = basenamePatterns.reduce<string[]>((all, current) => {
798
const basenames = (<ParsedStringPattern>current).basenames;
799
800
return basenames ? all.concat(basenames) : all;
801
}, [] as string[]);
802
803
let patterns: string[];
804
if (result) {
805
patterns = [];
806
807
for (let i = 0, n = basenames.length; i < n; i++) {
808
patterns.push(result);
809
}
810
} else {
811
patterns = basenamePatterns.reduce((all, current) => {
812
const patterns = (<ParsedStringPattern>current).patterns;
813
814
return patterns ? all.concat(patterns) : all;
815
}, [] as string[]);
816
}
817
818
const aggregate: ParsedStringPattern = function (path: string, basename?: string) {
819
if (typeof path !== 'string') {
820
return null;
821
}
822
823
if (!basename) {
824
let i: number;
825
for (i = path.length; i > 0; i--) {
826
const ch = path.charCodeAt(i - 1);
827
if (ch === CharCode.Slash || ch === CharCode.Backslash) {
828
break;
829
}
830
}
831
832
basename = path.substring(i);
833
}
834
835
const index = basenames.indexOf(basename);
836
return index !== -1 ? patterns[index] : null;
837
};
838
839
aggregate.basenames = basenames;
840
aggregate.patterns = patterns;
841
aggregate.allBasenames = basenames;
842
843
const aggregatedPatterns = parsedPatterns.filter(parsedPattern => !(<ParsedStringPattern>parsedPattern).basenames);
844
aggregatedPatterns.push(aggregate);
845
846
return aggregatedPatterns;
847
}
848
849
// NOTE: This is not used for actual matching, only for resetting watcher when patterns change.
850
// That is why it's ok to avoid case-insensitive comparison here.
851
export function patternsEquals(patternsA: Array<string | IRelativePattern> | undefined, patternsB: Array<string | IRelativePattern> | undefined): boolean {
852
return equals(patternsA, patternsB, (a, b) => {
853
if (typeof a === 'string' && typeof b === 'string') {
854
return a === b;
855
}
856
857
if (typeof a !== 'string' && typeof b !== 'string') {
858
return a.base === b.base && a.pattern === b.pattern;
859
}
860
861
return false;
862
});
863
}
864
865