Skip to content

Commit

Permalink
Detect leading dots in extglob subpatterns
Browse files Browse the repository at this point in the history
Fix: isaacs/node-glob#387

Will backport to v4 and v5
  • Loading branch information
isaacs committed Jan 17, 2023
1 parent 1029391 commit f35d0b8
Show file tree
Hide file tree
Showing 3 changed files with 131 additions and 59 deletions.
62 changes: 44 additions & 18 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -615,12 +615,20 @@ export class Minimatch {
let pl: PatternListEntry | undefined
let sp: SubparseReturn
// . and .. never match anything that doesn't start with .,
// even when options.dot is set.
const patternStart =
pattern.charAt(0) === '.'
? '' // anything
: // not (start or / followed by . or .. followed by / or end)
options.dot
// even when options.dot is set. However, if the pattern
// starts with ., then traversal patterns can match.
let dotTravAllowed = pattern.charAt(0) === '.'
let dotFileAllowed = options.dot || dotTravAllowed
const patternStart = () =>
dotTravAllowed
? ''
: dotFileAllowed
? '(?!(?:^|\\/)\\.{1,2}(?:$|\\/))'
: '(?!\\.)'
const subPatternStart = (p: string) =>
p.charAt(0) === '.'
? ''
: options.dot
? '(?!(?:^|\\/)\\.{1,2}(?:$|\\/))'
: '(?!\\.)'

Expand Down Expand Up @@ -719,7 +727,7 @@ export class Minimatch {
if (options.noext) clearStateChar()
continue

case '(':
case '(': {
if (inClass) {
re += '('
continue
Expand All @@ -730,45 +738,63 @@ export class Minimatch {
continue
}

patternListStack.push({
const plEntry: PatternListEntry = {
type: stateChar,
start: i - 1,
reStart: re.length,
open: plTypes[stateChar].open,
close: plTypes[stateChar].close,
})
// negation is (?:(?!js)[^/]*)
re += stateChar === '!' ? '(?:(?!(?:' : '(?:'
}
this.debug(this.pattern, '\t', plEntry)
patternListStack.push(plEntry)
// negation is (?:(?!(?:js)(?:<rest>))[^/]*)
re += plEntry.open
// next entry starts with a dot maybe?
if (plEntry.start === 0 && plEntry.type !== '!') {
dotTravAllowed = true
re += subPatternStart(pattern.slice(i + 1))
}
this.debug('plType %j %j', stateChar, re)
stateChar = false
continue
}

case ')':
if (inClass || !patternListStack.length) {
case ')': {
const plEntry = patternListStack.pop()
if (inClass || !plEntry) {
re += '\\)'
continue
}

// closing an extglob
clearStateChar()
hasMagic = true
pl = patternListStack.pop() as PatternListEntry
pl = plEntry
// negation is (?:(?!js)[^/]*)
// The others are (?:<pattern>)<type>
re += pl.close
if (pl.type === '!') {
negativeLists.push(Object.assign(pl, { reEnd: re.length }))
}
continue
}

case '|':
if (inClass || !patternListStack.length) {
case '|': {
const plEntry = patternListStack[patternListStack.length - 1]
if (inClass || !plEntry) {
re += '\\|'
continue
}

clearStateChar()
re += '|'
// next subpattern can start with a dot?
if (plEntry.start === 0 && plEntry.type !== '!') {
dotTravAllowed = true
re += subPatternStart(pattern.slice(i + 1))
}
continue
}

// these are mostly the same in regexp and glob
case '[':
Expand Down Expand Up @@ -852,7 +878,7 @@ export class Minimatch {
for (pl = patternListStack.pop(); pl; pl = patternListStack.pop()) {
let tail: string
tail = re.slice(pl.reStart + pl.open.length)
this.debug('setting tail', re, pl)
this.debug(this.pattern, 'setting tail', re, pl)
// maybe some even number of \, then maybe 1 \, followed by a |
tail = tail.replace(/((?:\\{2}){0,64})(\\?)\|/g, (_, $1, $2) => {
if (!$2) {
Expand Down Expand Up @@ -928,7 +954,7 @@ export class Minimatch {
}

if (addPatternStart) {
re = patternStart + re
re = patternStart() + re
}

// parsing just a piece of a larger pattern.
Expand Down
56 changes: 48 additions & 8 deletions tap-snapshots/test/basic.js.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ exports[`test/basic.js TAP basic tests > makeRe !!a* 1`] = `
/^(?:(?=.)a[^/]*?)$/
`

exports[`test/basic.js TAP basic tests > makeRe !(.a|js)@(.*) 1`] = `
/^(?:(?!\\.)(?=.)(?:(?!(?:\\.a|js)(?:\\.[^/]*?))[^/]*?)(?:\\.[^/]*?))$/
`

exports[`test/basic.js TAP basic tests > makeRe !\\!a* 1`] = `
/^(?!^(?:(?=.)\\!a[^/]*?)$).*$/
`
Expand All @@ -33,20 +37,24 @@ exports[`test/basic.js TAP basic tests > makeRe #* 1`] = `
/^(?:(?=.)#[^/]*?)$/
`

exports[`test/basic.js TAP basic tests > makeRe * 1`] = `
/^(?:(?!(?:^|\\/)\\.{1,2}(?:$|\\/))(?=.)[^/]*?)$/
`

exports[`test/basic.js TAP basic tests > makeRe *(a/b) 1`] = `
/^(?:(?!\\.)(?=.)[^/]*?\\(a\\/b\\))$/
/^(?:(?=.)[^/]*?\\((?!\\.)a\\/b\\))$/
`

exports[`test/basic.js TAP basic tests > makeRe *(a|{b),c)} 1`] = `
/^(?:(?!\\.)(?=.)(?:a|b)*|(?!\\.)(?=.)(?:a|c)*)$/
/^(?:(?=.)(?:(?!\\.)a|(?!\\.)b)*|(?=.)(?:(?!\\.)a|(?!\\.)c)*)$/
`

exports[`test/basic.js TAP basic tests > makeRe *(a|{b,c}) 1`] = `
/^(?:(?!\\.)(?=.)(?:a|b)*|(?!\\.)(?=.)(?:a|c)*)$/
/^(?:(?=.)(?:(?!\\.)a|(?!\\.)b)*|(?=.)(?:(?!\\.)a|(?!\\.)c)*)$/
`

exports[`test/basic.js TAP basic tests > makeRe *(a|{b|c,c}) 1`] = `
/^(?:(?!\\.)(?=.)(?:a|b|c)*|(?!\\.)(?=.)(?:a|c)*)$/
/^(?:(?=.)(?:(?!\\.)a|(?!\\.)b|(?!\\.)c)*|(?=.)(?:(?!\\.)a|(?!\\.)c)*)$/
`

exports[`test/basic.js TAP basic tests > makeRe *(a|{b|c,c}) 2`] = `
Expand Down Expand Up @@ -110,11 +118,15 @@ exports[`test/basic.js TAP basic tests > makeRe *c*?** 1`] = `
`

exports[`test/basic.js TAP basic tests > makeRe +(a)!(b)+(c) 1`] = `
/^(?:(?!\\.)(?=.)(?:a)+(?:(?!(?:b)(?:c)+)[^/]*?)(?:c)+)$/
/^(?:(?=.)(?:(?!\\.)a)+(?:(?!(?:b)(?:c)+)[^/]*?)(?:c)+)$/
`

exports[`test/basic.js TAP basic tests > makeRe +(a|*\\|c\\\\|d\\\\\\|e\\\\\\\\|f\\\\\\\\\\|g 1`] = `
/^(?:(?=.)\\+\\(a\\|[^/]*?\\|c\\\\\\\\\\|d\\\\\\\\\\|e\\\\\\\\\\\\\\\\\\|f\\\\\\\\\\\\\\\\\\|g)$/
/^(?:(?=.)\\+\\((?!\\.)a\\|(?!\\.)[^/]*?\\|c\\\\\\\\\\|(?!\\.)d\\\\\\\\\\|e\\\\\\\\\\\\\\\\\\|(?!\\.)f\\\\\\\\\\\\\\\\\\|g)$/
`

exports[`test/basic.js TAP basic tests > makeRe .* 1`] = `
/^(?:(?=.)\\.[^/]*?)$/
`

exports[`test/basic.js TAP basic tests > makeRe /^root:/{s/^[^:]*:[^:]*:([^:]*).*$// 1`] = `
Expand Down Expand Up @@ -157,6 +169,34 @@ exports[`test/basic.js TAP basic tests > makeRe ??**********?****c 1`] = `
/^(?:(?!\\.)(?=.)[^/][^/][^/]*?[^/]*?[^/]*?[^/]*?[^/]*?[^/]*?[^/]*?[^/]*?[^/]*?[^/]*?[^/][^/]*?[^/]*?[^/]*?[^/]*?c)$/
`

exports[`test/basic.js TAP basic tests > makeRe @(*|.*) 1`] = `
/^(?:(?=.)(?:(?!\\.)[^/]*?|\\.[^/]*?))$/
`

exports[`test/basic.js TAP basic tests > makeRe @(*|a) 1`] = `
/^(?:(?=.)(?:(?!(?:^|\\/)\\.{1,2}(?:$|\\/))[^/]*?|(?!(?:^|\\/)\\.{1,2}(?:$|\\/))a))$/
`

exports[`test/basic.js TAP basic tests > makeRe @(.*) 1`] = `
/^(?:(?=.)(?:\\.[^/]*?))$/
`

exports[`test/basic.js TAP basic tests > makeRe @(.*) 2`] = `
/^(?:(?=.)(?:\\.[^/]*?))$/
`

exports[`test/basic.js TAP basic tests > makeRe @(.*|*) 1`] = `
/^(?:(?=.)(?:\\.[^/]*?|(?!\\.)[^/]*?))$/
`

exports[`test/basic.js TAP basic tests > makeRe @(.*|js) 1`] = `
/^(?:(?=.)(?:\\.[^/]*?|(?!\\.)js))$/
`

exports[`test/basic.js TAP basic tests > makeRe @(js|.*) 1`] = `
/^(?:(?=.)(?:(?!\\.)js|\\.[^/]*?))$/
`

exports[`test/basic.js TAP basic tests > makeRe X* 1`] = `
/^(?:(?=.)X[^/]*?)$/
`
Expand Down Expand Up @@ -422,11 +462,11 @@ exports[`test/basic.js TAP basic tests > makeRe {/?,*} 1`] = `
`

exports[`test/basic.js TAP basic tests > makeRe {a,*(b|c,d)} 1`] = `
/^(?:a|(?!\\.)(?=.)[^/]*?\\(b\\|c|d\\))$/
/^(?:a|(?=.)[^/]*?\\((?!\\.)b\\|(?!\\.)c|d\\))$/
`

exports[`test/basic.js TAP basic tests > makeRe {a,*(b|{c,d})} 1`] = `
/^(?:a|(?!\\.)(?=.)(?:b|c)*|(?!\\.)(?=.)(?:b|d)*)$/
/^(?:a|(?=.)(?:(?!\\.)b|(?!\\.)c)*|(?=.)(?:(?!\\.)b|(?!\\.)d)*)$/
`

exports[`test/basic.js TAP basic tests > makeRe Å 1`] = `
Expand Down
72 changes: 39 additions & 33 deletions test/patterns.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,17 +58,11 @@ module.exports = [
['[a-c]b*', ['abc', 'abd', 'abe', 'bb', 'cb']],
['[a-y]*[^c]', ['abd', 'abe', 'bb', 'bcd', 'bdir/', 'ca', 'cb', 'dd', 'de']],
['a*[^c]', ['abd', 'abe']],
function () {
files.push('a-b', 'aXb')
},
() => files.push('a-b', 'aXb'),
['a[X-]b', ['a-b', 'aXb']],
function () {
files.push('.x', '.y')
},
() => files.push('.x', '.y'),
['[^a-c]*', ['d', 'dd', 'de']],
function () {
files.push('a*b/', 'a*b/ooo')
},
() => files.push('a*b/', 'a*b/ooo'),
['a\\*b/*', ['a*b/ooo']],
['a\\*?/*', ['a*b/ooo']],
['*\\\\!*', [], { null: true }, ['echo !7']],
Expand All @@ -82,9 +76,7 @@ module.exports = [

'http://www.opensource.apple.com/source/bash/bash-23/' +
'bash/tests/glob-test',
function () {
files.push('man/', 'man/man1/', 'man/man1/bash.1')
},
() => files.push('man/', 'man/man1/', 'man/man1/bash.1'),
['*/man*/bash.*', ['man/man1/bash.1']],
['man/man1/bash.1', ['man/man1/bash.1']],
['a***c', ['abc'], null, ['abc']],
Expand Down Expand Up @@ -146,9 +138,7 @@ module.exports = [

// .. and . can only match patterns starting with .,
// even when options.dot is set.
function () {
files = ['a/./b', 'a/../b', 'a/c/b', 'a/.d/b']
},
() => (files = ['a/./b', 'a/../b', 'a/c/b', 'a/.d/b']),
['a/*/b', ['a/c/b', 'a/.d/b'], { dot: true }],
['a/.*/b', ['a/./b', 'a/../b', 'a/.d/b'], { dot: true }],
['a/*/b', ['a/c/b'], { dot: false }],
Expand Down Expand Up @@ -192,8 +182,8 @@ module.exports = [
],

// crazy nested {,,} and *(||) tests.
function () {
files = [
() =>
(files = [
'a',
'b',
'c',
Expand All @@ -216,8 +206,7 @@ module.exports = [
'x(a|c)',
'(a|b|c)',
'(a|c)',
]
},
]),
['*(a|{b,c})', ['a', 'b', 'c', 'ab', 'ac']],
['{a,*(b|c,d)}', ['a', '(b|c', '*(b|c', 'd)']],
// a
Expand All @@ -238,9 +227,7 @@ module.exports = [

// begin channelling Boole and deMorgan...
'negation tests',
function () {
files = ['d', 'e', '!ab', '!abc', 'a!b', '\\!a']
},
() => (files = ['d', 'e', '!ab', '!abc', 'a!b', '\\!a']),

// anything that is NOT a* matches.
['!a*', ['\\!a', 'd', 'e', '!ab', '!abc']],
Expand All @@ -254,17 +241,23 @@ module.exports = [
// anything that is NOT !a* matches
['!\\!a*', ['a!b', 'd', 'e', '\\!a']],

// negation nestled within a pattern
function () {
files = ['foo.js', 'foo.bar', 'foo.js.js', 'blar.js', 'foo.', 'boo.js.boo']
},
'negation nestled within a pattern',
() =>
(files = [
'foo.js',
'foo.bar',
'foo.js.js',
'blar.js',
'foo.',
'boo.js.boo',
]),
// last one is tricky! * matches foo, . matches ., and 'js.js' != 'js'
// copy bash 4.3 behavior on this.
['*.!(js)', ['foo.bar', 'foo.', 'boo.js.boo', 'foo.js.js']],

'https://github.com/isaacs/minimatch/issues/5',
function () {
files = [
() =>
(files = [
'a/b/.x/c',
'a/b/.x/c/d',
'a/b/.x/c/d/e',
Expand All @@ -277,8 +270,7 @@ module.exports = [
'.x/a/b',
'a/.x/b/.x/c',
'.x/.x',
]
},
]),
[
'**/.x/**',
[
Expand Down Expand Up @@ -312,24 +304,38 @@ module.exports = [
['[\\-\\]]', []],
['[a-b-c]', []],

// https://github.com/isaacs/node-glob/issues/415
'https://github.com/isaacs/node-glob/issues/415',
() => {
files = ['ac', 'abc', 'acd', 'acc', 'acd', 'adc', 'bbc', 'bac', 'bcc']
},
['+(a)!(b)+(c)', ['ac', 'acc', 'adc']],

// https://github.com/isaacs/node-glob/issues/394
'https://github.com/isaacs/node-glob/issues/394',
() => (files = ['å']),
['å', ['å']],
['å', ['å'], { nocase: true }],
['Å', ['å'], { nocase: true }],
['Å', [], {}],

() => (files = ['Å']),
['Å', ['Å']],
['å', ['Å'], { nocase: true }],
['Å', ['Å'], { nocase: true }],
['å', [], {}],

'https://github.com/isaacs/node-glob/issues/387',
() => (files = ['.a', '.a.js', '.js', 'a', 'a.js', 'js']),
['.*', ['.a', '.a.js', '.js']],
['*', ['.a', '.a.js', '.js', 'a', 'a.js', 'js'], { dot: true }],
['@(*|.*)', ['.a', '.a.js', '.js', 'a', 'a.js', 'js']],
['@(.*|*)', ['.a', '.a.js', '.js', 'a', 'a.js', 'js']],
['@(*|a)', ['.a', '.a.js', '.js', 'a', 'a.js', 'js'], { dot: true }],
['@(.*)', ['.a', '.a.js', '.js']],
['@(.*)', ['.a', '.a.js', '.js'], { dot: true }],
['@(js|.*)', ['js', '.a', '.a.js', '.js']],
['@(.*|js)', ['js', '.a', '.a.js', '.js']],
// doesn't start at 0, no dice
// neg extglobs don't trigger this behavior.
['!(.a|js)@(.*)', ['a.js'], { nonegate: true }],
]

Object.defineProperty(module.exports, 'files', {
Expand Down

0 comments on commit f35d0b8

Please sign in to comment.