Skip to content
Open
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
136 changes: 133 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,135 @@ const path: { [k: string]: { sep: Sep } } = {
}
/* c8 ignore stop */

const isExtglobStart = (c: string) =>
c === '!' || c === '?' || c === '+' || c === '*' || c === '@'

const splitExtglobAlternatives = (p: string) => {
const parts: string[] = []
let part = ''
let extglobDepth = 0
let escaping = false

for (let i = 0; i < p.length; i++) {
const c = p.charAt(i)
if (escaping) {
escaping = false
part += c
continue
}

if (c === '\\') {
escaping = true
part += c
continue
}

if (isExtglobStart(c) && p.charAt(i + 1) === '(') {
extglobDepth++
part += c + '('
i++
continue
}

if (extglobDepth && c === ')') {
extglobDepth--
part += c
continue
}

if (!extglobDepth && c === '|') {
parts.push(part)
part = ''
continue
}

part += c
}

parts.push(part)
return parts
}

const hasSlash = (p: string) => p.includes('/')

const filterExtglobAlternatives = (p: string): string => {
let result = ''
let escaping = false

for (let i = 0; i < p.length; i++) {
const c = p.charAt(i)
if (escaping) {
escaping = false
result += c
continue
}

if (c === '\\') {
escaping = true
result += c
continue
}

if (!isExtglobStart(c) || p.charAt(i + 1) !== '(') {
result += c
continue
}

let depth = 1
let j = i + 2
let body = ''
let bodyEscaping = false

for (; j < p.length; j++) {
const bc = p.charAt(j)
if (bodyEscaping) {
bodyEscaping = false
body += bc
continue
}

if (bc === '\\') {
bodyEscaping = true
body += bc
continue
}

if (isExtglobStart(bc) && p.charAt(j + 1) === '(') {
depth++
body += bc + '('
j++
continue
}

if (bc === ')') {
depth--
if (!depth) {
break
}
}

body += bc
}

if (depth) {
result += p.slice(i)
break
}

const alternatives = splitExtglobAlternatives(body).map(alt =>
filterExtglobAlternatives(alt),
)
const valid = alternatives.filter(alt => !hasSlash(alt))
result +=
valid.length === 0 ?
p.slice(i, j + 1)
: c + '(' + valid.join('|') + ')'
i = j
}

return result
}

export const sep =
defaultPlatform === 'win32' ? path.win32.sep : path.posix.sep
minimatch.sep = sep
Expand Down Expand Up @@ -1396,13 +1525,14 @@ export class Minimatch {
// so that UNC paths aren't broken. Otherwise, any number of
// / characters are coalesced into one, unless
// preserveMultipleSlashes is set to true.
const filtered = filterExtglobAlternatives(p)
if (this.preserveMultipleSlashes) {
return p.split('/')
return filtered.split('/')
} else if (this.isWindows && /^\/\/[^/]+/.test(p)) {
// add an extra '' for the one we lose
return ['', ...p.split(/\/+/)]
return ['', ...filtered.split(/\/+/)]
} else {
return p.split(/\/+/)
return filtered.split(/\/+/)
}
}

Expand Down
11 changes: 11 additions & 0 deletions test/basic.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,17 @@ t.test('filter function', t => {
t.end()
})

t.test('extglob alternatives with slashes do not break pattern', t => {
const files = ['a/b', 'a/x', 'a/b/c', 'x/y']

t.same(mm.match(files, '@(x|a|a/b)/*'), ['a/b', 'a/x', 'x/y'])
t.same(mm.match(files, '@(a|a/b)/*'), ['a/b', 'a/x'])
t.same(mm.match(files, '@(a/b)/*'), [])
t.same(mm.match(files, '@(a\\|a/b)/*'), [])

t.end()
})

t.test('whitespace handling', t => {
t.equal(mm('x/y', 'y'), false)
t.equal(mm('x/y', 'y', { matchBase: true }), true)
Expand Down