From a12c8ca5b4bdbb7aac703c7a131d99e25c8bffb8 Mon Sep 17 00:00:00 2001 From: Xavier Stouder Date: Sat, 3 Jan 2026 23:31:35 +0100 Subject: [PATCH 1/8] path: add escapeGlob and unescapeGlob Expose minimatch escaping and unescaping capabilities on path module. Fixes: https://github.com/nodejs/node/issues/61258 --- lib/internal/fs/glob.js | 26 ++++++++++++++++++++++++++ lib/path.js | 20 ++++++++++++++++++-- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/lib/internal/fs/glob.js b/lib/internal/fs/glob.js index 1bfa39150e5196..865ea38ce8fd91 100644 --- a/lib/internal/fs/glob.js +++ b/lib/internal/fs/glob.js @@ -794,8 +794,34 @@ function matchGlobPattern(path, pattern, windows = isWindows) { }); } +/** + * Escape special glob characters in a pattern + * @param pattern the glob pattern to escape + * @returns {string} the escaped glob pattern + */ +function escapeGlobPattern(pattern) { + validateString(pattern, 'pattern'); + return lazyMinimatch().escape(pattern, { + windowsPathsNoEscape: true + }) +} + +/** + * Restore special glob characters in a pattern + * @param pattern the glob pattern to unescape + * @returns {string} the unescaped glob pattern + */ +function unescapeGlobPattern(pattern) { + validateString(pattern, 'pattern'); + return lazyMinimatch().unescape(pattern, { + windowsPathsNoEscape: true + }) +} + module.exports = { __proto__: null, Glob, + escapeGlobPattern, matchGlobPattern, + unescapeGlobPattern, }; diff --git a/lib/path.js b/lib/path.js index 63b037cddfb986..dbd1e84133797a 100644 --- a/lib/path.js +++ b/lib/path.js @@ -60,7 +60,7 @@ const { getLazy, } = require('internal/util'); -const lazyMatchGlobPattern = getLazy(() => require('internal/fs/glob').matchGlobPattern); +const lazyGlobPattern = getLazy(() => require('internal/fs/glob')); function isPathSeparator(code) { return code === CHAR_FORWARD_SLASH || code === CHAR_BACKWARD_SLASH; @@ -1212,7 +1212,15 @@ const win32 = { }, matchesGlob(path, pattern) { - return lazyMatchGlobPattern()(path, pattern, true); + return lazyGlobPattern().matchGlobPattern(path, pattern, true); + }, + + escapeGlob(pattern) { + return lazyGlobPattern().escapeGlobPattern(pattern); + }, + + unescapeGlob(pattern) { + return lazyGlobPattern().unescapeGlobPattern(pattern); }, sep: '\\', @@ -1697,6 +1705,14 @@ const posix = { return lazyMatchGlobPattern()(path, pattern, false); }, + escapeGlob(pattern) { + return lazyGlobPattern().escapeGlobPattern(pattern); + }, + + unescapeGlob(pattern) { + return lazyGlobPattern().unescapeGlobPattern(pattern); + }, + sep: '/', delimiter: ':', win32: null, From df8a836237a08c50660bdf0b4fef88d164490c84 Mon Sep 17 00:00:00 2001 From: Xavier Stouder Date: Sat, 3 Jan 2026 23:32:33 +0100 Subject: [PATCH 2/8] reorder --- doc/api/path.md | 59 ++++++++++++++++++++++++------------------------- 1 file changed, 29 insertions(+), 30 deletions(-) diff --git a/doc/api/path.md b/doc/api/path.md index e5d6fd7ef68304..1c009a62524648 100644 --- a/doc/api/path.md +++ b/doc/api/path.md @@ -282,36 +282,6 @@ path.format({ }); // Returns: 'C:\\path\\dir\\file.txt' ``` - -## `path.matchesGlob(path, pattern)` - - - -* `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)` + +* `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.normalize(path)` + +* `pattern` {string} +* Returns: {string} + +The `path.escapeGlob()` method escapes glob characters in a `pattern`. + +```js +path.escapeGlob('foo*bar'); +// Returns: 'foo[*]bar' +``` + ## `path.extname(path)` + +* `pattern` {string} +* Returns: {string} + +The `path.unescapeGlob()` method unescapes the given glob pattern. + +```js +path.unescapeGlob('foo[*]bar'); +// Returns: 'foo*bar' +``` + ## `path.win32` * `pattern` {string} @@ -658,7 +658,7 @@ method is non-operational and always returns `path` without modifications. ## `path.unescapeGlob(pattern)` * `pattern` {string} diff --git a/lib/internal/fs/glob.js b/lib/internal/fs/glob.js index 1439cdab5d34b8..43b203fce94de2 100644 --- a/lib/internal/fs/glob.js +++ b/lib/internal/fs/glob.js @@ -796,28 +796,28 @@ function matchGlobPattern(path, pattern, windows = isWindows) { /** * Escape special glob characters in a pattern - * @param pattern the glob pattern to escape + * @param {string} pattern the glob pattern to escape * @returns {string} the escaped glob pattern */ function escapeGlobPattern(pattern) { validateString(pattern, 'pattern'); return lazyMinimatch().escape(pattern, { windowsPathsNoEscape: true, - magicalBraces: true - }) + magicalBraces: true, + }); } /** * Restore special glob characters in a pattern - * @param pattern the glob pattern to unescape + * @param {string} pattern the glob pattern to unescape * @returns {string} the unescaped glob pattern */ function unescapeGlobPattern(pattern) { validateString(pattern, 'pattern'); return lazyMinimatch().unescape(pattern, { windowsPathsNoEscape: true, - magicalBraces: true - }) + magicalBraces: true, + }); } module.exports = { diff --git a/test/parallel/test-path-glob-escape.js b/test/parallel/test-path-glob-escape.js deleted file mode 100644 index 4c3a7df8e3161b..00000000000000 --- a/test/parallel/test-path-glob-escape.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -require('../common'); -const assert = require('assert'); -const path = require('path'); - - diff --git a/test/parallel/test-path-glob.js b/test/parallel/test-path-glob.js index 8eb0851f81c821..986446da817a53 100644 --- a/test/parallel/test-path-glob.js +++ b/test/parallel/test-path-glob.js @@ -10,7 +10,8 @@ function testMatch() { ['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\\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' @@ -54,7 +55,7 @@ function testEscaping() { ['file{1,2}.txt', 'file[{]1,2[}].txt'], ['file[0-9]?.txt', 'file[[]0-9[]][?].txt'], ['C:\\Users\\*.txt', 'C:\\Users\\[*].txt'], - ['?[]', '[?][[][]]'] + ['?[]', '[?][[][]]'], ]; for (const [pattern, expected] of samples) { @@ -68,9 +69,9 @@ function testEscaping() { } // Test for non-string input - assert.throws(() => path.escapeGlob(123)); - assert.throws(() => path.unescapeGlob(123)); + assert.throws(() => path.escapeGlob(123), /.*must be of type string.*/); + assert.throws(() => path.unescapeGlob(123), /.*must be of type string.*/); } -testMatch() -testEscaping() \ No newline at end of file +testMatch(); +testEscaping(); From d143cbcae8ec11f674c07d37854bcd9fe7169ba2 Mon Sep 17 00:00:00 2001 From: Xavier Stouder Date: Sun, 4 Jan 2026 00:07:00 +0100 Subject: [PATCH 7/8] lint --- doc/api/path.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/api/path.md b/doc/api/path.md index 11738f2d2bbd44..5b433b88a9d7bd 100644 --- a/doc/api/path.md +++ b/doc/api/path.md @@ -298,6 +298,7 @@ path.format({ }); // Returns: 'C:\\path\\dir\\file.txt' ``` + ## `path.isAbsolute(path)`