Skip to main content
Module

std/path/_common/glob_to_reg_exp.ts

The Deno Standard Library
Go to Latest
File
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.// This module is browser compatible.
/** Options for {@linkcode globToRegExp}. */export interface GlobOptions { /** Extended glob syntax. * See https://www.linuxjournal.com/content/bash-extended-globbing. * * @default {true} */ extended?: boolean; /** Globstar syntax. * See https://www.linuxjournal.com/content/globstar-new-bash-globbing-option. * If false, `**` is treated like `*`. * * @default {true} */ globstar?: boolean; /** Whether globstar should be case-insensitive. */ caseInsensitive?: boolean;}
export type GlobToRegExpOptions = GlobOptions;
const regExpEscapeChars = [ "!", "$", "(", ")", "*", "+", ".", "=", "?", "[", "\\", "^", "{", "|",];const rangeEscapeChars = ["-", "\\", "]"];
export interface GlobConstants { sep: string; sepMaybe: string; seps: string[]; globstar: string; wildcard: string; escapePrefix: string;}
export function _globToRegExp( c: GlobConstants, glob: string, { extended = true, globstar: globstarOption = true, // os = osType, caseInsensitive = false, }: GlobToRegExpOptions = {},): RegExp { if (glob === "") { return /(?!)/; }
// Remove trailing separators. let newLength = glob.length; for (; newLength > 1 && c.seps.includes(glob[newLength - 1]!); newLength--); glob = glob.slice(0, newLength);
let regExpString = "";
// Terminates correctly. Trust that `j` is incremented every iteration. for (let j = 0; j < glob.length;) { let segment = ""; const groupStack: string[] = []; let inRange = false; let inEscape = false; let endsWithSep = false; let i = j;
// Terminates with `i` at the non-inclusive end of the current segment. for (; i < glob.length && !c.seps.includes(glob[i]!); i++) { if (inEscape) { inEscape = false; const escapeChars = inRange ? rangeEscapeChars : regExpEscapeChars; segment += escapeChars.includes(glob[i]!) ? `\\${glob[i]}` : glob[i]; continue; }
if (glob[i] === c.escapePrefix) { inEscape = true; continue; }
if (glob[i] === "[") { if (!inRange) { inRange = true; segment += "["; if (glob[i + 1] === "!") { i++; segment += "^"; } else if (glob[i + 1] === "^") { i++; segment += "\\^"; } continue; } else if (glob[i + 1] === ":") { let k = i + 1; let value = ""; while (glob[k + 1] !== undefined && glob[k + 1] !== ":") { value += glob[k + 1]; k++; } if (glob[k + 1] === ":" && glob[k + 2] === "]") { i = k + 2; if (value === "alnum") segment += "\\dA-Za-z"; else if (value === "alpha") segment += "A-Za-z"; else if (value === "ascii") segment += "\x00-\x7F"; else if (value === "blank") segment += "\t "; else if (value === "cntrl") segment += "\x00-\x1F\x7F"; else if (value === "digit") segment += "\\d"; else if (value === "graph") segment += "\x21-\x7E"; else if (value === "lower") segment += "a-z"; else if (value === "print") segment += "\x20-\x7E"; else if (value === "punct") { segment += "!\"#$%&'()*+,\\-./:;<=>?@[\\\\\\]^_‘{|}~"; } else if (value === "space") segment += "\\s\v"; else if (value === "upper") segment += "A-Z"; else if (value === "word") segment += "\\w"; else if (value === "xdigit") segment += "\\dA-Fa-f"; continue; } } }
if (glob[i] === "]" && inRange) { inRange = false; segment += "]"; continue; }
if (inRange) { if (glob[i] === "\\") { segment += `\\\\`; } else { segment += glob[i]; } continue; }
if ( glob[i] === ")" && groupStack.length > 0 && groupStack[groupStack.length - 1] !== "BRACE" ) { segment += ")"; const type = groupStack.pop()!; if (type === "!") { segment += c.wildcard; } else if (type !== "@") { segment += type; } continue; }
if ( glob[i] === "|" && groupStack.length > 0 && groupStack[groupStack.length - 1] !== "BRACE" ) { segment += "|"; continue; }
if (glob[i] === "+" && extended && glob[i + 1] === "(") { i++; groupStack.push("+"); segment += "(?:"; continue; }
if (glob[i] === "@" && extended && glob[i + 1] === "(") { i++; groupStack.push("@"); segment += "(?:"; continue; }
if (glob[i] === "?") { if (extended && glob[i + 1] === "(") { i++; groupStack.push("?"); segment += "(?:"; } else { segment += "."; } continue; }
if (glob[i] === "!" && extended && glob[i + 1] === "(") { i++; groupStack.push("!"); segment += "(?!"; continue; }
if (glob[i] === "{") { groupStack.push("BRACE"); segment += "(?:"; continue; }
if (glob[i] === "}" && groupStack[groupStack.length - 1] === "BRACE") { groupStack.pop(); segment += ")"; continue; }
if (glob[i] === "," && groupStack[groupStack.length - 1] === "BRACE") { segment += "|"; continue; }
if (glob[i] === "*") { if (extended && glob[i + 1] === "(") { i++; groupStack.push("*"); segment += "(?:"; } else { const prevChar = glob[i - 1]; let numStars = 1; while (glob[i + 1] === "*") { i++; numStars++; } const nextChar = glob[i + 1]; if ( globstarOption && numStars === 2 && [...c.seps, undefined].includes(prevChar) && [...c.seps, undefined].includes(nextChar) ) { segment += c.globstar; endsWithSep = true; } else { segment += c.wildcard; } } continue; }
segment += regExpEscapeChars.includes(glob[i]!) ? `\\${glob[i]}` : glob[i]; }
// Check for unclosed groups or a dangling backslash. if (groupStack.length > 0 || inRange || inEscape) { // Parse failure. Take all characters from this segment literally. segment = ""; for (const c of glob.slice(j, i)) { segment += regExpEscapeChars.includes(c) ? `\\${c}` : c; endsWithSep = false; } }
regExpString += segment; if (!endsWithSep) { regExpString += i < glob.length ? c.sep : c.sepMaybe; endsWithSep = true; }
// Terminates with `i` at the start of the next segment. while (c.seps.includes(glob[i]!)) i++;
// Check that the next value of `j` is indeed higher than the current value. if (!(i > j)) { throw new Error("Assertion failure: i > j (potential infinite loop)"); } j = i; }
regExpString = `^${regExpString}$`; return new RegExp(regExpString, caseInsensitive ? "i" : "");}