1369 lines · 40.0 KB · TypeScript
RawDownload
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.