Skip to content

Commit

Permalink
path: add matchesGlob method
Browse files Browse the repository at this point in the history
PR-URL: #52881
Reviewed-By: Moshe Atlow <moshe@atlow.co.il>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
  • Loading branch information
avivkeller authored and marco-ippolito committed Aug 19, 2024
1 parent 62f4f6f commit 57b8b8e
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 0 deletions.
23 changes: 23 additions & 0 deletions doc/api/path.md
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,29 @@ path.format({
// Returns: 'C:\\path\\dir\\file.txt'
```

## `path.matchesGlob(path, pattern)`

<!-- YAML
added: REPLACEME
-->

> Stability: 1 - Experimental
* `path` {string} The path to glob-match against.
* `pattern` {string} The glob to check the path against.
* Returns: {boolean} Whether or not the `path` matched the `pattern`.

The `path.matchesGlob()` method determines if `path` matches the `pattern`.

For example:

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

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

## `path.isAbsolute(path)`

<!-- YAML
Expand Down
32 changes: 32 additions & 0 deletions lib/path.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,15 @@ const {
validateString,
} = require('internal/validators');

const {
getLazy,
emitExperimentalWarning,
} = require('internal/util');

const lazyMinimatch = getLazy(() => require('internal/deps/minimatch/index'));

const platformIsWin32 = (process.platform === 'win32');
const platformIsOSX = (process.platform === 'darwin');

function isPathSeparator(code) {
return code === CHAR_FORWARD_SLASH || code === CHAR_BACKWARD_SLASH;
Expand Down Expand Up @@ -153,6 +161,22 @@ function _format(sep, pathObject) {
return dir === pathObject.root ? `${dir}${base}` : `${dir}${sep}${base}`;
}

function glob(path, pattern, windows) {
emitExperimentalWarning('glob');
validateString(path, 'path');
validateString(pattern, 'pattern');
return lazyMinimatch().minimatch(path, pattern, {
__proto__: null,
nocase: platformIsOSX || platformIsWin32,
windowsPathsNoEscape: true,
nonegate: true,
nocomment: true,
optimizationLevel: 2,
platform: windows ? 'win32' : 'posix',
nocaseMagicOnly: true,
});
}

const win32 = {
/**
* path.resolve([from ...], to)
Expand Down Expand Up @@ -1065,6 +1089,10 @@ const win32 = {
return ret;
},

matchesGlob(path, pattern) {
return glob(path, pattern, true);
},

sep: '\\',
delimiter: ';',
win32: null,
Expand Down Expand Up @@ -1530,6 +1558,10 @@ const posix = {
return ret;
},

matchesGlob(path, pattern) {
return glob(path, pattern, false);
},

sep: '/',
delimiter: ':',
win32: null,
Expand Down
44 changes: 44 additions & 0 deletions test/parallel/test-path-glob.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
'use strict';

require('../common');
const assert = require('assert');
const path = require('path');

const globs = {
win32: [
['foo\\bar\\baz', 'foo\\[bcr]ar\\baz', true], // Matches 'bar' or 'car' in 'foo\\bar'
['foo\\bar\\baz', 'foo\\[!bcr]ar\\baz', false], // Matches anything except 'bar' or 'car' in 'foo\\bar'
['foo\\bar\\baz', 'foo\\[bc-r]ar\\baz', true], // Matches 'bar' or 'car' using range in 'foo\\bar'
['foo\\bar\\baz', 'foo\\*\\!bar\\*\\baz', false], // Matches anything with 'foo' and 'baz' but not 'bar' in between
['foo\\bar1\\baz', 'foo\\bar[0-9]\\baz', true], // Matches 'bar' followed by any digit in 'foo\\bar1'
['foo\\bar5\\baz', 'foo\\bar[0-9]\\baz', true], // Matches 'bar' followed by any digit in 'foo\\bar5'
['foo\\barx\\baz', 'foo\\bar[a-z]\\baz', true], // Matches 'bar' followed by any lowercase letter in 'foo\\barx'
['foo\\bar\\baz\\boo', 'foo\\[bc-r]ar\\baz\\*', true], // Matches 'bar' or 'car' in 'foo\\bar'
['foo\\bar\\baz', 'foo/**', true], // Matches anything in 'foo'
['foo\\bar\\baz', '*', false], // No match
],
posix: [
['foo/bar/baz', 'foo/[bcr]ar/baz', true], // Matches 'bar' or 'car' in 'foo/bar'
['foo/bar/baz', 'foo/[!bcr]ar/baz', false], // Matches anything except 'bar' or 'car' in 'foo/bar'
['foo/bar/baz', 'foo/[bc-r]ar/baz', true], // Matches 'bar' or 'car' using range in 'foo/bar'
['foo/bar/baz', 'foo/*/!bar/*/baz', false], // Matches anything with 'foo' and 'baz' but not 'bar' in between
['foo/bar1/baz', 'foo/bar[0-9]/baz', true], // Matches 'bar' followed by any digit in 'foo/bar1'
['foo/bar5/baz', 'foo/bar[0-9]/baz', true], // Matches 'bar' followed by any digit in 'foo/bar5'
['foo/barx/baz', 'foo/bar[a-z]/baz', true], // Matches 'bar' followed by any lowercase letter in 'foo/barx'
['foo/bar/baz/boo', 'foo/[bc-r]ar/baz/*', true], // Matches 'bar' or 'car' in 'foo/bar'
['foo/bar/baz', 'foo/**', true], // Matches anything in 'foo'
['foo/bar/baz', '*', false], // No match
],
};


for (const [platform, platformGlobs] of Object.entries(globs)) {
for (const [pathStr, glob, expected] of platformGlobs) {
const actual = path[platform].matchesGlob(pathStr, glob);
assert.strictEqual(actual, expected, `Expected ${pathStr} to ` + (expected ? '' : 'not ') + `match ${glob} on ${platform}`);
}
}

// 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.*/);

0 comments on commit 57b8b8e

Please sign in to comment.