Skip to content

Commit 9414d3c

Browse files
daeyeonaduh95
authored andcommitted
fs: allow exclude option in globs to accept glob patterns
Signed-off-by: Daeyeon Jeong <[email protected]> PR-URL: #56489 Reviewed-By: James M Snell <[email protected]> Reviewed-By: Chemi Atlow <[email protected]> Reviewed-By: Moshe Atlow <[email protected]> Reviewed-By: Jason Zhang <[email protected]>
1 parent a4045c9 commit 9414d3c

File tree

3 files changed

+227
-36
lines changed

3 files changed

+227
-36
lines changed

doc/api/fs.md

+15-3
Original file line numberDiff line numberDiff line change
@@ -1074,6 +1074,9 @@ behavior is similar to `cp dir1/ dir2/`.
10741074
<!-- YAML
10751075
added: v22.0.0
10761076
changes:
1077+
- version: REPLACEME
1078+
pr-url: https://github.com/nodejs/node/pull/56489
1079+
description: Add support for `exclude` option to accept glob patterns.
10771080
- version: v22.2.0
10781081
pr-url: https://github.com/nodejs/node/pull/52837
10791082
description: Add support for `withFileTypes` as an option.
@@ -1084,7 +1087,8 @@ changes:
10841087
* `pattern` {string|string\[]}
10851088
* `options` {Object}
10861089
* `cwd` {string} current working directory. **Default:** `process.cwd()`
1087-
* `exclude` {Function} Function to filter out files/directories. Return
1090+
* `exclude` {Function|string\[]} Function to filter out files/directories or a
1091+
list of glob patterns to be excluded. If a function is provided, return
10881092
`true` to exclude the item, `false` to include it. **Default:** `undefined`.
10891093
* `withFileTypes` {boolean} `true` if the glob should return paths as Dirents,
10901094
`false` otherwise. **Default:** `false`.
@@ -3120,6 +3124,9 @@ descriptor. See [`fs.utimes()`][].
31203124
<!-- YAML
31213125
added: v22.0.0
31223126
changes:
3127+
- version: REPLACEME
3128+
pr-url: https://github.com/nodejs/node/pull/56489
3129+
description: Add support for `exclude` option to accept glob patterns.
31233130
- version: v22.2.0
31243131
pr-url: https://github.com/nodejs/node/pull/52837
31253132
description: Add support for `withFileTypes` as an option.
@@ -3131,7 +3138,8 @@ changes:
31313138
31323139
* `options` {Object}
31333140
* `cwd` {string} current working directory. **Default:** `process.cwd()`
3134-
* `exclude` {Function} Function to filter out files/directories. Return
3141+
* `exclude` {Function|string\[]} Function to filter out files/directories or a
3142+
list of glob patterns to be excluded. If a function is provided, return
31353143
`true` to exclude the item, `false` to include it. **Default:** `undefined`.
31363144
* `withFileTypes` {boolean} `true` if the glob should return paths as Dirents,
31373145
`false` otherwise. **Default:** `false`.
@@ -5656,6 +5664,9 @@ Synchronous version of [`fs.futimes()`][]. Returns `undefined`.
56565664
<!-- YAML
56575665
added: v22.0.0
56585666
changes:
5667+
- version: REPLACEME
5668+
pr-url: https://github.com/nodejs/node/pull/56489
5669+
description: Add support for `exclude` option to accept glob patterns.
56595670
- version: v22.2.0
56605671
pr-url: https://github.com/nodejs/node/pull/52837
56615672
description: Add support for `withFileTypes` as an option.
@@ -5666,7 +5677,8 @@ changes:
56665677
* `pattern` {string|string\[]}
56675678
* `options` {Object}
56685679
* `cwd` {string} current working directory. **Default:** `process.cwd()`
5669-
* `exclude` {Function} Function to filter out files/directories. Return
5680+
* `exclude` {Function|string\[]} Function to filter out files/directories or a
5681+
list of glob patterns to be excluded. If a function is provided, return
56705682
`true` to exclude the item, `false` to include it. **Default:** `undefined`.
56715683
* `withFileTypes` {boolean} `true` if the glob should return paths as Dirents,
56725684
`false` otherwise. **Default:** `false`.

lib/internal/fs/glob.js

+118-33
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
const {
44
ArrayFrom,
5+
ArrayIsArray,
56
ArrayPrototypeAt,
67
ArrayPrototypeFlatMap,
78
ArrayPrototypeMap,
@@ -24,12 +25,18 @@ const {
2425
isMacOS,
2526
} = require('internal/util');
2627
const {
27-
validateFunction,
2828
validateObject,
2929
validateString,
3030
validateStringArray,
3131
} = require('internal/validators');
3232
const { DirentFromStats } = require('internal/fs/utils');
33+
const {
34+
codes: {
35+
ERR_INVALID_ARG_TYPE,
36+
},
37+
hideStackFrames,
38+
} = require('internal/errors');
39+
const assert = require('internal/assert');
3340

3441
let minimatch;
3542
function lazyMinimatch() {
@@ -63,6 +70,45 @@ function getDirentSync(path) {
6370
return new DirentFromStats(basename(path), stat, dirname(path));
6471
}
6572

73+
/**
74+
* @callback validateStringArrayOrFunction
75+
* @param {*} value
76+
* @param {string} name
77+
*/
78+
const validateStringArrayOrFunction = hideStackFrames((value, name) => {
79+
if (ArrayIsArray(value)) {
80+
for (let i = 0; i < value.length; ++i) {
81+
if (typeof value[i] !== 'string') {
82+
throw new ERR_INVALID_ARG_TYPE(`${name}[${i}]`, 'string', value[i]);
83+
}
84+
}
85+
return;
86+
}
87+
if (typeof value !== 'function') {
88+
throw new ERR_INVALID_ARG_TYPE(name, ['string[]', 'function'], value);
89+
}
90+
});
91+
92+
/**
93+
* @param {string} pattern
94+
* @param {options} options
95+
* @returns {Minimatch}
96+
*/
97+
function createMatcher(pattern, options = kEmptyObject) {
98+
const opts = {
99+
__proto__: null,
100+
nocase: isWindows || isMacOS,
101+
windowsPathsNoEscape: true,
102+
nonegate: true,
103+
nocomment: true,
104+
optimizationLevel: 2,
105+
platform: process.platform,
106+
nocaseMagicOnly: true,
107+
...options,
108+
};
109+
return new (lazyMinimatch().Minimatch)(pattern, opts);
110+
}
111+
66112
class Cache {
67113
#cache = new SafeMap();
68114
#statsCache = new SafeMap();
@@ -188,24 +234,56 @@ class Pattern {
188234
}
189235
}
190236

237+
class ResultSet extends SafeSet {
238+
#root = '.';
239+
#isExcluded = () => false;
240+
constructor(i) { super(i); } // eslint-disable-line no-useless-constructor
241+
242+
setup(root, isExcludedFn) {
243+
this.#root = root;
244+
this.#isExcluded = isExcludedFn;
245+
}
246+
247+
add(value) {
248+
if (this.#isExcluded(resolve(this.#root, value))) {
249+
return false;
250+
}
251+
super.add(value);
252+
return true;
253+
}
254+
}
255+
191256
class Glob {
192257
#root;
193258
#exclude;
194259
#cache = new Cache();
195-
#results = new SafeSet();
260+
#results = new ResultSet();
196261
#queue = [];
197262
#subpatterns = new SafeMap();
198263
#patterns;
199264
#withFileTypes;
265+
#isExcluded = () => false;
200266
constructor(pattern, options = kEmptyObject) {
201267
validateObject(options, 'options');
202268
const { exclude, cwd, withFileTypes } = options;
203-
if (exclude != null) {
204-
validateFunction(exclude, 'options.exclude');
205-
}
206269
this.#root = cwd ?? '.';
207-
this.#exclude = exclude;
208270
this.#withFileTypes = !!withFileTypes;
271+
if (exclude != null) {
272+
validateStringArrayOrFunction(exclude, 'options.exclude');
273+
if (ArrayIsArray(exclude)) {
274+
assert(typeof this.#root === 'string');
275+
// Convert the path part of exclude patterns to absolute paths for
276+
// consistent comparison before instantiating matchers.
277+
const matchers = exclude
278+
.map((pattern) => resolve(this.#root, pattern))
279+
.map((pattern) => createMatcher(pattern));
280+
this.#isExcluded = (value) =>
281+
matchers.some((matcher) => matcher.match(value));
282+
this.#results.setup(this.#root, this.#isExcluded);
283+
} else {
284+
this.#exclude = exclude;
285+
}
286+
}
209287
let patterns;
210288
if (typeof pattern === 'object') {
211289
validateStringArray(pattern, 'patterns');
@@ -214,17 +292,7 @@ class Glob {
214292
validateString(pattern, 'patterns');
215293
patterns = [pattern];
216294
}
217-
this.matchers = ArrayPrototypeMap(patterns, (pattern) => new (lazyMinimatch().Minimatch)(pattern, {
218-
__proto__: null,
219-
nocase: isWindows || isMacOS,
220-
windowsPathsNoEscape: true,
221-
nonegate: true,
222-
nocomment: true,
223-
optimizationLevel: 2,
224-
platform: process.platform,
225-
nocaseMagicOnly: true,
226-
}));
227-
295+
this.matchers = ArrayPrototypeMap(patterns, (pattern) => createMatcher(pattern));
228296
this.#patterns = ArrayPrototypeFlatMap(this.matchers, (matcher) => ArrayPrototypeMap(matcher.set,
229297
(pattern, i) => new Pattern(
230298
pattern,
@@ -255,6 +323,9 @@ class Glob {
255323
);
256324
}
257325
#addSubpattern(path, pattern) {
326+
if (this.#isExcluded(path)) {
327+
return;
328+
}
258329
if (!this.#subpatterns.has(path)) {
259330
this.#subpatterns.set(path, [pattern]);
260331
} else {
@@ -273,6 +344,9 @@ class Glob {
273344
const isLast = pattern.isLast(isDirectory);
274345
const isFirst = pattern.isFirst();
275346

347+
if (this.#isExcluded(fullpath)) {
348+
return;
349+
}
276350
if (isFirst && isWindows && typeof pattern.at(0) === 'string' && StringPrototypeEndsWith(pattern.at(0), ':')) {
277351
// Absolute path, go to root
278352
this.#addSubpattern(`${pattern.at(0)}\\`, pattern.child(new SafeSet().add(1)));
@@ -461,6 +535,9 @@ class Glob {
461535
const isLast = pattern.isLast(isDirectory);
462536
const isFirst = pattern.isFirst();
463537

538+
if (this.#isExcluded(fullpath)) {
539+
return;
540+
}
464541
if (isFirst && isWindows && typeof pattern.at(0) === 'string' && StringPrototypeEndsWith(pattern.at(0), ':')) {
465542
// Absolute path, go to root
466543
this.#addSubpattern(`${pattern.at(0)}\\`, pattern.child(new SafeSet().add(1)));
@@ -489,8 +566,9 @@ class Glob {
489566
if (stat && (p || isDirectory)) {
490567
const result = join(path, p);
491568
if (!this.#results.has(result)) {
492-
this.#results.add(result);
493-
yield this.#withFileTypes ? stat : result;
569+
if (this.#results.add(result)) {
570+
yield this.#withFileTypes ? stat : result;
571+
}
494572
}
495573
}
496574
if (pattern.indexes.size === 1 && pattern.indexes.has(last)) {
@@ -501,8 +579,9 @@ class Glob {
501579
// If pattern ends with **, add to results
502580
// if path is ".", add it only if pattern starts with "." or pattern is exactly "**"
503581
if (!this.#results.has(path)) {
504-
this.#results.add(path);
505-
yield this.#withFileTypes ? stat : path;
582+
if (this.#results.add(path)) {
583+
yield this.#withFileTypes ? stat : path;
584+
}
506585
}
507586
}
508587

@@ -551,8 +630,9 @@ class Glob {
551630
} else if (!fromSymlink && index === last) {
552631
// If ** is last, add to results
553632
if (!this.#results.has(entryPath)) {
554-
this.#results.add(entryPath);
555-
yield this.#withFileTypes ? entry : entryPath;
633+
if (this.#results.add(entryPath)) {
634+
yield this.#withFileTypes ? entry : entryPath;
635+
}
556636
}
557637
}
558638

@@ -562,8 +642,9 @@ class Glob {
562642
if (nextMatches && nextIndex === last && !isLast) {
563643
// If next pattern is the last one, add to results
564644
if (!this.#results.has(entryPath)) {
565-
this.#results.add(entryPath);
566-
yield this.#withFileTypes ? entry : entryPath;
645+
if (this.#results.add(entryPath)) {
646+
yield this.#withFileTypes ? entry : entryPath;
647+
}
567648
}
568649
} else if (nextMatches && entry.isDirectory()) {
569650
// Pattern matched, meaning two patterns forward
@@ -598,15 +679,17 @@ class Glob {
598679
if (!this.#cache.seen(path, pattern, nextIndex)) {
599680
this.#cache.add(path, pattern.child(new SafeSet().add(nextIndex)));
600681
if (!this.#results.has(path)) {
601-
this.#results.add(path);
602-
yield this.#withFileTypes ? this.#cache.statSync(fullpath) : path;
682+
if (this.#results.add(path)) {
683+
yield this.#withFileTypes ? this.#cache.statSync(fullpath) : path;
684+
}
603685
}
604686
}
605687
if (!this.#cache.seen(path, pattern, nextIndex) || !this.#cache.seen(parent, pattern, nextIndex)) {
606688
this.#cache.add(parent, pattern.child(new SafeSet().add(nextIndex)));
607689
if (!this.#results.has(parent)) {
608-
this.#results.add(parent);
609-
yield this.#withFileTypes ? this.#cache.statSync(join(this.#root, parent)) : parent;
690+
if (this.#results.add(parent)) {
691+
yield this.#withFileTypes ? this.#cache.statSync(join(this.#root, parent)) : parent;
692+
}
610693
}
611694
}
612695
}
@@ -621,8 +704,9 @@ class Glob {
621704
// If current pattern is ".", proceed to test next pattern
622705
if (nextIndex === last) {
623706
if (!this.#results.has(entryPath)) {
624-
this.#results.add(entryPath);
625-
yield this.#withFileTypes ? entry : entryPath;
707+
if (this.#results.add(entryPath)) {
708+
yield this.#withFileTypes ? entry : entryPath;
709+
}
626710
}
627711
} else {
628712
subPatterns.add(nextIndex + 1);
@@ -634,8 +718,9 @@ class Glob {
634718
// add next pattern to potential patterns, or to results if it's the last pattern
635719
if (index === last) {
636720
if (!this.#results.has(entryPath)) {
637-
this.#results.add(entryPath);
638-
yield this.#withFileTypes ? entry : entryPath;
721+
if (this.#results.add(entryPath)) {
722+
yield this.#withFileTypes ? entry : entryPath;
723+
}
639724
}
640725
} else if (entry.isDirectory()) {
641726
subPatterns.add(nextIndex);

0 commit comments

Comments
 (0)