Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/base/common/glob.ts
3294 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 { 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
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
interface ParsedStringPattern {
284
(path: string, basename?: string): string | null | Promise<string | null> /* the matching pattern */;
285
basenames?: string[];
286
patterns?: string[];
287
allBasenames?: string[];
288
allPaths?: string[];
289
}
290
291
interface ParsedExpressionPattern {
292
(path: string, basename?: string, name?: string, hasSibling?: (name: string) => boolean | Promise<boolean>): string | null | Promise<string | null> /* the matching pattern */;
293
requiresSiblings?: boolean;
294
allBasenames?: string[];
295
allPaths?: string[];
296
}
297
298
const CACHE = new LRUCache<string, ParsedStringPattern>(10000); // bounded to 10000 elements
299
300
const FALSE = function () {
301
return false;
302
};
303
304
const NULL = function (): string | null {
305
return null;
306
};
307
308
/**
309
* Check if a provided parsed pattern or expression
310
* is empty - hence it won't ever match anything.
311
*
312
* See {@link FALSE} and {@link NULL}.
313
*/
314
export function isEmptyPattern(pattern: ParsedPattern | ParsedExpression): pattern is (typeof FALSE | typeof NULL) {
315
if (pattern === FALSE) {
316
return true;
317
}
318
319
if (pattern === NULL) {
320
return true;
321
}
322
323
return false;
324
}
325
326
function parsePattern(arg1: string | IRelativePattern, options: IGlobOptions): ParsedStringPattern {
327
if (!arg1) {
328
return NULL;
329
}
330
331
// Handle relative patterns
332
let pattern: string;
333
if (typeof arg1 !== 'string') {
334
pattern = arg1.pattern;
335
} else {
336
pattern = arg1;
337
}
338
339
// Whitespace trimming
340
pattern = pattern.trim();
341
342
// Check cache
343
const patternKey = `${pattern}_${!!options.trimForExclusions}`;
344
let parsedPattern = CACHE.get(patternKey);
345
if (parsedPattern) {
346
return wrapRelativePattern(parsedPattern, arg1);
347
}
348
349
// Check for Trivials
350
let match: RegExpExecArray | null;
351
if (T1.test(pattern)) {
352
parsedPattern = trivia1(pattern.substr(4), pattern); // common pattern: **/*.txt just need endsWith check
353
} else if (match = T2.exec(trimForExclusions(pattern, options))) { // common pattern: **/some.txt just need basename check
354
parsedPattern = trivia2(match[1], pattern);
355
} else if ((options.trimForExclusions ? T3_2 : T3).test(pattern)) { // repetition of common patterns (see above) {**/*.txt,**/*.png}
356
parsedPattern = trivia3(pattern, options);
357
} else if (match = T4.exec(trimForExclusions(pattern, options))) { // common pattern: **/something/else just need endsWith check
358
parsedPattern = trivia4and5(match[1].substr(1), pattern, true);
359
} else if (match = T5.exec(trimForExclusions(pattern, options))) { // common pattern: something/else just need equals check
360
parsedPattern = trivia4and5(match[1], pattern, false);
361
}
362
363
// Otherwise convert to pattern
364
else {
365
parsedPattern = toRegExp(pattern);
366
}
367
368
// Cache
369
CACHE.set(patternKey, parsedPattern);
370
371
return wrapRelativePattern(parsedPattern, arg1);
372
}
373
374
function wrapRelativePattern(parsedPattern: ParsedStringPattern, arg2: string | IRelativePattern): ParsedStringPattern {
375
if (typeof arg2 === 'string') {
376
return parsedPattern;
377
}
378
379
const wrappedPattern: ParsedStringPattern = function (path, basename) {
380
if (!isEqualOrParent(path, arg2.base, !isLinux)) {
381
// skip glob matching if `base` is not a parent of `path`
382
return null;
383
}
384
385
// Given we have checked `base` being a parent of `path`,
386
// we can now remove the `base` portion of the `path`
387
// and only match on the remaining path components
388
// For that we try to extract the portion of the `path`
389
// that comes after the `base` portion. We have to account
390
// for the fact that `base` might end in a path separator
391
// (https://github.com/microsoft/vscode/issues/162498)
392
393
return parsedPattern(ltrim(path.substr(arg2.base.length), sep), basename);
394
};
395
396
// Make sure to preserve associated metadata
397
wrappedPattern.allBasenames = parsedPattern.allBasenames;
398
wrappedPattern.allPaths = parsedPattern.allPaths;
399
wrappedPattern.basenames = parsedPattern.basenames;
400
wrappedPattern.patterns = parsedPattern.patterns;
401
402
return wrappedPattern;
403
}
404
405
function trimForExclusions(pattern: string, options: IGlobOptions): string {
406
return options.trimForExclusions && pattern.endsWith('/**') ? pattern.substr(0, pattern.length - 2) : pattern; // dropping **, tailing / is dropped later
407
}
408
409
// common pattern: **/*.txt just need endsWith check
410
function trivia1(base: string, pattern: string): ParsedStringPattern {
411
return function (path: string, basename?: string) {
412
return typeof path === 'string' && path.endsWith(base) ? pattern : null;
413
};
414
}
415
416
// common pattern: **/some.txt just need basename check
417
function trivia2(base: string, pattern: string): ParsedStringPattern {
418
const slashBase = `/${base}`;
419
const backslashBase = `\\${base}`;
420
421
const parsedPattern: ParsedStringPattern = function (path: string, basename?: string) {
422
if (typeof path !== 'string') {
423
return null;
424
}
425
426
if (basename) {
427
return basename === base ? pattern : null;
428
}
429
430
return path === base || path.endsWith(slashBase) || path.endsWith(backslashBase) ? pattern : null;
431
};
432
433
const basenames = [base];
434
parsedPattern.basenames = basenames;
435
parsedPattern.patterns = [pattern];
436
parsedPattern.allBasenames = basenames;
437
438
return parsedPattern;
439
}
440
441
// repetition of common patterns (see above) {**/*.txt,**/*.png}
442
function trivia3(pattern: string, options: IGlobOptions): ParsedStringPattern {
443
const parsedPatterns = aggregateBasenameMatches(pattern.slice(1, -1)
444
.split(',')
445
.map(pattern => parsePattern(pattern, options))
446
.filter(pattern => pattern !== NULL), pattern);
447
448
const patternsLength = parsedPatterns.length;
449
if (!patternsLength) {
450
return NULL;
451
}
452
453
if (patternsLength === 1) {
454
return parsedPatterns[0];
455
}
456
457
const parsedPattern: ParsedStringPattern = function (path: string, basename?: string) {
458
for (let i = 0, n = parsedPatterns.length; i < n; i++) {
459
if (parsedPatterns[i](path, basename)) {
460
return pattern;
461
}
462
}
463
464
return null;
465
};
466
467
const withBasenames = parsedPatterns.find(pattern => !!pattern.allBasenames);
468
if (withBasenames) {
469
parsedPattern.allBasenames = withBasenames.allBasenames;
470
}
471
472
const allPaths = parsedPatterns.reduce((all, current) => current.allPaths ? all.concat(current.allPaths) : all, [] as string[]);
473
if (allPaths.length) {
474
parsedPattern.allPaths = allPaths;
475
}
476
477
return parsedPattern;
478
}
479
480
// common patterns: **/something/else just need endsWith check, something/else just needs and equals check
481
function trivia4and5(targetPath: string, pattern: string, matchPathEnds: boolean): ParsedStringPattern {
482
const usingPosixSep = sep === posix.sep;
483
const nativePath = usingPosixSep ? targetPath : targetPath.replace(ALL_FORWARD_SLASHES, sep);
484
const nativePathEnd = sep + nativePath;
485
const targetPathEnd = posix.sep + targetPath;
486
487
let parsedPattern: ParsedStringPattern;
488
if (matchPathEnds) {
489
parsedPattern = function (path: string, basename?: string) {
490
return typeof path === 'string' && ((path === nativePath || path.endsWith(nativePathEnd)) || !usingPosixSep && (path === targetPath || path.endsWith(targetPathEnd))) ? pattern : null;
491
};
492
} else {
493
parsedPattern = function (path: string, basename?: string) {
494
return typeof path === 'string' && (path === nativePath || (!usingPosixSep && path === targetPath)) ? pattern : null;
495
};
496
}
497
498
parsedPattern.allPaths = [(matchPathEnds ? '*/' : './') + targetPath];
499
500
return parsedPattern;
501
}
502
503
function toRegExp(pattern: string): ParsedStringPattern {
504
try {
505
const regExp = new RegExp(`^${parseRegExp(pattern)}$`);
506
return function (path: string) {
507
regExp.lastIndex = 0; // reset RegExp to its initial state to reuse it!
508
509
return typeof path === 'string' && regExp.test(path) ? pattern : null;
510
};
511
} catch (error) {
512
return NULL;
513
}
514
}
515
516
/**
517
* Simplified glob matching. Supports a subset of glob patterns:
518
* * `*` to match zero or more characters in a path segment
519
* * `?` to match on one character in a path segment
520
* * `**` to match any number of path segments, including none
521
* * `{}` to group conditions (e.g. *.{ts,js} matches all TypeScript and JavaScript files)
522
* * `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …)
523
* * `[!...]` 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`)
524
*/
525
export function match(pattern: string | IRelativePattern, path: string): boolean;
526
export function match(expression: IExpression, path: string, hasSibling?: (name: string) => boolean): string /* the matching pattern */;
527
export function match(arg1: string | IExpression | IRelativePattern, path: string, hasSibling?: (name: string) => boolean): boolean | string | null | Promise<string | null> {
528
if (!arg1 || typeof path !== 'string') {
529
return false;
530
}
531
532
return parse(arg1)(path, undefined, hasSibling);
533
}
534
535
/**
536
* Simplified glob matching. Supports a subset of glob patterns:
537
* * `*` to match zero or more characters in a path segment
538
* * `?` to match on one character in a path segment
539
* * `**` to match any number of path segments, including none
540
* * `{}` to group conditions (e.g. *.{ts,js} matches all TypeScript and JavaScript files)
541
* * `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …)
542
* * `[!...]` 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`)
543
*/
544
export function parse(pattern: string | IRelativePattern, options?: IGlobOptions): ParsedPattern;
545
export function parse(expression: IExpression, options?: IGlobOptions): ParsedExpression;
546
export function parse(arg1: string | IExpression | IRelativePattern, options?: IGlobOptions): ParsedPattern | ParsedExpression;
547
export function parse(arg1: string | IExpression | IRelativePattern, options: IGlobOptions = {}): ParsedPattern | ParsedExpression {
548
if (!arg1) {
549
return FALSE;
550
}
551
552
// Glob with String
553
if (typeof arg1 === 'string' || isRelativePattern(arg1)) {
554
const parsedPattern = parsePattern(arg1, options);
555
if (parsedPattern === NULL) {
556
return FALSE;
557
}
558
559
const resultPattern: ParsedPattern & { allBasenames?: string[]; allPaths?: string[] } = function (path: string, basename?: string) {
560
return !!parsedPattern(path, basename);
561
};
562
563
if (parsedPattern.allBasenames) {
564
resultPattern.allBasenames = parsedPattern.allBasenames;
565
}
566
567
if (parsedPattern.allPaths) {
568
resultPattern.allPaths = parsedPattern.allPaths;
569
}
570
571
return resultPattern;
572
}
573
574
// Glob with Expression
575
return parsedExpression(<IExpression>arg1, options);
576
}
577
578
export function isRelativePattern(obj: unknown): obj is IRelativePattern {
579
const rp = obj as IRelativePattern | undefined | null;
580
if (!rp) {
581
return false;
582
}
583
584
return typeof rp.base === 'string' && typeof rp.pattern === 'string';
585
}
586
587
export function getBasenameTerms(patternOrExpression: ParsedPattern | ParsedExpression): string[] {
588
return (<ParsedStringPattern>patternOrExpression).allBasenames || [];
589
}
590
591
export function getPathTerms(patternOrExpression: ParsedPattern | ParsedExpression): string[] {
592
return (<ParsedStringPattern>patternOrExpression).allPaths || [];
593
}
594
595
function parsedExpression(expression: IExpression, options: IGlobOptions): ParsedExpression {
596
const parsedPatterns = aggregateBasenameMatches(Object.getOwnPropertyNames(expression)
597
.map(pattern => parseExpressionPattern(pattern, expression[pattern], options))
598
.filter(pattern => pattern !== NULL));
599
600
const patternsLength = parsedPatterns.length;
601
if (!patternsLength) {
602
return NULL;
603
}
604
605
if (!parsedPatterns.some(parsedPattern => !!(<ParsedExpressionPattern>parsedPattern).requiresSiblings)) {
606
if (patternsLength === 1) {
607
return parsedPatterns[0] as ParsedStringPattern;
608
}
609
610
const resultExpression: ParsedStringPattern = function (path: string, basename?: string) {
611
let resultPromises: Promise<string | null>[] | undefined = undefined;
612
613
for (let i = 0, n = parsedPatterns.length; i < n; i++) {
614
const result = parsedPatterns[i](path, basename);
615
if (typeof result === 'string') {
616
return result; // immediately return as soon as the first expression matches
617
}
618
619
// If the result is a promise, we have to keep it for
620
// later processing and await the result properly.
621
if (isThenable(result)) {
622
if (!resultPromises) {
623
resultPromises = [];
624
}
625
626
resultPromises.push(result);
627
}
628
}
629
630
// With result promises, we have to loop over each and
631
// await the result before we can return any result.
632
if (resultPromises) {
633
return (async () => {
634
for (const resultPromise of resultPromises) {
635
const result = await resultPromise;
636
if (typeof result === 'string') {
637
return result;
638
}
639
}
640
641
return null;
642
})();
643
}
644
645
return null;
646
};
647
648
const withBasenames = parsedPatterns.find(pattern => !!pattern.allBasenames);
649
if (withBasenames) {
650
resultExpression.allBasenames = withBasenames.allBasenames;
651
}
652
653
const allPaths = parsedPatterns.reduce((all, current) => current.allPaths ? all.concat(current.allPaths) : all, [] as string[]);
654
if (allPaths.length) {
655
resultExpression.allPaths = allPaths;
656
}
657
658
return resultExpression;
659
}
660
661
const resultExpression: ParsedStringPattern = function (path: string, base?: string, hasSibling?: (name: string) => boolean | Promise<boolean>) {
662
let name: string | undefined = undefined;
663
let resultPromises: Promise<string | null>[] | undefined = undefined;
664
665
for (let i = 0, n = parsedPatterns.length; i < n; i++) {
666
667
// Pattern matches path
668
const parsedPattern = (<ParsedExpressionPattern>parsedPatterns[i]);
669
if (parsedPattern.requiresSiblings && hasSibling) {
670
if (!base) {
671
base = basename(path);
672
}
673
674
if (!name) {
675
name = base.substr(0, base.length - extname(path).length);
676
}
677
}
678
679
const result = parsedPattern(path, base, name, hasSibling);
680
if (typeof result === 'string') {
681
return result; // immediately return as soon as the first expression matches
682
}
683
684
// If the result is a promise, we have to keep it for
685
// later processing and await the result properly.
686
if (isThenable(result)) {
687
if (!resultPromises) {
688
resultPromises = [];
689
}
690
691
resultPromises.push(result);
692
}
693
}
694
695
// With result promises, we have to loop over each and
696
// await the result before we can return any result.
697
if (resultPromises) {
698
return (async () => {
699
for (const resultPromise of resultPromises) {
700
const result = await resultPromise;
701
if (typeof result === 'string') {
702
return result;
703
}
704
}
705
706
return null;
707
})();
708
}
709
710
return null;
711
};
712
713
const withBasenames = parsedPatterns.find(pattern => !!pattern.allBasenames);
714
if (withBasenames) {
715
resultExpression.allBasenames = withBasenames.allBasenames;
716
}
717
718
const allPaths = parsedPatterns.reduce((all, current) => current.allPaths ? all.concat(current.allPaths) : all, [] as string[]);
719
if (allPaths.length) {
720
resultExpression.allPaths = allPaths;
721
}
722
723
return resultExpression;
724
}
725
726
function parseExpressionPattern(pattern: string, value: boolean | SiblingClause, options: IGlobOptions): (ParsedStringPattern | ParsedExpressionPattern) {
727
if (value === false) {
728
return NULL; // pattern is disabled
729
}
730
731
const parsedPattern = parsePattern(pattern, options);
732
if (parsedPattern === NULL) {
733
return NULL;
734
}
735
736
// Expression Pattern is <boolean>
737
if (typeof value === 'boolean') {
738
return parsedPattern;
739
}
740
741
// Expression Pattern is <SiblingClause>
742
if (value) {
743
const when = value.when;
744
if (typeof when === 'string') {
745
const result: ParsedExpressionPattern = (path: string, basename?: string, name?: string, hasSibling?: (name: string) => boolean | Promise<boolean>) => {
746
if (!hasSibling || !parsedPattern(path, basename)) {
747
return null;
748
}
749
750
const clausePattern = when.replace('$(basename)', () => name!);
751
const matched = hasSibling(clausePattern);
752
return isThenable(matched) ?
753
matched.then(match => match ? pattern : null) :
754
matched ? pattern : null;
755
};
756
757
result.requiresSiblings = true;
758
759
return result;
760
}
761
}
762
763
// Expression is anything
764
return parsedPattern;
765
}
766
767
function aggregateBasenameMatches(parsedPatterns: Array<ParsedStringPattern | ParsedExpressionPattern>, result?: string): Array<ParsedStringPattern | ParsedExpressionPattern> {
768
const basenamePatterns = parsedPatterns.filter(parsedPattern => !!(<ParsedStringPattern>parsedPattern).basenames);
769
if (basenamePatterns.length < 2) {
770
return parsedPatterns;
771
}
772
773
const basenames = basenamePatterns.reduce<string[]>((all, current) => {
774
const basenames = (<ParsedStringPattern>current).basenames;
775
776
return basenames ? all.concat(basenames) : all;
777
}, [] as string[]);
778
779
let patterns: string[];
780
if (result) {
781
patterns = [];
782
783
for (let i = 0, n = basenames.length; i < n; i++) {
784
patterns.push(result);
785
}
786
} else {
787
patterns = basenamePatterns.reduce((all, current) => {
788
const patterns = (<ParsedStringPattern>current).patterns;
789
790
return patterns ? all.concat(patterns) : all;
791
}, [] as string[]);
792
}
793
794
const aggregate: ParsedStringPattern = function (path: string, basename?: string) {
795
if (typeof path !== 'string') {
796
return null;
797
}
798
799
if (!basename) {
800
let i: number;
801
for (i = path.length; i > 0; i--) {
802
const ch = path.charCodeAt(i - 1);
803
if (ch === CharCode.Slash || ch === CharCode.Backslash) {
804
break;
805
}
806
}
807
808
basename = path.substr(i);
809
}
810
811
const index = basenames.indexOf(basename);
812
return index !== -1 ? patterns[index] : null;
813
};
814
815
aggregate.basenames = basenames;
816
aggregate.patterns = patterns;
817
aggregate.allBasenames = basenames;
818
819
const aggregatedPatterns = parsedPatterns.filter(parsedPattern => !(<ParsedStringPattern>parsedPattern).basenames);
820
aggregatedPatterns.push(aggregate);
821
822
return aggregatedPatterns;
823
}
824
825
export function patternsEquals(patternsA: Array<string | IRelativePattern> | undefined, patternsB: Array<string | IRelativePattern> | undefined): boolean {
826
return equals(patternsA, patternsB, (a, b) => {
827
if (typeof a === 'string' && typeof b === 'string') {
828
return a === b;
829
}
830
831
if (typeof a !== 'string' && typeof b !== 'string') {
832
return a.base === b.base && a.pattern === b.pattern;
833
}
834
835
return false;
836
});
837
}
838
839