import { createRequire } from "node:module"; import type { Node, Parser, Tree } from "web-tree-sitter"; const require = createRequire(import.meta.url); let parser: Parser | undefined; let parserPromise: Promise | undefined; export function initializeBashParser(): void { parserPromise ??= createBashParser().then((created) => { parser = created; }); } export function extractBashCommand(command: string[]): [shell: string, script: string] | undefined { if (command.length !== 3) return undefined; const [shell, flag, script] = command as [string, string, string]; if (flag !== "-lc" && flag !== "-c") return undefined; const shellName = shell.replace(/\\/g, "/").split("/").pop()?.toLowerCase(); if (shellName !== "bash" && shellName !== "zsh" && shellName !== "sh") return undefined; return [shell, script]; } export function tryParseShell(shellLcArg: string): Tree | undefined { if (!parser) initializeBashParser(); return parser?.parse(shellLcArg) ?? undefined; } export function tryParseWordOnlyCommandsSequence(tree: Tree, src: string): string[][] | undefined { if (tree.rootNode.hasError) { return undefined; } const allowedKinds = new Set([ "program", "list", "pipeline", "command", "command_name", "word", "string", "string_content", "raw_string", "number", "concatenation", ]); const allowedPunctuation = new Set(["&&", "||", ";", "|", '"', "'"]); const commandNodes: Node[] = []; const stack: Node[] = [tree.rootNode]; while (stack.length > 0) { const node = stack.pop(); if (!node) continue; if (node.isNamed) { if (!allowedKinds.has(node.type)) return undefined; if (node.type === "command") commandNodes.push(node); } else { if ([...node.type].some((char) => "&;|".includes(char)) && !allowedPunctuation.has(node.type)) { return undefined; } if (!allowedPunctuation.has(node.type) && node.type.trim().length > 0) { return undefined; } } for (const child of node.children) { stack.push(child); } } commandNodes.sort((left, right) => left.startIndex - right.startIndex); const commands: string[][] = []; for (const node of commandNodes) { const parsed = parsePlainCommandFromNode(node, src); if (!parsed) return undefined; commands.push(parsed); } return commands; } export function parseShellLcPlainCommands(command: string[]): string[][] | undefined { const bash = extractBashCommand(command); if (!bash) return undefined; const [, script] = bash; const tree = tryParseShell(script); if (!tree) return undefined; return tryParseWordOnlyCommandsSequence(tree, script); } async function createBashParser(): Promise { try { const { Language, Parser } = await import("web-tree-sitter"); await Parser.init(); const language = await Language.load(require.resolve("tree-sitter-bash/tree-sitter-bash.wasm")); const parser = new Parser(); parser.setLanguage(language); return parser; } catch { return undefined; } } function parsePlainCommandFromNode(command: Node, src: string): string[] | undefined { if (command.type !== "command") return undefined; const words: string[] = []; for (const child of command.namedChildren) { switch (child.type) { case "command_name": { const wordNode = child.namedChild(0); if (!wordNode || wordNode.type !== "word") return undefined; words.push(textOf(wordNode, src)); break; } case "word": case "number": words.push(textOf(child, src)); break; case "string": { const parsed = parseDoubleQuotedString(child, src); if (parsed === undefined) return undefined; words.push(parsed); break; } case "raw_string": { const parsed = parseRawString(child, src); if (parsed === undefined) return undefined; words.push(parsed); break; } case "concatenation": { let concatenated = ""; for (const part of child.namedChildren) { switch (part.type) { case "word": case "number": concatenated += textOf(part, src); break; case "string": { const parsed = parseDoubleQuotedString(part, src); if (parsed === undefined) return undefined; concatenated += parsed; break; } case "raw_string": { const parsed = parseRawString(part, src); if (parsed === undefined) return undefined; concatenated += parsed; break; } default: return undefined; } } if (concatenated.length === 0) return undefined; words.push(concatenated); break; } default: return undefined; } } return words; } function parseDoubleQuotedString(node: Node, src: string): string | undefined { if (node.type !== "string") return undefined; for (const part of node.namedChildren) { if (part.type !== "string_content") return undefined; } const raw = textOf(node, src); if (!raw.startsWith('"') || !raw.endsWith('"')) return undefined; return raw.slice(1, -1); } function parseRawString(node: Node, src: string): string | undefined { if (node.type !== "raw_string") return undefined; const raw = textOf(node, src); if (!raw.startsWith("'") || !raw.endsWith("'")) return undefined; return raw.slice(1, -1); } function textOf(node: Node, _src: string): string { return node.text; }