Skip to content

path: add exclude option to matchesGlob method #59061

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 28 additions & 2 deletions doc/api/path.md
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ path.format({
// Returns: 'C:\\path\\dir\\file.txt'
```

## `path.matchesGlob(path, pattern)`
## `path.matchesGlob(path, pattern[, options])`

<!-- YAML
added:
Expand All @@ -295,17 +295,43 @@ added:

* `path` {string} The path to glob-match against.
* `pattern` {string} The glob to check the path against.
* `options` {Object}
* `exclude` {string|string\[]|Function} Patterns to exclude from matching.
* Returns: {boolean} Whether or not the `path` matched the `pattern`.

The `path.matchesGlob()` method determines if `path` matches the `pattern`.
The `path.matchesGlob()` method determines if `path` matches the `pattern`,
optionally excluding paths that match the `exclude` patterns.

For example:

```js
path.matchesGlob('/foo/bar', '/foo/*'); // true
path.matchesGlob('/foo/bar*', 'foo/bird'); // false

// Using exclude with string
path.matchesGlob('/foo/bar', '/foo/*', { exclude: '/foo/bar' }); // false
path.matchesGlob('/foo/baz', '/foo/*', { exclude: '/foo/bar' }); // true

// Using exclude with array
path.matchesGlob('/foo/bar', '/foo/*', {
exclude: ['/foo/bar', '/foo/baz'],
}); // false

// Using exclude with function
path.matchesGlob('/foo/bar', '/foo/*', {
exclude: (path) => path.includes('bar'),
}); // false
```

The `exclude` option can be:

* A string pattern that will exclude matching paths
* An array of string patterns
* A function that receives the path and returns `true` to exclude it

Unlike `fs.glob()`, the `exclude` patterns in `path.matchesGlob()` are
matched against the input path directly, without path resolution.

A [`TypeError`][] is thrown if `path` or `pattern` are not strings.

## `path.isAbsolute(path)`
Expand Down
58 changes: 52 additions & 6 deletions lib/internal/fs/glob.js
Original file line number Diff line number Diff line change
Expand Up @@ -761,22 +761,68 @@ class Glob {
* Check if a path matches a glob pattern
* @param {string} path the path to check
* @param {string} pattern the glob pattern to match
* @param {boolean} windows whether the path is on a Windows system, defaults to `isWindows`
* @param {boolean|object} windowsOrOptions whether the path is on a Windows system, or options object
* @returns {boolean}
*/
function matchGlobPattern(path, pattern, windows = isWindows) {
function matchGlobPattern(path, pattern, windowsOrOptions = isWindows) {
validateString(path, 'path');
validateString(pattern, 'pattern');
return lazyMinimatch().minimatch(path, pattern, {
kEmptyObject,
nocase: isMacOS || isWindows,

let windows = isWindows;
let options = kEmptyObject;

// Handle backward compatibility: third param can be boolean or options object
if (typeof windowsOrOptions === 'boolean') {
windows = windowsOrOptions;
} else if (windowsOrOptions != null) {
validateObject(windowsOrOptions, 'options');
options = windowsOrOptions;
windows = options.windows ?? isWindows;
}

const minimatchOptions = {
__proto__: null,
nocase: windows || isMacOS,
windowsPathsNoEscape: true,
nonegate: true,
nocomment: true,
optimizationLevel: 2,
platform: windows ? 'win32' : 'posix',
nocaseMagicOnly: true,
});
};

// Check if path matches the main pattern
const matches = lazyMinimatch().minimatch(path, pattern, minimatchOptions);

if (!matches) {
return false;
}

// Handle exclude patterns
const { exclude } = options;
if (!exclude) {
return matches;
}

if (typeof exclude === 'function') {
return !exclude(path);
}

if (typeof exclude === 'string') {
const excludeMatch = lazyMinimatch().minimatch(path, exclude, minimatchOptions);
return !excludeMatch;
}

if (ArrayIsArray(exclude)) {
for (const excludePattern of exclude) {
validateString(excludePattern, 'exclude pattern');
if (lazyMinimatch().minimatch(path, excludePattern, minimatchOptions)) {
return false;
}
}
}

return matches;
}

module.exports = {
Expand Down
14 changes: 12 additions & 2 deletions lib/path.js
Original file line number Diff line number Diff line change
Expand Up @@ -1149,7 +1149,12 @@ const win32 = {
return ret;
},

matchesGlob(path, pattern) {
matchesGlob(path, pattern, options) {
emitExperimentalWarning('glob');
if (options != null) {
validateObject(options, 'options');
return lazyMatchGlobPattern()(path, pattern, { windows: true, ...options });
}
return lazyMatchGlobPattern()(path, pattern, true);
},

Expand Down Expand Up @@ -1631,7 +1636,12 @@ const posix = {
return ret;
},

matchesGlob(path, pattern) {
matchesGlob(path, pattern, options) {
emitExperimentalWarning('glob');
if (options != null) {
validateObject(options, 'options');
return lazyMatchGlobPattern()(path, pattern, { windows: false, ...options });
}
return lazyMatchGlobPattern()(path, pattern, false);
},

Expand Down
38 changes: 38 additions & 0 deletions test/parallel/test-path-glob.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,41 @@ for (const [platform, platformGlobs] of Object.entries(globs)) {
// Test for non-string input
assert.throws(() => path.matchesGlob(123, 'foo/bar/baz'), /.*must be of type string.*/);
assert.throws(() => path.matchesGlob('foo/bar/baz', 123), /.*must be of type string.*/);

// Test exclude functionality
const excludeTests = {
win32: [
// Basic exclude with string
['foo\\bar\\baz', 'foo\\**\\*', { exclude: 'foo\\bar\\*' }, false],
['foo\\bar\\baz', 'foo\\**\\*', { exclude: 'foo\\other\\*' }, true],

// Exclude with array
['foo\\bar\\baz', 'foo\\**\\*', { exclude: ['foo\\bar\\*', 'foo\\other\\*'] }, false],
['foo\\test\\file', 'foo\\**\\*', { exclude: ['foo\\bar\\*', 'foo\\other\\*'] }, true],

// Exclude with function
['foo\\bar\\baz', 'foo\\**\\*', { exclude: (path) => path.includes('bar') }, false],
['foo\\test\\file', 'foo\\**\\*', { exclude: (path) => path.includes('bar') }, true],
],
posix: [
// Basic exclude with string
['foo/bar/baz', 'foo/**/*', { exclude: 'foo/bar/*' }, false],
['foo/bar/baz', 'foo/**/*', { exclude: 'foo/other/*' }, true],

// Exclude with array
['foo/bar/baz', 'foo/**/*', { exclude: ['foo/bar/*', 'foo/other/*'] }, false],
['foo/test/file', 'foo/**/*', { exclude: ['foo/bar/*', 'foo/other/*'] }, true],

// Exclude with function
['foo/bar/baz', 'foo/**/*', { exclude: (path) => path.includes('bar') }, false],
['foo/test/file', 'foo/**/*', { exclude: (path) => path.includes('bar') }, true],
],
};

for (const [platform, platformTests] of Object.entries(excludeTests)) {
for (const [pathStr, pattern, options, expected] of platformTests) {
const actual = path[platform].matchesGlob(pathStr, pattern, options);
assert.strictEqual(actual, expected,
`Expected ${pathStr} to ${expected ? '' : 'not '}match ${pattern} with exclude on ${platform}`);
}
}