function isWhitespace(ch) { return ch === " " || ch === "\t" || ch === "\n" || ch === "\r"; } function splitPipes(input) { const parts = []; let current = ""; let quote = null; for (let i = 0; i < input.length; i++) { const ch = input[i]; if (quote) { if (ch === "\\") { const next = input[i + 1]; if (next) { current += ch + next; i++; continue; } } current += ch; if (ch === quote) { quote = null; } continue; } if (ch === '"' || ch === "'") { quote = ch; current += ch; continue; } if (ch === "|") { parts.push(current.trim()); current = ""; continue; } current += ch; } if (quote) throw new Error("Unclosed quote"); if (current.trim().length > 0) parts.push(current.trim()); return parts; } function tokenizeCommand(input) { const tokens = []; let current = ""; let quote = null; const push = () => { if (current.length > 0) tokens.push(current); current = ""; }; for (let i = 0; i < input.length; i++) { const ch = input[i]; if (quote) { if (quote === "'") { if (ch === "\\" && input[i + 1] === quote) { current += quote; i++; continue; } if (ch === quote) { quote = null; continue; } current += ch; continue; } // Double-quoted mode: preserve unknown escapes (\n, \t, etc) while // unescaping only shell-like quote/backslash escapes. if (ch === "\\") { const next = input[i + 1]; if (next === '"' || next === "\\" || next === "$" || next === "`") { current += next; i++; continue; } if (next === "\n") { i++; continue; } current += ch; continue; } if (ch === quote) { quote = null; continue; } current += ch; continue; } if (ch === '"' || ch === "'") { quote = ch; continue; } if (isWhitespace(ch)) { push(); continue; } current += ch; } if (quote) throw new Error("Unclosed quote"); push(); return tokens; } function parseArgs(tokens) { const args = { _: [] }; for (let i = 0; i < tokens.length; i++) { const tok = tokens[i]; if (tok.startsWith("--")) { const eq = tok.indexOf("="); if (eq !== -1) { const key = tok.slice(2, eq); const value = tok.slice(eq + 1); args[key] = value; continue; } const key = tok.slice(2); const next = tokens[i + 1]; if (!next || next.startsWith("--")) { args[key] = true; continue; } args[key] = next; i++; continue; } args._.push(tok); } return args; } export function parsePipeline(input) { const stages = splitPipes(input); if (stages.length === 0) throw new Error("Empty pipeline"); return stages.map((stage) => { const tokens = tokenizeCommand(stage); if (tokens.length === 0) throw new Error("Empty command stage"); const name = tokens[0]; const args = parseArgs(tokens.slice(1)); return { name, args, raw: stage }; }); }