Skip to content

Commit 0512522

Browse files
committed
feat: add literalBrackets option to handle square brackets in file paths
- Add literalBrackets option to automatically escape square brackets - Helps with Node.js test runner compatibility for files in [param] folders - Maintains backward compatibility with existing glob behavior - Addresses issue #612 where test files in bracketed folders aren't found Closes #612
1 parent 59bf9ca commit 0512522

File tree

2 files changed

+91
-0
lines changed

2 files changed

+91
-0
lines changed

src/glob.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,18 @@ export interface GlobOptions {
127127
*/
128128
magicalBraces?: boolean
129129

130+
/**
131+
* Treat square brackets `[` and `]` literally instead of as character
132+
* classes. When set to true, patterns containing literal square brackets
133+
* in filenames will be automatically escaped.
134+
*
135+
* For example, with `literalBrackets: true`, the pattern
136+
* `'src/app/api/[id]/route.js'` will match the literal folder named `[id]`
137+
* instead of treating `[id]` as a character class matching any single
138+
* character 'i' or 'd'.
139+
*/
140+
literalBrackets?: boolean
141+
130142
/**
131143
* Add a `/` character to directory matches. Note that this requires
132144
* additional stat calls in some cases.
@@ -382,6 +394,7 @@ export class Glob<Opts extends GlobOptions> implements GlobOptions {
382394
follow: boolean
383395
ignore?: string | string[] | IgnoreLike
384396
magicalBraces: boolean
397+
literalBrackets: boolean
385398
mark?: boolean
386399
matchBase: boolean
387400
maxDepth: number
@@ -441,6 +454,7 @@ export class Glob<Opts extends GlobOptions> implements GlobOptions {
441454
this.cwd = opts.cwd || ''
442455
this.root = opts.root
443456
this.magicalBraces = !!opts.magicalBraces
457+
this.literalBrackets = !!opts.literalBrackets
444458
this.nobrace = !!opts.nobrace
445459
this.noext = !!opts.noext
446460
this.realpath = !!opts.realpath
@@ -471,6 +485,10 @@ export class Glob<Opts extends GlobOptions> implements GlobOptions {
471485
pattern = pattern.map(p => p.replace(/\\/g, '/'))
472486
}
473487

488+
if (this.literalBrackets) {
489+
pattern = pattern.map(p => p.replace(/\[/g, '\\[').replace(/\]/g, '\\]'))
490+
}
491+
474492
if (this.matchBase) {
475493
if (opts.noglobstar) {
476494
throw new TypeError('base matching requires globstar')

test/square-brackets.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import t from 'tap'
2+
import { glob } from '../dist/esm/index.js'
3+
4+
t.test('square brackets in folder names', async t => {
5+
// Set up test files in directories with square brackets
6+
const cwd = t.testdir({
7+
'app': {
8+
'api': {
9+
'[id]': {
10+
'route.spec.js': 'export const test = true;'
11+
},
12+
'[slug]': {
13+
'page.spec.js': 'export const test = true;'
14+
},
15+
'normal': {
16+
'file.spec.js': 'export const test = true;'
17+
}
18+
}
19+
}
20+
})
21+
22+
t.test('escaped brackets should match literal brackets in folders', async t => {
23+
const results = await glob('app/api/\\[id\\]/*.spec.js', { cwd })
24+
t.equal(results.length, 1)
25+
t.match(results[0], /\[id\]\/route\.spec\.js$/)
26+
})
27+
28+
t.test('unescaped brackets should not match literal bracket folders', async t => {
29+
const results = await glob('app/api/[id]/*.spec.js', { cwd })
30+
t.equal(results.length, 0)
31+
})
32+
33+
t.test('wildcard should match all directories including bracketed ones', async t => {
34+
const results = await glob('app/api/*/*.spec.js', { cwd })
35+
t.equal(results.length, 3) // [id], [slug], and normal
36+
})
37+
38+
t.test('globstar should find all spec files', async t => {
39+
const results = await glob('**/*.spec.js', { cwd })
40+
t.equal(results.length, 3)
41+
})
42+
43+
t.test('literalBrackets option should auto-escape brackets', async t => {
44+
const results = await glob('app/api/[id]/*.spec.js', { cwd, literalBrackets: true })
45+
t.equal(results.length, 1)
46+
t.match(results[0], /\[id\]\/route\.spec\.js$/)
47+
})
48+
49+
t.test('literalBrackets should work with multiple patterns', async t => {
50+
const patterns = ['app/api/[id]/*.spec.js', 'app/api/[slug]/*.spec.js']
51+
const results = await glob(patterns, { cwd, literalBrackets: true })
52+
t.equal(results.length, 2)
53+
t.match(results[0], /\[id\]\/route\.spec\.js$/)
54+
t.match(results[1], /\[slug\]\/page\.spec\.js$/)
55+
})
56+
57+
t.test('literalBrackets should not affect normal brackets used as character classes', async t => {
58+
// Create files that would match character classes
59+
const testCwd = t.testdir({
60+
'i': { 'test.js': 'content' },
61+
'd': { 'test.js': 'content' },
62+
'normal': { 'test.js': 'content' }
63+
})
64+
65+
// Without literalBrackets, [id] should match directories named 'i' or 'd'
66+
const results = await glob('[id]/test.js', { cwd: testCwd })
67+
t.equal(results.length, 2) // Should match 'i' and 'd' directories
68+
69+
// With literalBrackets, [id] should be treated literally and match nothing
70+
const literalResults = await glob('[id]/test.js', { cwd: testCwd, literalBrackets: true })
71+
t.equal(literalResults.length, 0) // Should not match any directory
72+
})
73+
})

0 commit comments

Comments
 (0)