type CommandWrapper = "env" | "sudo";type CommandProgramContext = "exec" | "shell"; const MAX_COMMAND_SEGMENTS = 64; const SHELL_PROGRAMS = new Set(["sh", "bash", "zsh", "dash", "ash", "ksh", "fish"]);const WINDOWS_SHELL_PROGRAMS = new Set([ "cmd", "cmd.exe", "powershell", "powershell.exe", "pwsh", "pwsh.exe",]);const SHELL_OPTIONS_WITH_VALUE = new Set(["-o", "-O", "--rcfile", "--init-file"]);const SHELL_COMMAND_WRAPPERS = new Set(["builtin", "command", "exec"]);const SHELL_PRECOMMAND_MODIFIERS = new Set(["nocorrect", "noglob", "time"]);const POWERSHELL_SETUP_PROGRAMS = new Set(["pop-location", "push-location", "set-location"]);const POWERSHELL_FLAGS = new Set(["-mta", "-nologo", "-noninteractive", "-noprofile", "-sta"]);const POWERSHELL_OPTIONS_WITH_VALUE = new Set([ "-configurationname", "-executionpolicy", "-inputformat", "-outputformat", "-version", "-windowstyle", "-workingdirectory",]);const START_PROCESS_FLAGS = new Set([ "-confirm", "-debug", "-loaduserprofile", "-nonewwindow", "-passthru", "-usenewenvironment", "-verbose", "-wait", "-whatif",]);const START_PROCESS_OPTIONS_WITH_VALUE = new Set([ "-argumentlist", "-credential", "-environment", "-erroraction", "-errorvariable", "-informationaction", "-informationvariable", "-outbuffer", "-outvariable", "-pipelinevariable", "-progressaction", "-redirectstandarderror", "-redirectstandardinput", "-redirectstandardoutput", "-verb", "-warningaction", "-warningvariable", "-windowstyle", "-workingdirectory",]);const SKIPPABLE_SUDO_PROBES = new Set(["[", "[[", "test", "true"]);const NON_PROGRAM_PREFIX_CHARACTERS = "<>(){}[];|&$`#!%@:";const NON_PROGRAM_SUFFIX_CHARACTERS = "){]}`"; // These tokens describe shell syntax or shell-local control flow, not a useful// executable name. Falling back to "command" is less misleading than labels// such as "Ran if", "Ran [", or "Ran function".const NON_DESCRIPTIVE_SHELL_PROGRAMS = new Set([ "!", "#", ".", ":", "[", "[[", "alias", "and", "autoload", "begin", "bg", "bind", "bindkey", "break", "builtin", "caller", "case", "catch", "cd", "command", "compgen", "complete", "compopt", "continue", "coproc", "declare", "dirs", "disown", "do", "done", "elif", "else", "enable", "end", "esac", "eval", "exec", "exit", "export", "false", "fc", "fg", "fi", "finally", "for", "foreach", "function", "getopts", "history", "if", "in", "jobs", "let", "local", "logout", "mapfile", "nocorrect", "noglob", "not", "or", "popd", "pushd", "read", "readarray", "readonly", "repeat", "return", "select", "set", "setopt", "shift", "shopt", "source", "switch", "suspend", "test", "then", "time", "times", "trap", "try", "true", "type", "typeset", "ulimit", "umask", "unalias", "until", "unset", "unsetopt", "wait", "while",]); // Unlike setup builtins, these can make later segments part of control flow or// otherwise unreachable, so do not use a later program as the command label.const TERMINAL_SHELL_PROGRAMS = new Set([ "and", "begin", "break", "case", "catch", "continue", "coproc", "do", "done", "elif", "else", "end", "esac", "eval", "exec", "exit", "false", "fi", "finally", "for", "foreach", "function", "if", "in", "not", "or", "repeat", "return", "select", "switch", "then", "try", "until", "while",]); function shellCommandArgumentIndex(tokens: ReadonlyArray<string>, start: number): number | null { for (let index = start; index < tokens.length; index += 1) { const option = tokens[index]!; if (option === "--" || !option.startsWith("-")) return null; if (SHELL_OPTIONS_WITH_VALUE.has(option)) { index += 1; continue; } if (option === "--command" || /^-[a-zA-Z]*c[a-zA-Z]*$/.test(option)) return index + 1; } return null;} const COMMAND_WRAPPER_OPTIONS_WITH_VALUE: Record<CommandWrapper, ReadonlySet<string>> = { env: new Set(["-C", "--chdir", "-S", "--split-string", "-u", "--unset"]), sudo: new Set(["-C", "--close-from", "-D", "--chdir", "-g", "--group", "-u", "--user"]),}; const COMMAND_WRAPPER_FLAGS: Record<CommandWrapper, ReadonlySet<string>> = { env: new Set(["-0", "--null", "-i", "--ignore-environment", "--debug", "-v"]), sudo: new Set(["-A", "--askpass", "-b", "--background", "-E", "-H", "-i", "-n", "-S"]),}; function tokenizeShellCommand(command: string): string[] | null { const input = command.trim(); const tokens: string[] = []; let current = ""; let quote: '"' | "'" | null = null; let escaping = false; let inBackticks = false; let substitutionDepth = 0; let parameterExpansionDepth = 0; let tokenStarted = false; for (let index = 0; index < input.length; index += 1) { const character = input[index]!; if (escaping) { current += character; escaping = false; tokenStarted = true; continue; } if (character === "\\" && quote !== "'") { const nextCharacter = input[index + 1]; const isWindowsPath = quote === null && /^(?:[A-Za-z]:|\.{1,2})(?:\\[^\s]*)?$/u.test(current); if ( (quote === '"' || isWindowsPath) && nextCharacter !== undefined && nextCharacter !== '"' && nextCharacter !== "\\" && nextCharacter !== "$" && nextCharacter !== "`" && nextCharacter !== "\n" ) { current += character; tokenStarted = true; continue; } escaping = true; tokenStarted = true; continue; } if (inBackticks) { current += character; if (character === "`") inBackticks = false; tokenStarted = true; continue; } if (quote !== null) { if (character === quote) { quote = null; } else { current += character; } tokenStarted = true; continue; } if (character === "`") { current += character; inBackticks = true; tokenStarted = true; continue; } if (character === "$" && input[index + 1] === "{") { current += "${"; parameterExpansionDepth += 1; tokenStarted = true; index += 1; continue; } if (character === "{" && parameterExpansionDepth > 0) { current += character; parameterExpansionDepth += 1; tokenStarted = true; continue; } if (character === "}" && parameterExpansionDepth > 0) { current += character; parameterExpansionDepth -= 1; tokenStarted = true; continue; } if (character === "$" && input[index + 1] === "(") { current += "$("; substitutionDepth += 1; tokenStarted = true; index += 1; continue; } if (character === "(") { current += character; substitutionDepth += 1; tokenStarted = true; continue; } if (character === ")" && substitutionDepth > 0) { current += character; substitutionDepth -= 1; tokenStarted = true; continue; } if (character === '"' || character === "'") { quote = character; tokenStarted = true; continue; } if (/\s/u.test(character)) { if (substitutionDepth > 0 || parameterExpansionDepth > 0) { current += character; tokenStarted = true; continue; } if (tokenStarted) { tokens.push(current); current = ""; tokenStarted = false; } continue; } current += character; tokenStarted = true; } if ( quote !== null || escaping || inBackticks || substitutionDepth > 0 || parameterExpansionDepth > 0 ) { return null; } if (tokenStarted) tokens.push(current); return tokens;} type ShellCommandSplit = { readonly firstCommand: string; readonly remainingCommand: string | null; readonly separator: string | null;}; type Heredoc = { readonly delimiter: string; readonly stripTabs: boolean;}; type ShellSeparator = { readonly index: number; readonly length: number;}; type ShellCommentRange = { readonly start: number; readonly end: number;}; function commandWithoutShellComments( command: string, end: number, comments: ReadonlyArray<ShellCommentRange>,): string { let result = ""; let cursor = 0; for (const comment of comments) { if (comment.start >= end) break; result += command.slice(cursor, comment.start); cursor = Math.min(comment.end, end); } return result + command.slice(cursor, end);} function readHeredocDelimiter( command: string, start: number, stripTabs: boolean,): { readonly heredoc: Heredoc; readonly end: number } | null { let index = start; while (command[index] === " " || command[index] === "\t") index += 1; let delimiter = ""; let quote: '"' | "'" | null = null; let escaping = false; for (; index < command.length; index += 1) { const character = command[index]!; if (escaping) { delimiter += character; escaping = false; continue; } if (character === "\\" && quote !== "'") { escaping = true; continue; } if (quote !== null) { if (character === quote) quote = null; else delimiter += character; continue; } if (character === '"' || character === "'") { quote = character; continue; } if (/\s/u.test(character) || ";&|<>()".includes(character)) break; delimiter += character; } if (!delimiter || quote !== null || escaping) return null; return { heredoc: { delimiter, stripTabs }, end: index };} function commandAfterHeredocs( command: string, start: number, heredocs: ReadonlyArray<Heredoc>,): string | null { let cursor = start; for (const heredoc of heredocs) { let foundDelimiter = false; while (cursor <= command.length) { const newlineIndex = command.indexOf("\n", cursor); const lineEnd = newlineIndex === -1 ? command.length : newlineIndex; const line = command.slice(cursor, lineEnd).replace(/\r$/u, ""); const comparableLine = heredoc.stripTabs ? line.replace(/^\t+/u, "") : line; cursor = newlineIndex === -1 ? command.length : newlineIndex + 1; if (comparableLine === heredoc.delimiter) { foundDelimiter = true; break; } if (newlineIndex === -1) break; } if (!foundDelimiter) return null; } return command.slice(cursor).trim() || null;} function splitFirstShellCommand(command: string): ShellCommandSplit { let quote: '"' | "'" | null = null; let powerShellHereStringQuote: '"' | "'" | null = null; let escaping = false; let inBackticks = false; let inComment = false; let substitutionDepth = 0; let parameterExpansionDepth = 0; const heredocs: Heredoc[] = []; const comments: ShellCommentRange[] = []; let commentStart = 0; let separatorBeforeHeredocs: ShellSeparator | null = null; for (let index = 0; index < command.length; index += 1) { const character = command[index]!; if (powerShellHereStringQuote !== null) { if ( character === powerShellHereStringQuote && command[index + 1] === "@" && (index === 0 || command[index - 1] === "\n") ) { powerShellHereStringQuote = null; index += 1; } continue; } if (inComment) { if (character !== "\n") continue; inComment = false; comments.push({ start: commentStart, end: index }); } if (escaping) { escaping = false; continue; } if (character === "\\" && quote !== "'") { escaping = true; continue; } if (inBackticks) { if (character === "`") inBackticks = false; continue;Lines 1–500 of 1369. Load more
type CommandWrapper = "env" | "sudo"; type CommandProgramContext = "exec" | "shell"; const MAX_COMMAND_SEGMENTS = 64; const SHELL_PROGRAMS = new Set(["sh", "bash", "zsh", "dash", "ash", "ksh", "fish"]); const WINDOWS_SHELL_PROGRAMS = new Set([ "cmd", "cmd.exe", "powershell", "powershell.exe", "pwsh", "pwsh.exe", ]); const SHELL_OPTIONS_WITH_VALUE = new Set(["-o", "-O", "--rcfile", "--init-file"]); const SHELL_COMMAND_WRAPPERS = new Set(["builtin", "command", "exec"]); const SHELL_PRECOMMAND_MODIFIERS = new Set(["nocorrect", "noglob", "time"]); const POWERSHELL_SETUP_PROGRAMS = new Set(["pop-location", "push-location", "set-location"]); const POWERSHELL_FLAGS = new Set(["-mta", "-nologo", "-noninteractive", "-noprofile", "-sta"]); const POWERSHELL_OPTIONS_WITH_VALUE = new Set([ "-configurationname", "-executionpolicy", "-inputformat", "-outputformat", "-version", "-windowstyle", "-workingdirectory", ]); const START_PROCESS_FLAGS = new Set([ "-confirm", "-debug", "-loaduserprofile", "-nonewwindow", "-passthru", "-usenewenvironment", "-verbose", "-wait", "-whatif", ]); const START_PROCESS_OPTIONS_WITH_VALUE = new Set([ "-argumentlist", "-credential", "-environment", "-erroraction", "-errorvariable", "-informationaction", "-informationvariable", "-outbuffer", "-outvariable", "-pipelinevariable", "-progressaction", "-redirectstandarderror", "-redirectstandardinput", "-redirectstandardoutput", "-verb", "-warningaction", "-warningvariable", "-windowstyle", "-workingdirectory", ]); const SKIPPABLE_SUDO_PROBES = new Set(["[", "[[", "test", "true"]); const NON_PROGRAM_PREFIX_CHARACTERS = "<>(){}[];|&$`#!%@:"; const NON_PROGRAM_SUFFIX_CHARACTERS = "){]}`"; // These tokens describe shell syntax or shell-local control flow, not a useful // executable name. Falling back to "command" is less misleading than labels // such as "Ran if", "Ran [", or "Ran function". const NON_DESCRIPTIVE_SHELL_PROGRAMS = new Set([ "!", "#", ".", ":", "[", "[[", "alias", "and", "autoload", "begin", "bg", "bind", "bindkey", "break", "builtin", "caller", "case", "catch", "cd", "command", "compgen", "complete", "compopt", "continue", "coproc", "declare", "dirs", "disown", "do", "done", "elif", "else", "enable", "end", "esac", "eval", "exec", "exit", "export", "false", "fc", "fg", "fi", "finally", "for", "foreach", "function", "getopts", "history", "if", "in", "jobs", "let", "local", "logout", "mapfile", "nocorrect", "noglob", "not", "or", "popd", "pushd", "read", "readarray", "readonly", "repeat", "return", "select", "set", "setopt", "shift", "shopt", "source", "switch", "suspend", "test", "then", "time", "times", "trap", "try", "true", "type", "typeset", "ulimit", "umask", "unalias", "until", "unset", "unsetopt", "wait", "while", ]); // Unlike setup builtins, these can make later segments part of control flow or // otherwise unreachable, so do not use a later program as the command label. const TERMINAL_SHELL_PROGRAMS = new Set([ "and", "begin", "break", "case", "catch", "continue", "coproc", "do", "done", "elif", "else", "end", "esac", "eval", "exec", "exit", "false", "fi", "finally", "for", "foreach", "function", "if", "in", "not", "or", "repeat", "return", "select", "switch", "then", "try", "until", "while", ]); function shellCommandArgumentIndex(tokens: ReadonlyArray<string>, start: number): number | null { for (let index = start; index < tokens.length; index += 1) { const option = tokens[index]!; if (option === "--" || !option.startsWith("-")) return null; if (SHELL_OPTIONS_WITH_VALUE.has(option)) { index += 1; continue; } if (option === "--command" || /^-[a-zA-Z]*c[a-zA-Z]*$/.test(option)) return index + 1; } return null; } const COMMAND_WRAPPER_OPTIONS_WITH_VALUE: Record<CommandWrapper, ReadonlySet<string>> = { env: new Set(["-C", "--chdir", "-S", "--split-string", "-u", "--unset"]), sudo: new Set(["-C", "--close-from", "-D", "--chdir", "-g", "--group", "-u", "--user"]), }; const COMMAND_WRAPPER_FLAGS: Record<CommandWrapper, ReadonlySet<string>> = { env: new Set(["-0", "--null", "-i", "--ignore-environment", "--debug", "-v"]), sudo: new Set(["-A", "--askpass", "-b", "--background", "-E", "-H", "-i", "-n", "-S"]), }; function tokenizeShellCommand(command: string): string[] | null { const input = command.trim(); const tokens: string[] = []; let current = ""; let quote: '"' | "'" | null = null; let escaping = false; let inBackticks = false; let substitutionDepth = 0; let parameterExpansionDepth = 0; let tokenStarted = false; for (let index = 0; index < input.length; index += 1) { const character = input[index]!; if (escaping) { current += character; escaping = false; tokenStarted = true; continue; } if (character === "\\" && quote !== "'") { const nextCharacter = input[index + 1]; const isWindowsPath = quote === null && /^(?:[A-Za-z]:|\.{1,2})(?:\\[^\s]*)?$/u.test(current); if ( (quote === '"' || isWindowsPath) && nextCharacter !== undefined && nextCharacter !== '"' && nextCharacter !== "\\" && nextCharacter !== "$" && nextCharacter !== "`" && nextCharacter !== "\n" ) { current += character; tokenStarted = true; continue; } escaping = true; tokenStarted = true; continue; } if (inBackticks) { current += character; if (character === "`") inBackticks = false; tokenStarted = true; continue; } if (quote !== null) { if (character === quote) { quote = null; } else { current += character; } tokenStarted = true; continue; } if (character === "`") { current += character; inBackticks = true; tokenStarted = true; continue; } if (character === "$" && input[index + 1] === "{") { current += "${"; parameterExpansionDepth += 1; tokenStarted = true; index += 1; continue; } if (character === "{" && parameterExpansionDepth > 0) { current += character; parameterExpansionDepth += 1; tokenStarted = true; continue; } if (character === "}" && parameterExpansionDepth > 0) { current += character; parameterExpansionDepth -= 1; tokenStarted = true; continue; } if (character === "$" && input[index + 1] === "(") { current += "$("; substitutionDepth += 1; tokenStarted = true; index += 1; continue; } if (character === "(") { current += character; substitutionDepth += 1; tokenStarted = true; continue; } if (character === ")" && substitutionDepth > 0) { current += character; substitutionDepth -= 1; tokenStarted = true; continue; } if (character === '"' || character === "'") { quote = character; tokenStarted = true; continue; } if (/\s/u.test(character)) { if (substitutionDepth > 0 || parameterExpansionDepth > 0) { current += character; tokenStarted = true; continue; } if (tokenStarted) { tokens.push(current); current = ""; tokenStarted = false; } continue; } current += character; tokenStarted = true; } if ( quote !== null || escaping || inBackticks || substitutionDepth > 0 || parameterExpansionDepth > 0 ) { return null; } if (tokenStarted) tokens.push(current); return tokens; } type ShellCommandSplit = { readonly firstCommand: string; readonly remainingCommand: string | null; readonly separator: string | null; }; type Heredoc = { readonly delimiter: string; readonly stripTabs: boolean; }; type ShellSeparator = { readonly index: number; readonly length: number; }; type ShellCommentRange = { readonly start: number; readonly end: number; }; function commandWithoutShellComments( command: string, end: number, comments: ReadonlyArray<ShellCommentRange>, ): string { let result = ""; let cursor = 0; for (const comment of comments) { if (comment.start >= end) break; result += command.slice(cursor, comment.start); cursor = Math.min(comment.end, end); } return result + command.slice(cursor, end); } function readHeredocDelimiter( command: string, start: number, stripTabs: boolean, ): { readonly heredoc: Heredoc; readonly end: number } | null { let index = start; while (command[index] === " " || command[index] === "\t") index += 1; let delimiter = ""; let quote: '"' | "'" | null = null; let escaping = false; for (; index < command.length; index += 1) { const character = command[index]!; if (escaping) { delimiter += character; escaping = false; continue; } if (character === "\\" && quote !== "'") { escaping = true; continue; } if (quote !== null) { if (character === quote) quote = null; else delimiter += character; continue; } if (character === '"' || character === "'") { quote = character; continue; } if (/\s/u.test(character) || ";&|<>()".includes(character)) break; delimiter += character; } if (!delimiter || quote !== null || escaping) return null; return { heredoc: { delimiter, stripTabs }, end: index }; } function commandAfterHeredocs( command: string, start: number, heredocs: ReadonlyArray<Heredoc>, ): string | null { let cursor = start; for (const heredoc of heredocs) { let foundDelimiter = false; while (cursor <= command.length) { const newlineIndex = command.indexOf("\n", cursor); const lineEnd = newlineIndex === -1 ? command.length : newlineIndex; const line = command.slice(cursor, lineEnd).replace(/\r$/u, ""); const comparableLine = heredoc.stripTabs ? line.replace(/^\t+/u, "") : line; cursor = newlineIndex === -1 ? command.length : newlineIndex + 1; if (comparableLine === heredoc.delimiter) { foundDelimiter = true; break; } if (newlineIndex === -1) break; } if (!foundDelimiter) return null; } return command.slice(cursor).trim() || null; } function splitFirstShellCommand(command: string): ShellCommandSplit { let quote: '"' | "'" | null = null; let powerShellHereStringQuote: '"' | "'" | null = null; let escaping = false; let inBackticks = false; let inComment = false; let substitutionDepth = 0; let parameterExpansionDepth = 0; const heredocs: Heredoc[] = []; const comments: ShellCommentRange[] = []; let commentStart = 0; let separatorBeforeHeredocs: ShellSeparator | null = null; for (let index = 0; index < command.length; index += 1) { const character = command[index]!; if (powerShellHereStringQuote !== null) { if ( character === powerShellHereStringQuote && command[index + 1] === "@" && (index === 0 || command[index - 1] === "\n") ) { powerShellHereStringQuote = null; index += 1; } continue; } if (inComment) { if (character !== "\n") continue; inComment = false; comments.push({ start: commentStart, end: index }); } if (escaping) { escaping = false; continue; } if (character === "\\" && quote !== "'") { escaping = true; continue; } if (inBackticks) { if (character === "`") inBackticks = false; continue;