You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1091 lines
27 KiB
1091 lines
27 KiB
'use strict'; |
|
|
|
const constants = require('./constants'); |
|
const utils = require('./utils'); |
|
|
|
/** |
|
* Constants |
|
*/ |
|
|
|
const { |
|
MAX_LENGTH, |
|
POSIX_REGEX_SOURCE, |
|
REGEX_NON_SPECIAL_CHARS, |
|
REGEX_SPECIAL_CHARS_BACKREF, |
|
REPLACEMENTS |
|
} = constants; |
|
|
|
/** |
|
* Helpers |
|
*/ |
|
|
|
const expandRange = (args, options) => { |
|
if (typeof options.expandRange === 'function') { |
|
return options.expandRange(...args, options); |
|
} |
|
|
|
args.sort(); |
|
const value = `[${args.join('-')}]`; |
|
|
|
try { |
|
/* eslint-disable-next-line no-new */ |
|
new RegExp(value); |
|
} catch (ex) { |
|
return args.map(v => utils.escapeRegex(v)).join('..'); |
|
} |
|
|
|
return value; |
|
}; |
|
|
|
/** |
|
* Create the message for a syntax error |
|
*/ |
|
|
|
const syntaxError = (type, char) => { |
|
return `Missing ${type}: "${char}" - use "\\\\${char}" to match literal characters`; |
|
}; |
|
|
|
/** |
|
* Parse the given input string. |
|
* @param {String} input |
|
* @param {Object} options |
|
* @return {Object} |
|
*/ |
|
|
|
const parse = (input, options) => { |
|
if (typeof input !== 'string') { |
|
throw new TypeError('Expected a string'); |
|
} |
|
|
|
input = REPLACEMENTS[input] || input; |
|
|
|
const opts = { ...options }; |
|
const max = typeof opts.maxLength === 'number' ? Math.min(MAX_LENGTH, opts.maxLength) : MAX_LENGTH; |
|
|
|
let len = input.length; |
|
if (len > max) { |
|
throw new SyntaxError(`Input length: ${len}, exceeds maximum allowed length: ${max}`); |
|
} |
|
|
|
const bos = { type: 'bos', value: '', output: opts.prepend || '' }; |
|
const tokens = [bos]; |
|
|
|
const capture = opts.capture ? '' : '?:'; |
|
const win32 = utils.isWindows(options); |
|
|
|
// create constants based on platform, for windows or posix |
|
const PLATFORM_CHARS = constants.globChars(win32); |
|
const EXTGLOB_CHARS = constants.extglobChars(PLATFORM_CHARS); |
|
|
|
const { |
|
DOT_LITERAL, |
|
PLUS_LITERAL, |
|
SLASH_LITERAL, |
|
ONE_CHAR, |
|
DOTS_SLASH, |
|
NO_DOT, |
|
NO_DOT_SLASH, |
|
NO_DOTS_SLASH, |
|
QMARK, |
|
QMARK_NO_DOT, |
|
STAR, |
|
START_ANCHOR |
|
} = PLATFORM_CHARS; |
|
|
|
const globstar = opts => { |
|
return `(${capture}(?:(?!${START_ANCHOR}${opts.dot ? DOTS_SLASH : DOT_LITERAL}).)*?)`; |
|
}; |
|
|
|
const nodot = opts.dot ? '' : NO_DOT; |
|
const qmarkNoDot = opts.dot ? QMARK : QMARK_NO_DOT; |
|
let star = opts.bash === true ? globstar(opts) : STAR; |
|
|
|
if (opts.capture) { |
|
star = `(${star})`; |
|
} |
|
|
|
// minimatch options support |
|
if (typeof opts.noext === 'boolean') { |
|
opts.noextglob = opts.noext; |
|
} |
|
|
|
const state = { |
|
input, |
|
index: -1, |
|
start: 0, |
|
dot: opts.dot === true, |
|
consumed: '', |
|
output: '', |
|
prefix: '', |
|
backtrack: false, |
|
negated: false, |
|
brackets: 0, |
|
braces: 0, |
|
parens: 0, |
|
quotes: 0, |
|
globstar: false, |
|
tokens |
|
}; |
|
|
|
input = utils.removePrefix(input, state); |
|
len = input.length; |
|
|
|
const extglobs = []; |
|
const braces = []; |
|
const stack = []; |
|
let prev = bos; |
|
let value; |
|
|
|
/** |
|
* Tokenizing helpers |
|
*/ |
|
|
|
const eos = () => state.index === len - 1; |
|
const peek = state.peek = (n = 1) => input[state.index + n]; |
|
const advance = state.advance = () => input[++state.index] || ''; |
|
const remaining = () => input.slice(state.index + 1); |
|
const consume = (value = '', num = 0) => { |
|
state.consumed += value; |
|
state.index += num; |
|
}; |
|
|
|
const append = token => { |
|
state.output += token.output != null ? token.output : token.value; |
|
consume(token.value); |
|
}; |
|
|
|
const negate = () => { |
|
let count = 1; |
|
|
|
while (peek() === '!' && (peek(2) !== '(' || peek(3) === '?')) { |
|
advance(); |
|
state.start++; |
|
count++; |
|
} |
|
|
|
if (count % 2 === 0) { |
|
return false; |
|
} |
|
|
|
state.negated = true; |
|
state.start++; |
|
return true; |
|
}; |
|
|
|
const increment = type => { |
|
state[type]++; |
|
stack.push(type); |
|
}; |
|
|
|
const decrement = type => { |
|
state[type]--; |
|
stack.pop(); |
|
}; |
|
|
|
/** |
|
* Push tokens onto the tokens array. This helper speeds up |
|
* tokenizing by 1) helping us avoid backtracking as much as possible, |
|
* and 2) helping us avoid creating extra tokens when consecutive |
|
* characters are plain text. This improves performance and simplifies |
|
* lookbehinds. |
|
*/ |
|
|
|
const push = tok => { |
|
if (prev.type === 'globstar') { |
|
const isBrace = state.braces > 0 && (tok.type === 'comma' || tok.type === 'brace'); |
|
const isExtglob = tok.extglob === true || (extglobs.length && (tok.type === 'pipe' || tok.type === 'paren')); |
|
|
|
if (tok.type !== 'slash' && tok.type !== 'paren' && !isBrace && !isExtglob) { |
|
state.output = state.output.slice(0, -prev.output.length); |
|
prev.type = 'star'; |
|
prev.value = '*'; |
|
prev.output = star; |
|
state.output += prev.output; |
|
} |
|
} |
|
|
|
if (extglobs.length && tok.type !== 'paren') { |
|
extglobs[extglobs.length - 1].inner += tok.value; |
|
} |
|
|
|
if (tok.value || tok.output) append(tok); |
|
if (prev && prev.type === 'text' && tok.type === 'text') { |
|
prev.value += tok.value; |
|
prev.output = (prev.output || '') + tok.value; |
|
return; |
|
} |
|
|
|
tok.prev = prev; |
|
tokens.push(tok); |
|
prev = tok; |
|
}; |
|
|
|
const extglobOpen = (type, value) => { |
|
const token = { ...EXTGLOB_CHARS[value], conditions: 1, inner: '' }; |
|
|
|
token.prev = prev; |
|
token.parens = state.parens; |
|
token.output = state.output; |
|
const output = (opts.capture ? '(' : '') + token.open; |
|
|
|
increment('parens'); |
|
push({ type, value, output: state.output ? '' : ONE_CHAR }); |
|
push({ type: 'paren', extglob: true, value: advance(), output }); |
|
extglobs.push(token); |
|
}; |
|
|
|
const extglobClose = token => { |
|
let output = token.close + (opts.capture ? ')' : ''); |
|
let rest; |
|
|
|
if (token.type === 'negate') { |
|
let extglobStar = star; |
|
|
|
if (token.inner && token.inner.length > 1 && token.inner.includes('/')) { |
|
extglobStar = globstar(opts); |
|
} |
|
|
|
if (extglobStar !== star || eos() || /^\)+$/.test(remaining())) { |
|
output = token.close = `)$))${extglobStar}`; |
|
} |
|
|
|
if (token.inner.includes('*') && (rest = remaining()) && /^\.[^\\/.]+$/.test(rest)) { |
|
// Any non-magical string (`.ts`) or even nested expression (`.{ts,tsx}`) can follow after the closing parenthesis. |
|
// In this case, we need to parse the string and use it in the output of the original pattern. |
|
// Suitable patterns: `/!(*.d).ts`, `/!(*.d).{ts,tsx}`, `**/!(*-dbg).@(js)`. |
|
// |
|
// Disabling the `fastpaths` option due to a problem with parsing strings as `.ts` in the pattern like `**/!(*.d).ts`. |
|
const expression = parse(rest, { ...options, fastpaths: false }).output; |
|
|
|
output = token.close = `)${expression})${extglobStar})`; |
|
} |
|
|
|
if (token.prev.type === 'bos') { |
|
state.negatedExtglob = true; |
|
} |
|
} |
|
|
|
push({ type: 'paren', extglob: true, value, output }); |
|
decrement('parens'); |
|
}; |
|
|
|
/** |
|
* Fast paths |
|
*/ |
|
|
|
if (opts.fastpaths !== false && !/(^[*!]|[/()[\]{}"])/.test(input)) { |
|
let backslashes = false; |
|
|
|
let output = input.replace(REGEX_SPECIAL_CHARS_BACKREF, (m, esc, chars, first, rest, index) => { |
|
if (first === '\\') { |
|
backslashes = true; |
|
return m; |
|
} |
|
|
|
if (first === '?') { |
|
if (esc) { |
|
return esc + first + (rest ? QMARK.repeat(rest.length) : ''); |
|
} |
|
if (index === 0) { |
|
return qmarkNoDot + (rest ? QMARK.repeat(rest.length) : ''); |
|
} |
|
return QMARK.repeat(chars.length); |
|
} |
|
|
|
if (first === '.') { |
|
return DOT_LITERAL.repeat(chars.length); |
|
} |
|
|
|
if (first === '*') { |
|
if (esc) { |
|
return esc + first + (rest ? star : ''); |
|
} |
|
return star; |
|
} |
|
return esc ? m : `\\${m}`; |
|
}); |
|
|
|
if (backslashes === true) { |
|
if (opts.unescape === true) { |
|
output = output.replace(/\\/g, ''); |
|
} else { |
|
output = output.replace(/\\+/g, m => { |
|
return m.length % 2 === 0 ? '\\\\' : (m ? '\\' : ''); |
|
}); |
|
} |
|
} |
|
|
|
if (output === input && opts.contains === true) { |
|
state.output = input; |
|
return state; |
|
} |
|
|
|
state.output = utils.wrapOutput(output, state, options); |
|
return state; |
|
} |
|
|
|
/** |
|
* Tokenize input until we reach end-of-string |
|
*/ |
|
|
|
while (!eos()) { |
|
value = advance(); |
|
|
|
if (value === '\u0000') { |
|
continue; |
|
} |
|
|
|
/** |
|
* Escaped characters |
|
*/ |
|
|
|
if (value === '\\') { |
|
const next = peek(); |
|
|
|
if (next === '/' && opts.bash !== true) { |
|
continue; |
|
} |
|
|
|
if (next === '.' || next === ';') { |
|
continue; |
|
} |
|
|
|
if (!next) { |
|
value += '\\'; |
|
push({ type: 'text', value }); |
|
continue; |
|
} |
|
|
|
// collapse slashes to reduce potential for exploits |
|
const match = /^\\+/.exec(remaining()); |
|
let slashes = 0; |
|
|
|
if (match && match[0].length > 2) { |
|
slashes = match[0].length; |
|
state.index += slashes; |
|
if (slashes % 2 !== 0) { |
|
value += '\\'; |
|
} |
|
} |
|
|
|
if (opts.unescape === true) { |
|
value = advance(); |
|
} else { |
|
value += advance(); |
|
} |
|
|
|
if (state.brackets === 0) { |
|
push({ type: 'text', value }); |
|
continue; |
|
} |
|
} |
|
|
|
/** |
|
* If we're inside a regex character class, continue |
|
* until we reach the closing bracket. |
|
*/ |
|
|
|
if (state.brackets > 0 && (value !== ']' || prev.value === '[' || prev.value === '[^')) { |
|
if (opts.posix !== false && value === ':') { |
|
const inner = prev.value.slice(1); |
|
if (inner.includes('[')) { |
|
prev.posix = true; |
|
|
|
if (inner.includes(':')) { |
|
const idx = prev.value.lastIndexOf('['); |
|
const pre = prev.value.slice(0, idx); |
|
const rest = prev.value.slice(idx + 2); |
|
const posix = POSIX_REGEX_SOURCE[rest]; |
|
if (posix) { |
|
prev.value = pre + posix; |
|
state.backtrack = true; |
|
advance(); |
|
|
|
if (!bos.output && tokens.indexOf(prev) === 1) { |
|
bos.output = ONE_CHAR; |
|
} |
|
continue; |
|
} |
|
} |
|
} |
|
} |
|
|
|
if ((value === '[' && peek() !== ':') || (value === '-' && peek() === ']')) { |
|
value = `\\${value}`; |
|
} |
|
|
|
if (value === ']' && (prev.value === '[' || prev.value === '[^')) { |
|
value = `\\${value}`; |
|
} |
|
|
|
if (opts.posix === true && value === '!' && prev.value === '[') { |
|
value = '^'; |
|
} |
|
|
|
prev.value += value; |
|
append({ value }); |
|
continue; |
|
} |
|
|
|
/** |
|
* If we're inside a quoted string, continue |
|
* until we reach the closing double quote. |
|
*/ |
|
|
|
if (state.quotes === 1 && value !== '"') { |
|
value = utils.escapeRegex(value); |
|
prev.value += value; |
|
append({ value }); |
|
continue; |
|
} |
|
|
|
/** |
|
* Double quotes |
|
*/ |
|
|
|
if (value === '"') { |
|
state.quotes = state.quotes === 1 ? 0 : 1; |
|
if (opts.keepQuotes === true) { |
|
push({ type: 'text', value }); |
|
} |
|
continue; |
|
} |
|
|
|
/** |
|
* Parentheses |
|
*/ |
|
|
|
if (value === '(') { |
|
increment('parens'); |
|
push({ type: 'paren', value }); |
|
continue; |
|
} |
|
|
|
if (value === ')') { |
|
if (state.parens === 0 && opts.strictBrackets === true) { |
|
throw new SyntaxError(syntaxError('opening', '(')); |
|
} |
|
|
|
const extglob = extglobs[extglobs.length - 1]; |
|
if (extglob && state.parens === extglob.parens + 1) { |
|
extglobClose(extglobs.pop()); |
|
continue; |
|
} |
|
|
|
push({ type: 'paren', value, output: state.parens ? ')' : '\\)' }); |
|
decrement('parens'); |
|
continue; |
|
} |
|
|
|
/** |
|
* Square brackets |
|
*/ |
|
|
|
if (value === '[') { |
|
if (opts.nobracket === true || !remaining().includes(']')) { |
|
if (opts.nobracket !== true && opts.strictBrackets === true) { |
|
throw new SyntaxError(syntaxError('closing', ']')); |
|
} |
|
|
|
value = `\\${value}`; |
|
} else { |
|
increment('brackets'); |
|
} |
|
|
|
push({ type: 'bracket', value }); |
|
continue; |
|
} |
|
|
|
if (value === ']') { |
|
if (opts.nobracket === true || (prev && prev.type === 'bracket' && prev.value.length === 1)) { |
|
push({ type: 'text', value, output: `\\${value}` }); |
|
continue; |
|
} |
|
|
|
if (state.brackets === 0) { |
|
if (opts.strictBrackets === true) { |
|
throw new SyntaxError(syntaxError('opening', '[')); |
|
} |
|
|
|
push({ type: 'text', value, output: `\\${value}` }); |
|
continue; |
|
} |
|
|
|
decrement('brackets'); |
|
|
|
const prevValue = prev.value.slice(1); |
|
if (prev.posix !== true && prevValue[0] === '^' && !prevValue.includes('/')) { |
|
value = `/${value}`; |
|
} |
|
|
|
prev.value += value; |
|
append({ value }); |
|
|
|
// when literal brackets are explicitly disabled |
|
// assume we should match with a regex character class |
|
if (opts.literalBrackets === false || utils.hasRegexChars(prevValue)) { |
|
continue; |
|
} |
|
|
|
const escaped = utils.escapeRegex(prev.value); |
|
state.output = state.output.slice(0, -prev.value.length); |
|
|
|
// when literal brackets are explicitly enabled |
|
// assume we should escape the brackets to match literal characters |
|
if (opts.literalBrackets === true) { |
|
state.output += escaped; |
|
prev.value = escaped; |
|
continue; |
|
} |
|
|
|
// when the user specifies nothing, try to match both |
|
prev.value = `(${capture}${escaped}|${prev.value})`; |
|
state.output += prev.value; |
|
continue; |
|
} |
|
|
|
/** |
|
* Braces |
|
*/ |
|
|
|
if (value === '{' && opts.nobrace !== true) { |
|
increment('braces'); |
|
|
|
const open = { |
|
type: 'brace', |
|
value, |
|
output: '(', |
|
outputIndex: state.output.length, |
|
tokensIndex: state.tokens.length |
|
}; |
|
|
|
braces.push(open); |
|
push(open); |
|
continue; |
|
} |
|
|
|
if (value === '}') { |
|
const brace = braces[braces.length - 1]; |
|
|
|
if (opts.nobrace === true || !brace) { |
|
push({ type: 'text', value, output: value }); |
|
continue; |
|
} |
|
|
|
let output = ')'; |
|
|
|
if (brace.dots === true) { |
|
const arr = tokens.slice(); |
|
const range = []; |
|
|
|
for (let i = arr.length - 1; i >= 0; i--) { |
|
tokens.pop(); |
|
if (arr[i].type === 'brace') { |
|
break; |
|
} |
|
if (arr[i].type !== 'dots') { |
|
range.unshift(arr[i].value); |
|
} |
|
} |
|
|
|
output = expandRange(range, opts); |
|
state.backtrack = true; |
|
} |
|
|
|
if (brace.comma !== true && brace.dots !== true) { |
|
const out = state.output.slice(0, brace.outputIndex); |
|
const toks = state.tokens.slice(brace.tokensIndex); |
|
brace.value = brace.output = '\\{'; |
|
value = output = '\\}'; |
|
state.output = out; |
|
for (const t of toks) { |
|
state.output += (t.output || t.value); |
|
} |
|
} |
|
|
|
push({ type: 'brace', value, output }); |
|
decrement('braces'); |
|
braces.pop(); |
|
continue; |
|
} |
|
|
|
/** |
|
* Pipes |
|
*/ |
|
|
|
if (value === '|') { |
|
if (extglobs.length > 0) { |
|
extglobs[extglobs.length - 1].conditions++; |
|
} |
|
push({ type: 'text', value }); |
|
continue; |
|
} |
|
|
|
/** |
|
* Commas |
|
*/ |
|
|
|
if (value === ',') { |
|
let output = value; |
|
|
|
const brace = braces[braces.length - 1]; |
|
if (brace && stack[stack.length - 1] === 'braces') { |
|
brace.comma = true; |
|
output = '|'; |
|
} |
|
|
|
push({ type: 'comma', value, output }); |
|
continue; |
|
} |
|
|
|
/** |
|
* Slashes |
|
*/ |
|
|
|
if (value === '/') { |
|
// if the beginning of the glob is "./", advance the start |
|
// to the current index, and don't add the "./" characters |
|
// to the state. This greatly simplifies lookbehinds when |
|
// checking for BOS characters like "!" and "." (not "./") |
|
if (prev.type === 'dot' && state.index === state.start + 1) { |
|
state.start = state.index + 1; |
|
state.consumed = ''; |
|
state.output = ''; |
|
tokens.pop(); |
|
prev = bos; // reset "prev" to the first token |
|
continue; |
|
} |
|
|
|
push({ type: 'slash', value, output: SLASH_LITERAL }); |
|
continue; |
|
} |
|
|
|
/** |
|
* Dots |
|
*/ |
|
|
|
if (value === '.') { |
|
if (state.braces > 0 && prev.type === 'dot') { |
|
if (prev.value === '.') prev.output = DOT_LITERAL; |
|
const brace = braces[braces.length - 1]; |
|
prev.type = 'dots'; |
|
prev.output += value; |
|
prev.value += value; |
|
brace.dots = true; |
|
continue; |
|
} |
|
|
|
if ((state.braces + state.parens) === 0 && prev.type !== 'bos' && prev.type !== 'slash') { |
|
push({ type: 'text', value, output: DOT_LITERAL }); |
|
continue; |
|
} |
|
|
|
push({ type: 'dot', value, output: DOT_LITERAL }); |
|
continue; |
|
} |
|
|
|
/** |
|
* Question marks |
|
*/ |
|
|
|
if (value === '?') { |
|
const isGroup = prev && prev.value === '('; |
|
if (!isGroup && opts.noextglob !== true && peek() === '(' && peek(2) !== '?') { |
|
extglobOpen('qmark', value); |
|
continue; |
|
} |
|
|
|
if (prev && prev.type === 'paren') { |
|
const next = peek(); |
|
let output = value; |
|
|
|
if (next === '<' && !utils.supportsLookbehinds()) { |
|
throw new Error('Node.js v10 or higher is required for regex lookbehinds'); |
|
} |
|
|
|
if ((prev.value === '(' && !/[!=<:]/.test(next)) || (next === '<' && !/<([!=]|\w+>)/.test(remaining()))) { |
|
output = `\\${value}`; |
|
} |
|
|
|
push({ type: 'text', value, output }); |
|
continue; |
|
} |
|
|
|
if (opts.dot !== true && (prev.type === 'slash' || prev.type === 'bos')) { |
|
push({ type: 'qmark', value, output: QMARK_NO_DOT }); |
|
continue; |
|
} |
|
|
|
push({ type: 'qmark', value, output: QMARK }); |
|
continue; |
|
} |
|
|
|
/** |
|
* Exclamation |
|
*/ |
|
|
|
if (value === '!') { |
|
if (opts.noextglob !== true && peek() === '(') { |
|
if (peek(2) !== '?' || !/[!=<:]/.test(peek(3))) { |
|
extglobOpen('negate', value); |
|
continue; |
|
} |
|
} |
|
|
|
if (opts.nonegate !== true && state.index === 0) { |
|
negate(); |
|
continue; |
|
} |
|
} |
|
|
|
/** |
|
* Plus |
|
*/ |
|
|
|
if (value === '+') { |
|
if (opts.noextglob !== true && peek() === '(' && peek(2) !== '?') { |
|
extglobOpen('plus', value); |
|
continue; |
|
} |
|
|
|
if ((prev && prev.value === '(') || opts.regex === false) { |
|
push({ type: 'plus', value, output: PLUS_LITERAL }); |
|
continue; |
|
} |
|
|
|
if ((prev && (prev.type === 'bracket' || prev.type === 'paren' || prev.type === 'brace')) || state.parens > 0) { |
|
push({ type: 'plus', value }); |
|
continue; |
|
} |
|
|
|
push({ type: 'plus', value: PLUS_LITERAL }); |
|
continue; |
|
} |
|
|
|
/** |
|
* Plain text |
|
*/ |
|
|
|
if (value === '@') { |
|
if (opts.noextglob !== true && peek() === '(' && peek(2) !== '?') { |
|
push({ type: 'at', extglob: true, value, output: '' }); |
|
continue; |
|
} |
|
|
|
push({ type: 'text', value }); |
|
continue; |
|
} |
|
|
|
/** |
|
* Plain text |
|
*/ |
|
|
|
if (value !== '*') { |
|
if (value === '$' || value === '^') { |
|
value = `\\${value}`; |
|
} |
|
|
|
const match = REGEX_NON_SPECIAL_CHARS.exec(remaining()); |
|
if (match) { |
|
value += match[0]; |
|
state.index += match[0].length; |
|
} |
|
|
|
push({ type: 'text', value }); |
|
continue; |
|
} |
|
|
|
/** |
|
* Stars |
|
*/ |
|
|
|
if (prev && (prev.type === 'globstar' || prev.star === true)) { |
|
prev.type = 'star'; |
|
prev.star = true; |
|
prev.value += value; |
|
prev.output = star; |
|
state.backtrack = true; |
|
state.globstar = true; |
|
consume(value); |
|
continue; |
|
} |
|
|
|
let rest = remaining(); |
|
if (opts.noextglob !== true && /^\([^?]/.test(rest)) { |
|
extglobOpen('star', value); |
|
continue; |
|
} |
|
|
|
if (prev.type === 'star') { |
|
if (opts.noglobstar === true) { |
|
consume(value); |
|
continue; |
|
} |
|
|
|
const prior = prev.prev; |
|
const before = prior.prev; |
|
const isStart = prior.type === 'slash' || prior.type === 'bos'; |
|
const afterStar = before && (before.type === 'star' || before.type === 'globstar'); |
|
|
|
if (opts.bash === true && (!isStart || (rest[0] && rest[0] !== '/'))) { |
|
push({ type: 'star', value, output: '' }); |
|
continue; |
|
} |
|
|
|
const isBrace = state.braces > 0 && (prior.type === 'comma' || prior.type === 'brace'); |
|
const isExtglob = extglobs.length && (prior.type === 'pipe' || prior.type === 'paren'); |
|
if (!isStart && prior.type !== 'paren' && !isBrace && !isExtglob) { |
|
push({ type: 'star', value, output: '' }); |
|
continue; |
|
} |
|
|
|
// strip consecutive `/**/` |
|
while (rest.slice(0, 3) === '/**') { |
|
const after = input[state.index + 4]; |
|
if (after && after !== '/') { |
|
break; |
|
} |
|
rest = rest.slice(3); |
|
consume('/**', 3); |
|
} |
|
|
|
if (prior.type === 'bos' && eos()) { |
|
prev.type = 'globstar'; |
|
prev.value += value; |
|
prev.output = globstar(opts); |
|
state.output = prev.output; |
|
state.globstar = true; |
|
consume(value); |
|
continue; |
|
} |
|
|
|
if (prior.type === 'slash' && prior.prev.type !== 'bos' && !afterStar && eos()) { |
|
state.output = state.output.slice(0, -(prior.output + prev.output).length); |
|
prior.output = `(?:${prior.output}`; |
|
|
|
prev.type = 'globstar'; |
|
prev.output = globstar(opts) + (opts.strictSlashes ? ')' : '|$)'); |
|
prev.value += value; |
|
state.globstar = true; |
|
state.output += prior.output + prev.output; |
|
consume(value); |
|
continue; |
|
} |
|
|
|
if (prior.type === 'slash' && prior.prev.type !== 'bos' && rest[0] === '/') { |
|
const end = rest[1] !== void 0 ? '|$' : ''; |
|
|
|
state.output = state.output.slice(0, -(prior.output + prev.output).length); |
|
prior.output = `(?:${prior.output}`; |
|
|
|
prev.type = 'globstar'; |
|
prev.output = `${globstar(opts)}${SLASH_LITERAL}|${SLASH_LITERAL}${end})`; |
|
prev.value += value; |
|
|
|
state.output += prior.output + prev.output; |
|
state.globstar = true; |
|
|
|
consume(value + advance()); |
|
|
|
push({ type: 'slash', value: '/', output: '' }); |
|
continue; |
|
} |
|
|
|
if (prior.type === 'bos' && rest[0] === '/') { |
|
prev.type = 'globstar'; |
|
prev.value += value; |
|
prev.output = `(?:^|${SLASH_LITERAL}|${globstar(opts)}${SLASH_LITERAL})`; |
|
state.output = prev.output; |
|
state.globstar = true; |
|
consume(value + advance()); |
|
push({ type: 'slash', value: '/', output: '' }); |
|
continue; |
|
} |
|
|
|
// remove single star from output |
|
state.output = state.output.slice(0, -prev.output.length); |
|
|
|
// reset previous token to globstar |
|
prev.type = 'globstar'; |
|
prev.output = globstar(opts); |
|
prev.value += value; |
|
|
|
// reset output with globstar |
|
state.output += prev.output; |
|
state.globstar = true; |
|
consume(value); |
|
continue; |
|
} |
|
|
|
const token = { type: 'star', value, output: star }; |
|
|
|
if (opts.bash === true) { |
|
token.output = '.*?'; |
|
if (prev.type === 'bos' || prev.type === 'slash') { |
|
token.output = nodot + token.output; |
|
} |
|
push(token); |
|
continue; |
|
} |
|
|
|
if (prev && (prev.type === 'bracket' || prev.type === 'paren') && opts.regex === true) { |
|
token.output = value; |
|
push(token); |
|
continue; |
|
} |
|
|
|
if (state.index === state.start || prev.type === 'slash' || prev.type === 'dot') { |
|
if (prev.type === 'dot') { |
|
state.output += NO_DOT_SLASH; |
|
prev.output += NO_DOT_SLASH; |
|
|
|
} else if (opts.dot === true) { |
|
state.output += NO_DOTS_SLASH; |
|
prev.output += NO_DOTS_SLASH; |
|
|
|
} else { |
|
state.output += nodot; |
|
prev.output += nodot; |
|
} |
|
|
|
if (peek() !== '*') { |
|
state.output += ONE_CHAR; |
|
prev.output += ONE_CHAR; |
|
} |
|
} |
|
|
|
push(token); |
|
} |
|
|
|
while (state.brackets > 0) { |
|
if (opts.strictBrackets === true) throw new SyntaxError(syntaxError('closing', ']')); |
|
state.output = utils.escapeLast(state.output, '['); |
|
decrement('brackets'); |
|
} |
|
|
|
while (state.parens > 0) { |
|
if (opts.strictBrackets === true) throw new SyntaxError(syntaxError('closing', ')')); |
|
state.output = utils.escapeLast(state.output, '('); |
|
decrement('parens'); |
|
} |
|
|
|
while (state.braces > 0) { |
|
if (opts.strictBrackets === true) throw new SyntaxError(syntaxError('closing', '}')); |
|
state.output = utils.escapeLast(state.output, '{'); |
|
decrement('braces'); |
|
} |
|
|
|
if (opts.strictSlashes !== true && (prev.type === 'star' || prev.type === 'bracket')) { |
|
push({ type: 'maybe_slash', value: '', output: `${SLASH_LITERAL}?` }); |
|
} |
|
|
|
// rebuild the output if we had to backtrack at any point |
|
if (state.backtrack === true) { |
|
state.output = ''; |
|
|
|
for (const token of state.tokens) { |
|
state.output += token.output != null ? token.output : token.value; |
|
|
|
if (token.suffix) { |
|
state.output += token.suffix; |
|
} |
|
} |
|
} |
|
|
|
return state; |
|
}; |
|
|
|
/** |
|
* Fast paths for creating regular expressions for common glob patterns. |
|
* This can significantly speed up processing and has very little downside |
|
* impact when none of the fast paths match. |
|
*/ |
|
|
|
parse.fastpaths = (input, options) => { |
|
const opts = { ...options }; |
|
const max = typeof opts.maxLength === 'number' ? Math.min(MAX_LENGTH, opts.maxLength) : MAX_LENGTH; |
|
const len = input.length; |
|
if (len > max) { |
|
throw new SyntaxError(`Input length: ${len}, exceeds maximum allowed length: ${max}`); |
|
} |
|
|
|
input = REPLACEMENTS[input] || input; |
|
const win32 = utils.isWindows(options); |
|
|
|
// create constants based on platform, for windows or posix |
|
const { |
|
DOT_LITERAL, |
|
SLASH_LITERAL, |
|
ONE_CHAR, |
|
DOTS_SLASH, |
|
NO_DOT, |
|
NO_DOTS, |
|
NO_DOTS_SLASH, |
|
STAR, |
|
START_ANCHOR |
|
} = constants.globChars(win32); |
|
|
|
const nodot = opts.dot ? NO_DOTS : NO_DOT; |
|
const slashDot = opts.dot ? NO_DOTS_SLASH : NO_DOT; |
|
const capture = opts.capture ? '' : '?:'; |
|
const state = { negated: false, prefix: '' }; |
|
let star = opts.bash === true ? '.*?' : STAR; |
|
|
|
if (opts.capture) { |
|
star = `(${star})`; |
|
} |
|
|
|
const globstar = opts => { |
|
if (opts.noglobstar === true) return star; |
|
return `(${capture}(?:(?!${START_ANCHOR}${opts.dot ? DOTS_SLASH : DOT_LITERAL}).)*?)`; |
|
}; |
|
|
|
const create = str => { |
|
switch (str) { |
|
case '*': |
|
return `${nodot}${ONE_CHAR}${star}`; |
|
|
|
case '.*': |
|
return `${DOT_LITERAL}${ONE_CHAR}${star}`; |
|
|
|
case '*.*': |
|
return `${nodot}${star}${DOT_LITERAL}${ONE_CHAR}${star}`; |
|
|
|
case '*/*': |
|
return `${nodot}${star}${SLASH_LITERAL}${ONE_CHAR}${slashDot}${star}`; |
|
|
|
case '**': |
|
return nodot + globstar(opts); |
|
|
|
case '**/*': |
|
return `(?:${nodot}${globstar(opts)}${SLASH_LITERAL})?${slashDot}${ONE_CHAR}${star}`; |
|
|
|
case '**/*.*': |
|
return `(?:${nodot}${globstar(opts)}${SLASH_LITERAL})?${slashDot}${star}${DOT_LITERAL}${ONE_CHAR}${star}`; |
|
|
|
case '**/.*': |
|
return `(?:${nodot}${globstar(opts)}${SLASH_LITERAL})?${DOT_LITERAL}${ONE_CHAR}${star}`; |
|
|
|
default: { |
|
const match = /^(.*?)\.(\w+)$/.exec(str); |
|
if (!match) return; |
|
|
|
const source = create(match[1]); |
|
if (!source) return; |
|
|
|
return source + DOT_LITERAL + match[2]; |
|
} |
|
} |
|
}; |
|
|
|
const output = utils.removePrefix(input, state); |
|
let source = create(output); |
|
|
|
if (source && opts.strictSlashes !== true) { |
|
source += `${SLASH_LITERAL}?`; |
|
} |
|
|
|
return source; |
|
}; |
|
|
|
module.exports = parse;
|
|
|