From 1756fccfe713a2d83bfe6ac6e4d93e2d5dc0e69e Mon Sep 17 00:00:00 2001 From: isaacs Date: Thu, 12 May 2022 12:04:38 -0700 Subject: [PATCH] add windowsPathsNoEscape option Fix: #468 PR-URL: https://github.com/isaacs/node-glob/pull/470 Credit: @isaacs Close: #470 Reviewed-by: @isaacs --- README.md | 25 +++++++++++- changelog.md | 4 ++ common.js | 8 +++- test/windows-paths-fs.js | 70 +++++++++++++++++++++++++++++++++ test/windows-paths-no-escape.js | 45 +++++++++++++++++++++ 5 files changed, 148 insertions(+), 4 deletions(-) create mode 100644 test/windows-paths-fs.js create mode 100644 test/windows-paths-no-escape.js diff --git a/README.md b/README.md index 83f0c83a..62939110 100644 --- a/README.md +++ b/README.md @@ -210,10 +210,26 @@ parallel glob operations will be sped up by sharing information about the filesystem. * `cwd` The current working directory in which to search. Defaults - to `process.cwd()`. + to `process.cwd()`. This option is always coerced to use + forward-slashes as a path separator, because it is not tested + as a glob pattern, so there is no need to escape anything. * `root` The place where patterns starting with `/` will be mounted onto. Defaults to `path.resolve(options.cwd, "/")` (`/` on Unix - systems, and `C:\` or some such on Windows.) + systems, and `C:\` or some such on Windows.) This option is + always coerced to use forward-slashes as a path separator, + because it is not tested as a glob pattern, so there is no need + to escape anything. +* `windowsPathsNoEscape` Use `\\` as a path separator _only_, and + _never_ as an escape character. If set, all `\\` characters + are replaced with `/` in the pattern. Note that this makes it + **impossible** to match against paths containing literal glob + pattern characters, but allows matching with patterns constructed + using `path.join()` and `path.resolve()` on Windows platforms, + mimicking the (buggy!) behavior of Glob v7 and before on + Windows. Please use with caution, and be mindful of [the caveat + below about Windows paths](#windows). (For legacy reasons, + this is also set if `allowWindowsEscape` is set to the exact + value `false`.) * `dot` Include `.dot` files in normal matches and `globstar` matches. Note that an explicit dot in a portion of the pattern will always match dot files. @@ -332,6 +348,11 @@ Results from absolute patterns such as `/foo/*` are mounted onto the root setting using `path.join`. On windows, this will by default result in `/foo/*` matching `C:\foo\bar.txt`. +To automatically coerce all `\` characters to `/` in pattern +strings, **thus making it impossible to escape literal glob +characters**, you may set the `windowsPathsNoEscape` option to +`true`. + ## Race Conditions Glob searching, by its very nature, is susceptible to race conditions, diff --git a/changelog.md b/changelog.md index e3dd7354..80e2e5b2 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,7 @@ +## 8.1 + +- Add `windowsPathsNoEscape` option + ## 8.0 - Only support node v12 and higher diff --git a/common.js b/common.js index e094f750..61a4452f 100644 --- a/common.js +++ b/common.js @@ -57,6 +57,12 @@ function setopts (self, pattern, options) { pattern = "**/" + pattern } + self.windowsPathsNoEscape = !!options.windowsPathsNoEscape || + options.allowWindowsEscape === false + if (self.windowsPathsNoEscape) { + pattern = pattern.replace(/\\/g, '/') + } + self.silent = !!options.silent self.pattern = pattern self.strict = options.strict !== false @@ -112,8 +118,6 @@ function setopts (self, pattern, options) { // Note that they are not supported in Glob itself anyway. options.nonegate = true options.nocomment = true - // always treat \ in patterns as escapes, not path separators - options.allowWindowsEscape = true self.minimatch = new Minimatch(pattern, options) self.options = self.minimatch.options diff --git a/test/windows-paths-fs.js b/test/windows-paths-fs.js new file mode 100644 index 00000000..85ff9ab8 --- /dev/null +++ b/test/windows-paths-fs.js @@ -0,0 +1,70 @@ +// test that escape chars are handled properly according to configs +// when found in patterns and paths containing glob magic. + +const t = require('tap') +const dir = t.testdir({ + // treat escapes as path separators + a: { + '[x': { + ']b': { + y: '', + }, + }, + }, + // escape parent dir name only, not filename + 'a[x]b': { + y: '', + }, + // no path separators, all escaped + 'a[x]by': '', +}) + +const glob = require('../') +t.test('treat backslash as escape', async t => { + const cases = { + 'a[x]b/y': [], + 'a\\[x\\]b/y': ['a[x]b/y'], + 'a\\[x\\]b\\y': ['a[x]by'], + } + for (const [pattern, expect] of Object.entries(cases)) { + t.test(pattern, t => { + const s = glob.sync(pattern, { cwd: dir }) + .map(s => s.replace(/\\/g, '/')) + t.strictSame(s, expect, 'sync') + glob(pattern, {cwd: dir}, (er, s) => { + if (er) { + throw er + } + s = s.map(s => s.replace(/\\/g, '/')) + t.strictSame(s, expect, 'async') + t.end() + }) + }) + } +}) + +t.test('treat backslash as separator', async t => { + Object.defineProperty(process, 'platform', { + value: 'win32' + }) + const cases = { + 'a[x]b/y': [], + 'a\\[x\\]b/y': ['a/[x/]b/y'], + 'a\\[x\\]b\\y': ['a/[x/]b/y'], + } + for (const [pattern, expect] of Object.entries(cases)) { + t.test(pattern, t => { + const s = glob.sync(pattern, { cwd: dir, windowsPathsNoEscape: true }) + .map(s => s.replace(/\\/g, '/')) + t.strictSame(s, expect, 'sync') + glob(pattern, {cwd: dir, windowsPathsNoEscape: true}, (er, s) => { + if (er) { + throw er + } + s = s.map(s => s.replace(/\\/g, '/')) + t.strictSame(s, expect, 'async') + t.end() + }) + }) + } +}) diff --git a/test/windows-paths-no-escape.js b/test/windows-paths-no-escape.js new file mode 100644 index 00000000..6a1bf1d9 --- /dev/null +++ b/test/windows-paths-no-escape.js @@ -0,0 +1,45 @@ +const t = require('tap') +const g = require('../') + +const platforms = ['win32', 'posix'] +const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform') +for (const p of platforms) { + t.test(p, t => { + Object.defineProperty(process, 'platform', { + value: p, + enumerable: true, + configurable: true, + writable: true, + }) + t.equal(process.platform, p, 'gut check: actually set platform') + const pattern = '/a/b/c/x\\[a-b\\]y\\*' + const def = new g.Glob(pattern, { noprocess: true }) + const winpath = new g.Glob(pattern, { + windowsPathsNoEscape: true, + noprocess: true, + }) + const winpathLegacy = new g.Glob(pattern, { + allowWindowsEscape: false, + noprocess: true, + }) + const nowinpath = new g.Glob(pattern, { + windowsPathsNoEscape: false, + noprocess: true, + }) + + t.strictSame([ + def.pattern, + nowinpath.pattern, + winpath.pattern, + winpathLegacy.pattern, + ], [ + '/a/b/c/x\\[a-b\\]y\\*', + '/a/b/c/x\\[a-b\\]y\\*', + '/a/b/c/x/[a-b/]y/*', + '/a/b/c/x/[a-b/]y/*', + ]) + t.end() + }) +} + +Object.defineProperty(process, 'platform', originalPlatform)