Skip to content

Commit 1756fcc

Browse files
committed
add windowsPathsNoEscape option
Fix: #468 PR-URL: #470 Credit: @isaacs Close: #470 Reviewed-by: @isaacs
1 parent af57da2 commit 1756fcc

File tree

5 files changed

+148
-4
lines changed

5 files changed

+148
-4
lines changed

README.md

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -210,10 +210,26 @@ parallel glob operations will be sped up by sharing information about
210210
the filesystem.
211211

212212
* `cwd` The current working directory in which to search. Defaults
213-
to `process.cwd()`.
213+
to `process.cwd()`. This option is always coerced to use
214+
forward-slashes as a path separator, because it is not tested
215+
as a glob pattern, so there is no need to escape anything.
214216
* `root` The place where patterns starting with `/` will be mounted
215217
onto. Defaults to `path.resolve(options.cwd, "/")` (`/` on Unix
216-
systems, and `C:\` or some such on Windows.)
218+
systems, and `C:\` or some such on Windows.) This option is
219+
always coerced to use forward-slashes as a path separator,
220+
because it is not tested as a glob pattern, so there is no need
221+
to escape anything.
222+
* `windowsPathsNoEscape` Use `\\` as a path separator _only_, and
223+
_never_ as an escape character. If set, all `\\` characters
224+
are replaced with `/` in the pattern. Note that this makes it
225+
**impossible** to match against paths containing literal glob
226+
pattern characters, but allows matching with patterns constructed
227+
using `path.join()` and `path.resolve()` on Windows platforms,
228+
mimicking the (buggy!) behavior of Glob v7 and before on
229+
Windows. Please use with caution, and be mindful of [the caveat
230+
below about Windows paths](#windows). (For legacy reasons,
231+
this is also set if `allowWindowsEscape` is set to the exact
232+
value `false`.)
217233
* `dot` Include `.dot` files in normal matches and `globstar` matches.
218234
Note that an explicit dot in a portion of the pattern will always
219235
match dot files.
@@ -332,6 +348,11 @@ Results from absolute patterns such as `/foo/*` are mounted onto the
332348
root setting using `path.join`. On windows, this will by default result
333349
in `/foo/*` matching `C:\foo\bar.txt`.
334350

351+
To automatically coerce all `\` characters to `/` in pattern
352+
strings, **thus making it impossible to escape literal glob
353+
characters**, you may set the `windowsPathsNoEscape` option to
354+
`true`.
355+
335356
## Race Conditions
336357

337358
Glob searching, by its very nature, is susceptible to race conditions,

changelog.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 8.1
2+
3+
- Add `windowsPathsNoEscape` option
4+
15
## 8.0
26

37
- Only support node v12 and higher

common.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ function setopts (self, pattern, options) {
5757
pattern = "**/" + pattern
5858
}
5959

60+
self.windowsPathsNoEscape = !!options.windowsPathsNoEscape ||
61+
options.allowWindowsEscape === false
62+
if (self.windowsPathsNoEscape) {
63+
pattern = pattern.replace(/\\/g, '/')
64+
}
65+
6066
self.silent = !!options.silent
6167
self.pattern = pattern
6268
self.strict = options.strict !== false
@@ -112,8 +118,6 @@ function setopts (self, pattern, options) {
112118
// Note that they are not supported in Glob itself anyway.
113119
options.nonegate = true
114120
options.nocomment = true
115-
// always treat \ in patterns as escapes, not path separators
116-
options.allowWindowsEscape = true
117121

118122
self.minimatch = new Minimatch(pattern, options)
119123
self.options = self.minimatch.options

test/windows-paths-fs.js

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// test that escape chars are handled properly according to configs
2+
// when found in patterns and paths containing glob magic.
3+
4+
const t = require('tap')
5+
const dir = t.testdir({
6+
// treat escapes as path separators
7+
a: {
8+
'[x': {
9+
']b': {
10+
y: '',
11+
},
12+
},
13+
},
14+
// escape parent dir name only, not filename
15+
'a[x]b': {
16+
y: '',
17+
},
18+
// no path separators, all escaped
19+
'a[x]by': '',
20+
})
21+
22+
const glob = require('../')
23+
t.test('treat backslash as escape', async t => {
24+
const cases = {
25+
'a[x]b/y': [],
26+
'a\\[x\\]b/y': ['a[x]b/y'],
27+
'a\\[x\\]b\\y': ['a[x]by'],
28+
}
29+
for (const [pattern, expect] of Object.entries(cases)) {
30+
t.test(pattern, t => {
31+
const s = glob.sync(pattern, { cwd: dir })
32+
.map(s => s.replace(/\\/g, '/'))
33+
t.strictSame(s, expect, 'sync')
34+
glob(pattern, {cwd: dir}, (er, s) => {
35+
if (er) {
36+
throw er
37+
}
38+
s = s.map(s => s.replace(/\\/g, '/'))
39+
t.strictSame(s, expect, 'async')
40+
t.end()
41+
})
42+
})
43+
}
44+
})
45+
46+
t.test('treat backslash as separator', async t => {
47+
Object.defineProperty(process, 'platform', {
48+
value: 'win32'
49+
})
50+
const cases = {
51+
'a[x]b/y': [],
52+
'a\\[x\\]b/y': ['a/[x/]b/y'],
53+
'a\\[x\\]b\\y': ['a/[x/]b/y'],
54+
}
55+
for (const [pattern, expect] of Object.entries(cases)) {
56+
t.test(pattern, t => {
57+
const s = glob.sync(pattern, { cwd: dir, windowsPathsNoEscape: true })
58+
.map(s => s.replace(/\\/g, '/'))
59+
t.strictSame(s, expect, 'sync')
60+
glob(pattern, {cwd: dir, windowsPathsNoEscape: true}, (er, s) => {
61+
if (er) {
62+
throw er
63+
}
64+
s = s.map(s => s.replace(/\\/g, '/'))
65+
t.strictSame(s, expect, 'async')
66+
t.end()
67+
})
68+
})
69+
}
70+
})

test/windows-paths-no-escape.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
const t = require('tap')
2+
const g = require('../')
3+
4+
const platforms = ['win32', 'posix']
5+
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')
6+
for (const p of platforms) {
7+
t.test(p, t => {
8+
Object.defineProperty(process, 'platform', {
9+
value: p,
10+
enumerable: true,
11+
configurable: true,
12+
writable: true,
13+
})
14+
t.equal(process.platform, p, 'gut check: actually set platform')
15+
const pattern = '/a/b/c/x\\[a-b\\]y\\*'
16+
const def = new g.Glob(pattern, { noprocess: true })
17+
const winpath = new g.Glob(pattern, {
18+
windowsPathsNoEscape: true,
19+
noprocess: true,
20+
})
21+
const winpathLegacy = new g.Glob(pattern, {
22+
allowWindowsEscape: false,
23+
noprocess: true,
24+
})
25+
const nowinpath = new g.Glob(pattern, {
26+
windowsPathsNoEscape: false,
27+
noprocess: true,
28+
})
29+
30+
t.strictSame([
31+
def.pattern,
32+
nowinpath.pattern,
33+
winpath.pattern,
34+
winpathLegacy.pattern,
35+
], [
36+
'/a/b/c/x\\[a-b\\]y\\*',
37+
'/a/b/c/x\\[a-b\\]y\\*',
38+
'/a/b/c/x/[a-b/]y/*',
39+
'/a/b/c/x/[a-b/]y/*',
40+
])
41+
t.end()
42+
})
43+
}
44+
45+
Object.defineProperty(process, 'platform', originalPlatform)

0 commit comments

Comments
 (0)