diff --git a/scripts/lib/shell-substitution.js b/scripts/lib/shell-substitution.js index 5b747f0d0..efe6a9be1 100644 --- a/scripts/lib/shell-substitution.js +++ b/scripts/lib/shell-substitution.js @@ -120,23 +120,21 @@ function escapesActive(quotes) { } /** - * Toggle quote state for the quote character at `source[i]`. - * Returns true when it opened or closed a quote. Callers consume escapes + * Quote state after the quote character at `source[i]`, as a new object, or + * null when it neither opens nor closes a quote. Callers consume escapes * first, so a quote after a backslash here still closes (`'C:\'`, `"C:\\"`), * while `\'` inside `$'...'` is consumed before it can close the string. */ function toggleQuote(source, i, quotes) { const ch = source[i]; if (ch === "'" && !quotes.inDouble) { - quotes.inSingle = !quotes.inSingle; - quotes.ansiC = quotes.inSingle && opensAnsiC(source, i); - return true; + const inSingle = !quotes.inSingle; + return { ...quotes, inSingle, ansiC: inSingle && opensAnsiC(source, i) }; } if (ch === '"' && !quotes.inSingle) { - quotes.inDouble = !quotes.inDouble; - return true; + return { ...quotes, inDouble: !quotes.inDouble }; } - return false; + return null; } /** @@ -144,7 +142,7 @@ function toggleQuote(source, i, quotes) { * is at `start`, honoring quotes and escapes inside the span. */ function findParenSpanEnd(source, start) { - const quotes = { inSingle: false, inDouble: false, ansiC: false }; + let quotes = { inSingle: false, inDouble: false, ansiC: false }; let depth = 1; let i = start; while (i < source.length && depth > 0) { @@ -153,7 +151,10 @@ function findParenSpanEnd(source, start) { i += 2; continue; } - if (!toggleQuote(source, i, quotes) && !quotes.inSingle && !quotes.inDouble) { + const toggled = toggleQuote(source, i, quotes); + if (toggled) { + quotes = toggled; + } else if (!quotes.inSingle && !quotes.inDouble) { depth += parenDelta(inner); } i += 1; @@ -179,17 +180,19 @@ function findBacktickEnd(source, start) { /** * Top-level scan step shared by the group extractors: escapes, quote state, - * `$(...)` and backtick spans. Returns the last index consumed so a `for` - * loop's `i++` resumes after it, or -1 when the caller must inspect `source[i]`. + * `$(...)` and backtick spans. Returns the quote state after `source[i]` and + * `skipped`, the last index consumed so a `for` loop's `i++` resumes after it, + * or -1 when the caller must inspect `source[i]`. */ function skipOuterSpan(source, i, quotes) { const ch = source[i]; - if (ch === '\\' && escapesActive(quotes)) return i + 1; - if (toggleQuote(source, i, quotes)) return i; - if (quotes.inSingle || quotes.inDouble) return i; - if (ch === '$' && source[i + 1] === '(') return findParenSpanEnd(source, i + 2) - 1; - if (ch === '`') return findBacktickEnd(source, i + 1); - return -1; + if (ch === '\\' && escapesActive(quotes)) return { skipped: i + 1, quotes }; + const toggled = toggleQuote(source, i, quotes); + if (toggled) return { skipped: i, quotes: toggled }; + if (quotes.inSingle || quotes.inDouble) return { skipped: i, quotes }; + if (ch === '$' && source[i + 1] === '(') return { skipped: findParenSpanEnd(source, i + 2) - 1, quotes }; + if (ch === '`') return { skipped: findBacktickEnd(source, i + 1), quotes }; + return { skipped: -1, quotes }; } /** @@ -197,7 +200,7 @@ function skipOuterSpan(source, i, quotes) { * the body text and the index of the closing paren (or where input ran out). */ function readSubshellBody(source, start) { - const quotes = { inSingle: false, inDouble: false, ansiC: false }; + let quotes = { inSingle: false, inDouble: false, ansiC: false }; let depth = 1; let body = ''; let i = start; @@ -210,7 +213,10 @@ function readSubshellBody(source, start) { i = Math.min(i + 2, source.length); continue; } - if (!toggleQuote(source, i, quotes) && !quotes.inSingle && !quotes.inDouble) { + const toggled = toggleQuote(source, i, quotes); + if (toggled) { + quotes = toggled; + } else if (!quotes.inSingle && !quotes.inDouble) { depth += parenDelta(inner); if (depth === 0) break; } @@ -240,12 +246,13 @@ function readSubshellBody(source, start) { function extractSubshellGroups(input) { const source = String(input || ''); const groups = []; - const quotes = { inSingle: false, inDouble: false, ansiC: false }; + let quotes = { inSingle: false, inDouble: false, ansiC: false }; for (let i = 0; i < source.length; i++) { - const skipped = skipOuterSpan(source, i, quotes); - if (skipped !== -1) { - i = skipped; + const step = skipOuterSpan(source, i, quotes); + quotes = step.quotes; + if (step.skipped !== -1) { + i = step.skipped; continue; } if (source[i] !== '(') continue; @@ -285,7 +292,7 @@ function opensBraceGroup(source, i) { * the body text and the index of the closing brace (or where input ran out). */ function readBraceBody(source, start) { - const quotes = { inSingle: false, inDouble: false, ansiC: false }; + let quotes = { inSingle: false, inDouble: false, ansiC: false }; let depth = 1; let body = ''; let i = start; @@ -297,7 +304,9 @@ function readBraceBody(source, start) { i = Math.min(i + 2, source.length); continue; } - if (toggleQuote(source, i, quotes) || quotes.inSingle || quotes.inDouble) { + const toggled = toggleQuote(source, i, quotes); + if (toggled) quotes = toggled; + if (toggled || quotes.inSingle || quotes.inDouble) { body += inner; i += 1; continue; @@ -347,12 +356,13 @@ function readBraceBody(source, start) { function extractBraceGroups(input) { const source = String(input || ''); const groups = []; - const quotes = { inSingle: false, inDouble: false, ansiC: false }; + let quotes = { inSingle: false, inDouble: false, ansiC: false }; for (let i = 0; i < source.length; i++) { - const skipped = skipOuterSpan(source, i, quotes); - if (skipped !== -1) { - i = skipped; + const step = skipOuterSpan(source, i, quotes); + quotes = step.quotes; + if (step.skipped !== -1) { + i = step.skipped; continue; } if (source[i] === '(') {