'use strict'; // $'...' quoting, in which a backslash also escapes the closing quote const ANSI_C_QUOTED_RE = /\$'((?:\\[\s\S]|[^'\\])*)'/y; const ANSI_C_ESCAPES = { a: '\x07', b: '\b', e: '\x1b', E: '\x1b', f: '\f', n: '\n', r: '\r', t: '\t', v: '\v' }; // The text of $'...' quoting with its escapes decoded, as Bash decodes them. function decodeAnsiC(text) { return text.replace(/\\(x[0-9A-Fa-f]{1,2}|u[0-9A-Fa-f]{1,4}|U[0-9A-Fa-f]{1,8}|[0-7]{1,3}|c[\s\S]|[\s\S])/g, (escape, code) => { if ('xuU'.includes(code[0]) && code.length > 1) { const value = parseInt(code.slice(1), 16); return value <= 0x10ffff ? String.fromCodePoint(value) : escape; } if (/^[0-7]/.test(code)) return String.fromCharCode(parseInt(code, 8) & 0xff); if (code[0] === 'c' && code.length > 1) return String.fromCharCode(code.charCodeAt(1) & 0x1f); return ANSI_C_ESCAPES[code] ?? (`\\'"?`.includes(code) ? code : escape); }); } // The shell word at `start`, up to an unquoted blank or operator character, with its quoting // removed, and whether any part of it was quoted. Null if a quote does not close. function readWord(command, start) { let word = ''; let quote = null; let quoted = false; let i = start; for (; i < command.length; i++) { const ch = command[i]; ANSI_C_QUOTED_RE.lastIndex = i; const ansiC = !quote && ch === '$' ? ANSI_C_QUOTED_RE.exec(command) : null; if (quote) { if (ch === quote) quote = null; else word += ch; } else if (ansiC) { // $'...' decodes its escapes, so $'E\x4fF' is EOF word += decodeAnsiC(ansiC[1]); quoted = true; i += ansiC[0].length - 1; } else if (ch === "'" || ch === '"') { quote = ch; quoted = true; } else if (ch === '$' && command[i + 1] === '"') { // $"..." is double quoting; the $ is not part of the word continue; } else if (ch === '\\' && i + 1 < command.length) { word += command[++i]; quoted = true; } else if (/[\s|&;()<>]/.test(ch)) { break; } else { word += ch; } } return quote ? null : { word, quoted, end: i }; } // A here-document opener at `index`, but not a here-string (<<<): `<<` or `<<-`, then the // delimiter, a shell word whose quotes are removed (`<<'EOF'`, `<, >&, 2>&1) are NOT treated as separators, and the * body of a here-document, which is data, is left out of the segments. */ function splitShellSegments(command) { const segments = []; let current = ''; let quote = null; // Here-documents opened on the current line, read after its newline. let heredocs = []; for (let i = 0; i < command.length; i++) { const ch = command[i]; // Inside quotes: handle escapes and closing quote if (quote) { if (ch === '\\' && i + 1 < command.length) { current += ch + command[i + 1]; i++; continue; } if (ch === quote) quote = null; current += ch; continue; } // Backslash escape outside quotes if (ch === '\\' && i + 1 < command.length) { current += ch + command[i + 1]; i++; continue; } // Opening quote if (ch === '"' || ch === "'") { quote = ch; current += ch; continue; } const next = command[i + 1] || ''; const prev = i > 0 ? command[i - 1] : ''; const spanEnd = literalSpanEnd(command, i, current === '' || /[\s;&|(]/.test(prev)); if (spanEnd !== -1) { current += command.slice(i, spanEnd); i = spanEnd - 1; continue; } const heredoc = ch === '<' && next === '<' ? heredocAt(command, i) : null; if (heredoc) { heredocs.push(heredoc); current += heredoc.text; i += heredoc.text.length - 1; continue; } // A newline ends a command, after the bodies of the here-documents opened on its line if (ch === '\n') { i = endLine(command, i, current, heredocs, segments) - 1; heredocs = []; current = ''; continue; } // && operator if (ch === '&' && next === '&') { if (current.trim()) segments.push(current.trim()); current = ''; i++; continue; } // || operator if (ch === '|' && next === '|') { if (current.trim()) segments.push(current.trim()); current = ''; i++; continue; } // ; separator if (ch === ';') { if (current.trim()) segments.push(current.trim()); current = ''; continue; } // Single & — but skip redirection patterns (&>, >&, digit>&) if (ch === '&' && next !== '&') { if (next === '>' || prev === '>') { current += ch; continue; } if (current.trim()) segments.push(current.trim()); current = ''; continue; } current += ch; } if (current.trim()) segments.push(current.trim()); return segments; } module.exports = { splitShellSegments };