mirror of
https://github.com/nodejs/node.git
synced 2025-08-15 13:48:44 +02:00
fs: allow exclude
option in globs to accept glob patterns
Signed-off-by: Daeyeon Jeong <daeyeon.dev@gmail.com> PR-URL: https://github.com/nodejs/node/pull/56489 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Chemi Atlow <chemi@atlow.co.il> Reviewed-By: Moshe Atlow <moshe@atlow.co.il> Reviewed-By: Jason Zhang <xzha4350@gmail.com>
This commit is contained in:
parent
57478281aa
commit
dc5d0f9bb4
3 changed files with 227 additions and 36 deletions
|
@ -1074,6 +1074,9 @@ behavior is similar to `cp dir1/ dir2/`.
|
|||
<!-- YAML
|
||||
added: v22.0.0
|
||||
changes:
|
||||
- version: REPLACEME
|
||||
pr-url: https://github.com/nodejs/node/pull/56489
|
||||
description: Add support for `exclude` option to accept glob patterns.
|
||||
- version: v22.2.0
|
||||
pr-url: https://github.com/nodejs/node/pull/52837
|
||||
description: Add support for `withFileTypes` as an option.
|
||||
|
@ -1084,7 +1087,8 @@ changes:
|
|||
* `pattern` {string|string\[]}
|
||||
* `options` {Object}
|
||||
* `cwd` {string} current working directory. **Default:** `process.cwd()`
|
||||
* `exclude` {Function} Function to filter out files/directories. Return
|
||||
* `exclude` {Function|string\[]} Function to filter out files/directories or a
|
||||
list of glob patterns to be excluded. If a function is provided, return
|
||||
`true` to exclude the item, `false` to include it. **Default:** `undefined`.
|
||||
* `withFileTypes` {boolean} `true` if the glob should return paths as Dirents,
|
||||
`false` otherwise. **Default:** `false`.
|
||||
|
@ -3120,6 +3124,9 @@ descriptor. See [`fs.utimes()`][].
|
|||
<!-- YAML
|
||||
added: v22.0.0
|
||||
changes:
|
||||
- version: REPLACEME
|
||||
pr-url: https://github.com/nodejs/node/pull/56489
|
||||
description: Add support for `exclude` option to accept glob patterns.
|
||||
- version: v22.2.0
|
||||
pr-url: https://github.com/nodejs/node/pull/52837
|
||||
description: Add support for `withFileTypes` as an option.
|
||||
|
@ -3131,7 +3138,8 @@ changes:
|
|||
|
||||
* `options` {Object}
|
||||
* `cwd` {string} current working directory. **Default:** `process.cwd()`
|
||||
* `exclude` {Function} Function to filter out files/directories. Return
|
||||
* `exclude` {Function|string\[]} Function to filter out files/directories or a
|
||||
list of glob patterns to be excluded. If a function is provided, return
|
||||
`true` to exclude the item, `false` to include it. **Default:** `undefined`.
|
||||
* `withFileTypes` {boolean} `true` if the glob should return paths as Dirents,
|
||||
`false` otherwise. **Default:** `false`.
|
||||
|
@ -5656,6 +5664,9 @@ Synchronous version of [`fs.futimes()`][]. Returns `undefined`.
|
|||
<!-- YAML
|
||||
added: v22.0.0
|
||||
changes:
|
||||
- version: REPLACEME
|
||||
pr-url: https://github.com/nodejs/node/pull/56489
|
||||
description: Add support for `exclude` option to accept glob patterns.
|
||||
- version: v22.2.0
|
||||
pr-url: https://github.com/nodejs/node/pull/52837
|
||||
description: Add support for `withFileTypes` as an option.
|
||||
|
@ -5666,7 +5677,8 @@ changes:
|
|||
* `pattern` {string|string\[]}
|
||||
* `options` {Object}
|
||||
* `cwd` {string} current working directory. **Default:** `process.cwd()`
|
||||
* `exclude` {Function} Function to filter out files/directories. Return
|
||||
* `exclude` {Function|string\[]} Function to filter out files/directories or a
|
||||
list of glob patterns to be excluded. If a function is provided, return
|
||||
`true` to exclude the item, `false` to include it. **Default:** `undefined`.
|
||||
* `withFileTypes` {boolean} `true` if the glob should return paths as Dirents,
|
||||
`false` otherwise. **Default:** `false`.
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
const {
|
||||
ArrayFrom,
|
||||
ArrayIsArray,
|
||||
ArrayPrototypeAt,
|
||||
ArrayPrototypeFlatMap,
|
||||
ArrayPrototypeMap,
|
||||
|
@ -24,12 +25,18 @@ const {
|
|||
isMacOS,
|
||||
} = require('internal/util');
|
||||
const {
|
||||
validateFunction,
|
||||
validateObject,
|
||||
validateString,
|
||||
validateStringArray,
|
||||
} = require('internal/validators');
|
||||
const { DirentFromStats } = require('internal/fs/utils');
|
||||
const {
|
||||
codes: {
|
||||
ERR_INVALID_ARG_TYPE,
|
||||
},
|
||||
hideStackFrames,
|
||||
} = require('internal/errors');
|
||||
const assert = require('internal/assert');
|
||||
|
||||
let minimatch;
|
||||
function lazyMinimatch() {
|
||||
|
@ -63,6 +70,45 @@ function getDirentSync(path) {
|
|||
return new DirentFromStats(basename(path), stat, dirname(path));
|
||||
}
|
||||
|
||||
/**
|
||||
* @callback validateStringArrayOrFunction
|
||||
* @param {*} value
|
||||
* @param {string} name
|
||||
*/
|
||||
const validateStringArrayOrFunction = hideStackFrames((value, name) => {
|
||||
if (ArrayIsArray(value)) {
|
||||
for (let i = 0; i < value.length; ++i) {
|
||||
if (typeof value[i] !== 'string') {
|
||||
throw new ERR_INVALID_ARG_TYPE(`${name}[${i}]`, 'string', value[i]);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (typeof value !== 'function') {
|
||||
throw new ERR_INVALID_ARG_TYPE(name, ['string[]', 'function'], value);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {string} pattern
|
||||
* @param {options} options
|
||||
* @returns {Minimatch}
|
||||
*/
|
||||
function createMatcher(pattern, options = kEmptyObject) {
|
||||
const opts = {
|
||||
__proto__: null,
|
||||
nocase: isWindows || isMacOS,
|
||||
windowsPathsNoEscape: true,
|
||||
nonegate: true,
|
||||
nocomment: true,
|
||||
optimizationLevel: 2,
|
||||
platform: process.platform,
|
||||
nocaseMagicOnly: true,
|
||||
...options,
|
||||
};
|
||||
return new (lazyMinimatch().Minimatch)(pattern, opts);
|
||||
}
|
||||
|
||||
class Cache {
|
||||
#cache = new SafeMap();
|
||||
#statsCache = new SafeMap();
|
||||
|
@ -188,24 +234,56 @@ class Pattern {
|
|||
}
|
||||
}
|
||||
|
||||
class ResultSet extends SafeSet {
|
||||
#root = '.';
|
||||
#isExcluded = () => false;
|
||||
constructor(i) { super(i); } // eslint-disable-line no-useless-constructor
|
||||
|
||||
setup(root, isExcludedFn) {
|
||||
this.#root = root;
|
||||
this.#isExcluded = isExcludedFn;
|
||||
}
|
||||
|
||||
add(value) {
|
||||
if (this.#isExcluded(resolve(this.#root, value))) {
|
||||
return false;
|
||||
}
|
||||
super.add(value);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class Glob {
|
||||
#root;
|
||||
#exclude;
|
||||
#cache = new Cache();
|
||||
#results = new SafeSet();
|
||||
#results = new ResultSet();
|
||||
#queue = [];
|
||||
#subpatterns = new SafeMap();
|
||||
#patterns;
|
||||
#withFileTypes;
|
||||
#isExcluded = () => false;
|
||||
constructor(pattern, options = kEmptyObject) {
|
||||
validateObject(options, 'options');
|
||||
const { exclude, cwd, withFileTypes } = options;
|
||||
if (exclude != null) {
|
||||
validateFunction(exclude, 'options.exclude');
|
||||
}
|
||||
this.#root = cwd ?? '.';
|
||||
this.#exclude = exclude;
|
||||
this.#withFileTypes = !!withFileTypes;
|
||||
if (exclude != null) {
|
||||
validateStringArrayOrFunction(exclude, 'options.exclude');
|
||||
if (ArrayIsArray(exclude)) {
|
||||
assert(typeof this.#root === 'string');
|
||||
// Convert the path part of exclude patterns to absolute paths for
|
||||
// consistent comparison before instantiating matchers.
|
||||
const matchers = exclude
|
||||
.map((pattern) => resolve(this.#root, pattern))
|
||||
.map((pattern) => createMatcher(pattern));
|
||||
this.#isExcluded = (value) =>
|
||||
matchers.some((matcher) => matcher.match(value));
|
||||
this.#results.setup(this.#root, this.#isExcluded);
|
||||
} else {
|
||||
this.#exclude = exclude;
|
||||
}
|
||||
}
|
||||
let patterns;
|
||||
if (typeof pattern === 'object') {
|
||||
validateStringArray(pattern, 'patterns');
|
||||
|
@ -214,17 +292,7 @@ class Glob {
|
|||
validateString(pattern, 'patterns');
|
||||
patterns = [pattern];
|
||||
}
|
||||
this.matchers = ArrayPrototypeMap(patterns, (pattern) => new (lazyMinimatch().Minimatch)(pattern, {
|
||||
__proto__: null,
|
||||
nocase: isWindows || isMacOS,
|
||||
windowsPathsNoEscape: true,
|
||||
nonegate: true,
|
||||
nocomment: true,
|
||||
optimizationLevel: 2,
|
||||
platform: process.platform,
|
||||
nocaseMagicOnly: true,
|
||||
}));
|
||||
|
||||
this.matchers = ArrayPrototypeMap(patterns, (pattern) => createMatcher(pattern));
|
||||
this.#patterns = ArrayPrototypeFlatMap(this.matchers, (matcher) => ArrayPrototypeMap(matcher.set,
|
||||
(pattern, i) => new Pattern(
|
||||
pattern,
|
||||
|
@ -255,6 +323,9 @@ class Glob {
|
|||
);
|
||||
}
|
||||
#addSubpattern(path, pattern) {
|
||||
if (this.#isExcluded(path)) {
|
||||
return;
|
||||
}
|
||||
if (!this.#subpatterns.has(path)) {
|
||||
this.#subpatterns.set(path, [pattern]);
|
||||
} else {
|
||||
|
@ -273,6 +344,9 @@ class Glob {
|
|||
const isLast = pattern.isLast(isDirectory);
|
||||
const isFirst = pattern.isFirst();
|
||||
|
||||
if (this.#isExcluded(fullpath)) {
|
||||
return;
|
||||
}
|
||||
if (isFirst && isWindows && typeof pattern.at(0) === 'string' && StringPrototypeEndsWith(pattern.at(0), ':')) {
|
||||
// Absolute path, go to root
|
||||
this.#addSubpattern(`${pattern.at(0)}\\`, pattern.child(new SafeSet().add(1)));
|
||||
|
@ -461,6 +535,9 @@ class Glob {
|
|||
const isLast = pattern.isLast(isDirectory);
|
||||
const isFirst = pattern.isFirst();
|
||||
|
||||
if (this.#isExcluded(fullpath)) {
|
||||
return;
|
||||
}
|
||||
if (isFirst && isWindows && typeof pattern.at(0) === 'string' && StringPrototypeEndsWith(pattern.at(0), ':')) {
|
||||
// Absolute path, go to root
|
||||
this.#addSubpattern(`${pattern.at(0)}\\`, pattern.child(new SafeSet().add(1)));
|
||||
|
@ -489,8 +566,9 @@ class Glob {
|
|||
if (stat && (p || isDirectory)) {
|
||||
const result = join(path, p);
|
||||
if (!this.#results.has(result)) {
|
||||
this.#results.add(result);
|
||||
yield this.#withFileTypes ? stat : result;
|
||||
if (this.#results.add(result)) {
|
||||
yield this.#withFileTypes ? stat : result;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (pattern.indexes.size === 1 && pattern.indexes.has(last)) {
|
||||
|
@ -501,8 +579,9 @@ class Glob {
|
|||
// If pattern ends with **, add to results
|
||||
// if path is ".", add it only if pattern starts with "." or pattern is exactly "**"
|
||||
if (!this.#results.has(path)) {
|
||||
this.#results.add(path);
|
||||
yield this.#withFileTypes ? stat : path;
|
||||
if (this.#results.add(path)) {
|
||||
yield this.#withFileTypes ? stat : path;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -551,8 +630,9 @@ class Glob {
|
|||
} else if (!fromSymlink && index === last) {
|
||||
// If ** is last, add to results
|
||||
if (!this.#results.has(entryPath)) {
|
||||
this.#results.add(entryPath);
|
||||
yield this.#withFileTypes ? entry : entryPath;
|
||||
if (this.#results.add(entryPath)) {
|
||||
yield this.#withFileTypes ? entry : entryPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -562,8 +642,9 @@ class Glob {
|
|||
if (nextMatches && nextIndex === last && !isLast) {
|
||||
// If next pattern is the last one, add to results
|
||||
if (!this.#results.has(entryPath)) {
|
||||
this.#results.add(entryPath);
|
||||
yield this.#withFileTypes ? entry : entryPath;
|
||||
if (this.#results.add(entryPath)) {
|
||||
yield this.#withFileTypes ? entry : entryPath;
|
||||
}
|
||||
}
|
||||
} else if (nextMatches && entry.isDirectory()) {
|
||||
// Pattern matched, meaning two patterns forward
|
||||
|
@ -598,15 +679,17 @@ class Glob {
|
|||
if (!this.#cache.seen(path, pattern, nextIndex)) {
|
||||
this.#cache.add(path, pattern.child(new SafeSet().add(nextIndex)));
|
||||
if (!this.#results.has(path)) {
|
||||
this.#results.add(path);
|
||||
yield this.#withFileTypes ? this.#cache.statSync(fullpath) : path;
|
||||
if (this.#results.add(path)) {
|
||||
yield this.#withFileTypes ? this.#cache.statSync(fullpath) : path;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!this.#cache.seen(path, pattern, nextIndex) || !this.#cache.seen(parent, pattern, nextIndex)) {
|
||||
this.#cache.add(parent, pattern.child(new SafeSet().add(nextIndex)));
|
||||
if (!this.#results.has(parent)) {
|
||||
this.#results.add(parent);
|
||||
yield this.#withFileTypes ? this.#cache.statSync(join(this.#root, parent)) : parent;
|
||||
if (this.#results.add(parent)) {
|
||||
yield this.#withFileTypes ? this.#cache.statSync(join(this.#root, parent)) : parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -621,8 +704,9 @@ class Glob {
|
|||
// If current pattern is ".", proceed to test next pattern
|
||||
if (nextIndex === last) {
|
||||
if (!this.#results.has(entryPath)) {
|
||||
this.#results.add(entryPath);
|
||||
yield this.#withFileTypes ? entry : entryPath;
|
||||
if (this.#results.add(entryPath)) {
|
||||
yield this.#withFileTypes ? entry : entryPath;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
subPatterns.add(nextIndex + 1);
|
||||
|
@ -634,8 +718,9 @@ class Glob {
|
|||
// add next pattern to potential patterns, or to results if it's the last pattern
|
||||
if (index === last) {
|
||||
if (!this.#results.has(entryPath)) {
|
||||
this.#results.add(entryPath);
|
||||
yield this.#withFileTypes ? entry : entryPath;
|
||||
if (this.#results.add(entryPath)) {
|
||||
yield this.#withFileTypes ? entry : entryPath;
|
||||
}
|
||||
}
|
||||
} else if (entry.isDirectory()) {
|
||||
subPatterns.add(nextIndex);
|
||||
|
|
|
@ -386,3 +386,97 @@ describe('fsPromises glob - withFileTypes', function() {
|
|||
});
|
||||
}
|
||||
});
|
||||
|
||||
// [pattern, exclude option, expected result]
|
||||
const pattern2 = [
|
||||
['a/{b,c}*', ['a/*c'], ['a/b', 'a/cb']],
|
||||
['a/{a,b,c}*', ['a/*bc*', 'a/cb'], ['a/b', 'a/c']],
|
||||
['a/**/[cg]', ['**/c'], ['a/abcdef/g', 'a/abcfed/g']],
|
||||
['a/**/[cg]', ['./**/c'], ['a/abcdef/g', 'a/abcfed/g']],
|
||||
['a/**/[cg]', ['a/**/[cg]/../c'], ['a/abcdef/g', 'a/abcfed/g']],
|
||||
['a/*/+(c|g)/*', ['**/./h'], ['a/b/c/d']],
|
||||
[
|
||||
'a/**/[cg]/../[cg]',
|
||||
['a/ab{cde,cfe}*'],
|
||||
[
|
||||
'a/b/c',
|
||||
'a/c',
|
||||
'a/c/d/c',
|
||||
...(common.isWindows ? [] : ['a/symlink/a/b/c']),
|
||||
],
|
||||
],
|
||||
[
|
||||
`${absDir}/*`,
|
||||
[`${absDir}/asdf`, `${absDir}/ba*`],
|
||||
[`${absDir}/foo`, `${absDir}/quux`, `${absDir}/qwer`, `${absDir}/rewq`],
|
||||
],
|
||||
[
|
||||
`${absDir}/*`,
|
||||
[`${absDir}/asdf`, `**/ba*`],
|
||||
[
|
||||
`${absDir}/bar`,
|
||||
`${absDir}/baz`,
|
||||
`${absDir}/foo`,
|
||||
`${absDir}/quux`,
|
||||
`${absDir}/qwer`,
|
||||
`${absDir}/rewq`,
|
||||
],
|
||||
],
|
||||
[
|
||||
[`${absDir}/*`, 'a/**/[cg]'],
|
||||
[`${absDir}/*{a,q}*`, './a/*{c,b}*/*'],
|
||||
[`${absDir}/foo`, 'a/c', ...(common.isWindows ? [] : ['a/symlink/a/b/c'])],
|
||||
],
|
||||
];
|
||||
|
||||
describe('globSync - exclude', function() {
|
||||
for (const [pattern, exclude] of Object.entries(patterns).map(([k, v]) => [k, v.filter(Boolean)])) {
|
||||
test(`${pattern} - exclude: ${exclude}`, () => {
|
||||
const actual = globSync(pattern, { cwd: fixtureDir, exclude }).sort();
|
||||
assert.strictEqual(actual.length, 0);
|
||||
});
|
||||
}
|
||||
for (const [pattern, exclude, expected] of pattern2) {
|
||||
test(`${pattern} - exclude: ${exclude}`, () => {
|
||||
const actual = globSync(pattern, { cwd: fixtureDir, exclude }).sort();
|
||||
const normalized = expected.filter(Boolean).map((item) => item.replaceAll('/', sep)).sort();
|
||||
assert.deepStrictEqual(actual, normalized);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('glob - exclude', function() {
|
||||
const promisified = promisify(glob);
|
||||
for (const [pattern, exclude] of Object.entries(patterns).map(([k, v]) => [k, v.filter(Boolean)])) {
|
||||
test(`${pattern} - exclude: ${exclude}`, async () => {
|
||||
const actual = (await promisified(pattern, { cwd: fixtureDir, exclude })).sort();
|
||||
assert.strictEqual(actual.length, 0);
|
||||
});
|
||||
}
|
||||
for (const [pattern, exclude, expected] of pattern2) {
|
||||
test(`${pattern} - exclude: ${exclude}`, async () => {
|
||||
const actual = (await promisified(pattern, { cwd: fixtureDir, exclude })).sort();
|
||||
const normalized = expected.filter(Boolean).map((item) => item.replaceAll('/', sep)).sort();
|
||||
assert.deepStrictEqual(actual, normalized);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('fsPromises glob - exclude', function() {
|
||||
for (const [pattern, exclude] of Object.entries(patterns).map(([k, v]) => [k, v.filter(Boolean)])) {
|
||||
test(`${pattern} - exclude: ${exclude}`, async () => {
|
||||
const actual = [];
|
||||
for await (const item of asyncGlob(pattern, { cwd: fixtureDir, exclude })) actual.push(item);
|
||||
actual.sort();
|
||||
assert.strictEqual(actual.length, 0);
|
||||
});
|
||||
}
|
||||
for (const [pattern, exclude, expected] of pattern2) {
|
||||
test(`${pattern} - exclude: ${exclude}`, async () => {
|
||||
const actual = [];
|
||||
for await (const item of asyncGlob(pattern, { cwd: fixtureDir, exclude })) actual.push(item);
|
||||
const normalized = expected.filter(Boolean).map((item) => item.replaceAll('/', sep)).sort();
|
||||
assert.deepStrictEqual(actual.sort(), normalized);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue