Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -435,11 +435,11 @@ share the previously loaded cache.
is used as the starting point for absolute patterns that start
with `/`, (but not drive letters or UNC paths on Windows).

To start absolute and non-absolute patterns in the same path,
you can use `{root:''}`. However, be aware that on Windows
systems, a pattern like `x:/*` or `//host/share/*` will
_always_ start in the `x:/` or `//host/share` directory,
regardless of the `root` setting.
To start absolute and non-absolute patterns in the same path,
you can use `{root:''}`. However, be aware that on Windows
systems, a pattern like `x:/*` or `//host/share/*` will
_always_ start in the `x:/` or `//host/share` directory,
regardless of the `root` setting.

> [!NOTE] This _doesn't_ necessarily limit the walk to the
> `root` directory, and doesn't affect the cwd starting point
Expand Down Expand Up @@ -477,6 +477,15 @@ share the previously loaded cache.
Only has effect on the {@link hasMagic} function, no effect on
glob pattern matching itself.

- `literalBrackets` Treat square brackets `[` and `]` literally instead
of as character classes. When set to true, patterns containing literal
square brackets in filenames will be automatically escaped.

For example, with `literalBrackets: true`, the pattern
`'src/app/api/[id]/route.js'` will match the literal folder named `[id]`
instead of treating `[id]` as a character class matching any single
character 'i' or 'd'.

- `dotRelative` Prepend all relative path strings with `./` (or
`.\` on Windows).

Expand Down Expand Up @@ -664,7 +673,6 @@ share the previously loaded cache.
> already be added before its ancestor, if multiple or braced
> patterns are used.


## Glob Primer

Much more information about glob pattern expansion can be found
Expand Down
18 changes: 18 additions & 0 deletions src/glob.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,18 @@ export interface GlobOptions {
*/
magicalBraces?: boolean

/**
* Treat square brackets `[` and `]` literally instead of as character
* classes. When set to true, patterns containing literal square brackets
* in filenames will be automatically escaped.
*
* For example, with `literalBrackets: true`, the pattern
* `'src/app/api/[id]/route.js'` will match the literal folder named `[id]`
* instead of treating `[id]` as a character class matching any single
* character 'i' or 'd'.
*/
literalBrackets?: boolean

/**
* Add a `/` character to directory matches. Note that this requires
* additional stat calls in some cases.
Expand Down Expand Up @@ -382,6 +394,7 @@ export class Glob<Opts extends GlobOptions> implements GlobOptions {
follow: boolean
ignore?: string | string[] | IgnoreLike
magicalBraces: boolean
literalBrackets: boolean
mark?: boolean
matchBase: boolean
maxDepth: number
Expand Down Expand Up @@ -441,6 +454,7 @@ export class Glob<Opts extends GlobOptions> implements GlobOptions {
this.cwd = opts.cwd || ''
this.root = opts.root
this.magicalBraces = !!opts.magicalBraces
this.literalBrackets = !!opts.literalBrackets
this.nobrace = !!opts.nobrace
this.noext = !!opts.noext
this.realpath = !!opts.realpath
Expand Down Expand Up @@ -471,6 +485,10 @@ export class Glob<Opts extends GlobOptions> implements GlobOptions {
pattern = pattern.map(p => p.replace(/\\/g, '/'))
}

if (this.literalBrackets) {
pattern = pattern.map(p => p.replace(/\[/g, '\\[').replace(/\]/g, '\\]'))
}

if (this.matchBase) {
if (opts.noglobstar) {
throw new TypeError('base matching requires globstar')
Expand Down
73 changes: 73 additions & 0 deletions test/square-brackets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import t from 'tap'
import { glob } from '../dist/esm/index.js'

t.test('square brackets in folder names', async t => {
// Set up test files in directories with square brackets
const cwd = t.testdir({
'app': {
'api': {
'[id]': {
'route.spec.js': 'export const test = true;'
},
'[slug]': {
'page.spec.js': 'export const test = true;'
},
'normal': {
'file.spec.js': 'export const test = true;'
}
}
}
})

t.test('escaped brackets should match literal brackets in folders', async t => {
const results = await glob('app/api/\\[id\\]/*.spec.js', { cwd })
t.equal(results.length, 1)
t.match(results[0], /\[id\]\/route\.spec\.js$/)
})

t.test('unescaped brackets should not match literal bracket folders', async t => {
const results = await glob('app/api/[id]/*.spec.js', { cwd })
t.equal(results.length, 0)
})

t.test('wildcard should match all directories including bracketed ones', async t => {
const results = await glob('app/api/*/*.spec.js', { cwd })
t.equal(results.length, 3) // [id], [slug], and normal
})

t.test('globstar should find all spec files', async t => {
const results = await glob('**/*.spec.js', { cwd })
t.equal(results.length, 3)
})

t.test('literalBrackets option should auto-escape brackets', async t => {
const results = await glob('app/api/[id]/*.spec.js', { cwd, literalBrackets: true })
t.equal(results.length, 1)
t.match(results[0], /\[id\]\/route\.spec\.js$/)
})

t.test('literalBrackets should work with multiple patterns', async t => {
const patterns = ['app/api/[id]/*.spec.js', 'app/api/[slug]/*.spec.js']
const results = await glob(patterns, { cwd, literalBrackets: true })
t.equal(results.length, 2)
t.match(results[0], /\[id\]\/route\.spec\.js$/)
t.match(results[1], /\[slug\]\/page\.spec\.js$/)
})

t.test('literalBrackets should not affect normal brackets used as character classes', async t => {
// Create files that would match character classes
const testCwd = t.testdir({
'i': { 'test.js': 'content' },
'd': { 'test.js': 'content' },
'normal': { 'test.js': 'content' }
})

// Without literalBrackets, [id] should match directories named 'i' or 'd'
const results = await glob('[id]/test.js', { cwd: testCwd })
t.equal(results.length, 2) // Should match 'i' and 'd' directories

// With literalBrackets, [id] should be treated literally and match nothing
const literalResults = await glob('[id]/test.js', { cwd: testCwd, literalBrackets: true })
t.equal(literalResults.length, 0) // Should not match any directory
})
})
Loading