diff --git a/src/infra/command-analysis/risks.ts b/src/infra/command-analysis/risks.ts index d33446223fb..9ed3bb48816 100644 --- a/src/infra/command-analysis/risks.ts +++ b/src/infra/command-analysis/risks.ts @@ -1,4 +1,10 @@ import { splitShellArgs } from "../../utils/shell-argv.js"; +import { + COMMAND_CARRIER_EXECUTABLES, + isEnvAssignmentToken, + resolveCarrierCommandArgv, + SOURCE_EXECUTABLES, +} from "../command-carriers.js"; import { unwrapKnownDispatchWrapperInvocation } from "../dispatch-wrapper-resolution.js"; import type { ExecCommandSegment } from "../exec-approvals-analysis.js"; import { normalizeExecutableToken } from "../exec-wrapper-resolution.js"; @@ -8,9 +14,7 @@ import { } from "../shell-wrapper-resolution.js"; import { detectInterpreterInlineEvalArgv, type InterpreterInlineEvalHit } from "./inline-eval.js"; -export const COMMAND_CARRIER_EXECUTABLES = new Set(["sudo", "doas", "env", "command", "builtin"]); - -export const SOURCE_EXECUTABLES = new Set([".", "source"]); +export { COMMAND_CARRIER_EXECUTABLES, resolveCarrierCommandArgv, SOURCE_EXECUTABLES }; export type CommandCarrierHit = { command: string; @@ -19,340 +23,6 @@ export type CommandCarrierHit = { export type CarriedShellBuiltinHit = { kind: "eval" } | { kind: "source"; command: string }; -const MAX_ENV_SPLIT_PAYLOAD_DEPTH = 32; - -const COMMAND_EXECUTING_OPTIONS = new Set(["-p"]); -const COMMAND_QUERY_OPTIONS = new Set(["-v", "-V"]); -const ENV_OPTIONS_WITH_VALUE = new Set([ - "-C", - "-S", - "-u", - "--argv0", - "--block-signal", - "--chdir", - "--default-signal", - "--ignore-signal", - "--split-string", - "--unset", -]); -const ENV_SPLIT_STRING_OPTIONS = new Set(["-S", "--split-string"]); -const ENV_STANDALONE_OPTIONS = new Set(["-0", "-i", "--ignore-environment", "--null"]); -const SUDO_OPTIONS_WITH_VALUE = new Set([ - "-C", - "-D", - "-g", - "-h", - "-p", - "-R", - "-T", - "-U", - "-u", - "--chdir", - "--close-from", - "--group", - "--host", - "--other-user", - "--prompt", - "--role", - "--type", - "--user", -]); -const SUDO_STANDALONE_OPTIONS = new Set([ - "-A", - "-b", - "-E", - "-H", - "-n", - "-P", - "-S", - "--askpass", - "--background", - "--login", - "--non-interactive", - "--preserve-env", - "--reset-home", - "--stdin", -]); -const SUDO_NON_EXEC_OPTIONS = new Set([ - "-K", - "-k", - "-l", - "-V", - "-v", - "-e", - "--edit", - "--help", - "--list", - "--remove-timestamp", - "--reset-timestamp", - "--validate", - "--version", -]); -const DOAS_OPTIONS_WITH_VALUE = new Set(["-a", "-C", "-u"]); -const DOAS_STANDALONE_OPTIONS = new Set(["-L", "-n", "-s"]); -const EXEC_OPTIONS_WITH_VALUE = new Set(["-a"]); -const EXEC_STANDALONE_OPTIONS = new Set(["-c", "-l"]); - -function isEnvAssignmentToken(token: string): boolean { - return /^[A-Za-z_][A-Za-z0-9_]*=.*$/u.test(token); -} - -function optionName(token: string): string { - return token.split("=", 1)[0] ?? token; -} - -type ParsedCarrierOption = { - name: string; - hasInlineValue: boolean; - inlineValue?: string; -}; - -function parseCarrierOptionToken( - token: string, - standaloneOptions: ReadonlySet, - optionsWithValue: ReadonlySet, - nonExecutingOptions: ReadonlySet = new Set(), -): ParsedCarrierOption[] | null { - if (token.startsWith("--")) { - const name = optionName(token); - if ( - standaloneOptions.has(name) || - optionsWithValue.has(name) || - nonExecutingOptions.has(name) - ) { - const valueDelimiter = token.indexOf("="); - return [ - { - name, - hasInlineValue: valueDelimiter >= 0, - inlineValue: valueDelimiter >= 0 ? token.slice(valueDelimiter + 1) : undefined, - }, - ]; - } - return null; - } - - if (!/^-[A-Za-z0-9]/u.test(token)) { - return null; - } - - const options: ParsedCarrierOption[] = []; - for (let index = 1; index < token.length; index += 1) { - const name = `-${token[index] ?? ""}`; - if (optionsWithValue.has(name)) { - options.push({ - name, - hasInlineValue: index < token.length - 1, - inlineValue: index < token.length - 1 ? token.slice(index + 1) : undefined, - }); - return options; - } - if (standaloneOptions.has(name) || nonExecutingOptions.has(name)) { - options.push({ name, hasInlineValue: false }); - continue; - } - return null; - } - return options.length > 0 ? options : null; -} - -function knownCarrierOptionConsumesNextValue( - options: readonly ParsedCarrierOption[], - optionsWithValue: ReadonlySet, - nonExecutingOptions: ReadonlySet = new Set(), -): boolean | null { - let consumesNextValue = false; - for (const option of options) { - if (nonExecutingOptions.has(option.name)) { - return null; - } - if (optionsWithValue.has(option.name)) { - consumesNextValue = !option.hasInlineValue; - } - } - return consumesNextValue; -} - -function findParsedCarrierOption( - options: readonly ParsedCarrierOption[], - names: ReadonlySet, -): ParsedCarrierOption | undefined { - return options.find((option) => names.has(option.name)); -} - -function resolveEnvSplitPayload( - payload: string, - trailingArgv: string[], - depth: number, -): string[] | null { - const innerArgv = splitShellArgs(payload); - if (!innerArgv || innerArgv.length === 0) { - return null; - } - const carriedArgv = [...innerArgv, ...trailingArgv]; - return resolveEnvCarriedArgv(["env", ...carriedArgv], depth + 1) ?? carriedArgv; -} - -function resolveEnvCarriedArgv(argv: string[], depth = 0): string[] | null { - if (depth > MAX_ENV_SPLIT_PAYLOAD_DEPTH || normalizeExecutableToken(argv[0] ?? "") !== "env") { - return null; - } - for (let index = 1; index < argv.length; index += 1) { - const token = argv[index] ?? ""; - if (!token) { - return null; - } - if (isEnvAssignmentToken(token)) { - continue; - } - if (token === "--") { - return argv.slice(index + 1); - } - if (token.startsWith("-")) { - const option = parseCarrierOptionToken(token, ENV_STANDALONE_OPTIONS, ENV_OPTIONS_WITH_VALUE); - if (!option) { - return null; - } - const splitStringOption = findParsedCarrierOption(option, ENV_SPLIT_STRING_OPTIONS); - if (splitStringOption) { - const payloadIndex = splitStringOption.inlineValue === undefined ? index + 1 : index; - const payload = splitStringOption.inlineValue ?? argv[payloadIndex]; - return typeof payload === "string" - ? resolveEnvSplitPayload(payload, argv.slice(payloadIndex + 1), depth) - : null; - } - const consumeNextValue = knownCarrierOptionConsumesNextValue(option, ENV_OPTIONS_WITH_VALUE); - if (consumeNextValue) { - index += 1; - } - continue; - } - return argv.slice(index); - } - return null; -} - -function resolveCommandBuiltinCarriedArgv(argv: string[]): string[] | null { - const executable = normalizeExecutableToken(argv[0] ?? ""); - if (executable !== "command" && executable !== "builtin") { - return null; - } - for (let index = 1; index < argv.length; index += 1) { - const token = argv[index] ?? ""; - if (token === "--") { - return argv.slice(index + 1); - } - if (!token.startsWith("-")) { - return argv.slice(index); - } - const normalized = optionName(token); - if (COMMAND_QUERY_OPTIONS.has(normalized)) { - return null; - } - if (!COMMAND_EXECUTING_OPTIONS.has(normalized)) { - return null; - } - } - return null; -} - -function resolveSudoLikeCarriedArgv(argv: string[]): string[] | null { - const executable = normalizeExecutableToken(argv[0] ?? ""); - const standaloneOptions = - executable === "sudo" - ? SUDO_STANDALONE_OPTIONS - : executable === "doas" - ? DOAS_STANDALONE_OPTIONS - : null; - const optionsWithValue = - executable === "sudo" - ? SUDO_OPTIONS_WITH_VALUE - : executable === "doas" - ? DOAS_OPTIONS_WITH_VALUE - : null; - if (!standaloneOptions || !optionsWithValue) { - return null; - } - for (let index = 1; index < argv.length; index += 1) { - const token = argv[index] ?? ""; - if (token === "--") { - return argv.slice(index + 1); - } - if (!token.startsWith("-")) { - return argv.slice(index); - } - const option = parseCarrierOptionToken( - token, - standaloneOptions, - optionsWithValue, - executable === "sudo" ? SUDO_NON_EXEC_OPTIONS : undefined, - ); - if (!option) { - return null; - } - const consumeNextValue = knownCarrierOptionConsumesNextValue( - option, - optionsWithValue, - executable === "sudo" ? SUDO_NON_EXEC_OPTIONS : undefined, - ); - if (consumeNextValue === null) { - return null; - } - if (consumeNextValue) { - index += 1; - } - continue; - } - return null; -} - -export function resolveCarrierCommandArgv( - argv: string[], - depth = 0, - options?: { includeExec?: boolean }, -): string[] | null { - const executable = normalizeExecutableToken(argv[0] ?? ""); - switch (executable) { - case "env": - return resolveEnvCarriedArgv(argv, depth); - case "command": - case "builtin": - return resolveCommandBuiltinCarriedArgv(argv); - case "sudo": - case "doas": - return resolveSudoLikeCarriedArgv(argv); - case "exec": - return options?.includeExec ? resolveExecCarriedArgv(argv) : null; - default: - return null; - } -} - -function resolveExecCarriedArgv(argv: string[]): string[] | null { - if (normalizeExecutableToken(argv[0] ?? "") !== "exec") { - return null; - } - for (let index = 1; index < argv.length; index += 1) { - const token = argv[index] ?? ""; - if (token === "--") { - return argv.slice(index + 1); - } - if (!token.startsWith("-")) { - return argv.slice(index); - } - const option = parseCarrierOptionToken(token, EXEC_STANDALONE_OPTIONS, EXEC_OPTIONS_WITH_VALUE); - if (!option) { - return null; - } - const consumeNextValue = knownCarrierOptionConsumesNextValue(option, EXEC_OPTIONS_WITH_VALUE); - if (consumeNextValue) { - index += 1; - } - continue; - } - return null; -} - function commandArgvKey(argv: readonly string[]): string { return argv.join("\0"); } diff --git a/src/infra/command-carriers.ts b/src/infra/command-carriers.ts new file mode 100644 index 00000000000..5e0da01c603 --- /dev/null +++ b/src/infra/command-carriers.ts @@ -0,0 +1,384 @@ +import { splitShellArgs } from "../utils/shell-argv.js"; +import { normalizeExecutableToken } from "./exec-wrapper-tokens.js"; + +export const COMMAND_CARRIER_EXECUTABLES = new Set(["sudo", "doas", "env", "command", "builtin"]); + +export const SOURCE_EXECUTABLES = new Set([".", "source"]); + +const MAX_ENV_SPLIT_PAYLOAD_DEPTH = 32; + +const COMMAND_EXECUTING_OPTIONS = new Set(["-p"]); +const COMMAND_QUERY_OPTIONS = new Set(["-v", "-V"]); +const ENV_OPTIONS_WITH_VALUE = new Set([ + "-C", + "-S", + "-s", + "-u", + "--argv0", + "--block-signal", + "--chdir", + "--default-signal", + "--ignore-signal", + "--split-string", + "--unset", +]); +const ENV_SPLIT_STRING_OPTIONS = new Set(["-S", "-s", "--split-string"]); +const ENV_STANDALONE_OPTIONS = new Set(["-0", "-i", "--ignore-environment", "--null"]); +const SUDO_OPTIONS_WITH_VALUE = new Set([ + "-C", + "-D", + "-g", + "-h", + "-p", + "-R", + "-T", + "-U", + "-u", + "--chdir", + "--close-from", + "--group", + "--host", + "--other-user", + "--prompt", + "--role", + "--type", + "--user", +]); +const SUDO_STANDALONE_OPTIONS = new Set([ + "-A", + "-b", + "-E", + "-H", + "-n", + "-P", + "-S", + "--askpass", + "--background", + "--login", + "--non-interactive", + "--preserve-env", + "--reset-home", + "--stdin", +]); +const SUDO_NON_EXEC_OPTIONS = new Set([ + "-K", + "-k", + "-l", + "-V", + "-v", + "-e", + "--edit", + "--help", + "--list", + "--remove-timestamp", + "--reset-timestamp", + "--validate", + "--version", +]); +const DOAS_OPTIONS_WITH_VALUE = new Set(["-a", "-C", "-u"]); +const DOAS_STANDALONE_OPTIONS = new Set(["-L", "-n", "-s"]); +const EXEC_OPTIONS_WITH_VALUE = new Set(["-a"]); +const EXEC_STANDALONE_OPTIONS = new Set(["-c", "-l"]); + +export function isEnvAssignmentToken(token: string): boolean { + return /^[A-Za-z_][A-Za-z0-9_]*=.*$/u.test(token); +} + +function optionName(token: string): string { + return token.split("=", 1)[0] ?? token; +} + +type ParsedCarrierOption = { + name: string; + hasInlineValue: boolean; + inlineValue?: string; +}; + +function parseCarrierOptionToken( + token: string, + standaloneOptions: ReadonlySet, + optionsWithValue: ReadonlySet, + nonExecutingOptions: ReadonlySet = new Set(), +): ParsedCarrierOption[] | null { + if (token.startsWith("--")) { + const name = optionName(token); + if ( + standaloneOptions.has(name) || + optionsWithValue.has(name) || + nonExecutingOptions.has(name) + ) { + const valueDelimiter = token.indexOf("="); + return [ + { + name, + hasInlineValue: valueDelimiter >= 0, + inlineValue: valueDelimiter >= 0 ? token.slice(valueDelimiter + 1) : undefined, + }, + ]; + } + return null; + } + + if (!/^-[A-Za-z0-9]/u.test(token)) { + return null; + } + + const options: ParsedCarrierOption[] = []; + for (let index = 1; index < token.length; index += 1) { + const name = `-${token[index] ?? ""}`; + if (optionsWithValue.has(name)) { + options.push({ + name, + hasInlineValue: index < token.length - 1, + inlineValue: index < token.length - 1 ? token.slice(index + 1) : undefined, + }); + return options; + } + if (standaloneOptions.has(name) || nonExecutingOptions.has(name)) { + options.push({ name, hasInlineValue: false }); + continue; + } + return null; + } + return options.length > 0 ? options : null; +} + +function knownCarrierOptionConsumesNextValue( + options: readonly ParsedCarrierOption[], + optionsWithValue: ReadonlySet, + nonExecutingOptions: ReadonlySet = new Set(), +): boolean | null { + let consumesNextValue = false; + for (const option of options) { + if (nonExecutingOptions.has(option.name)) { + return null; + } + if (optionsWithValue.has(option.name)) { + consumesNextValue = !option.hasInlineValue; + } + } + return consumesNextValue; +} + +function findParsedCarrierOption( + options: readonly ParsedCarrierOption[], + names: ReadonlySet, +): ParsedCarrierOption | undefined { + return options.find((option) => names.has(option.name)); +} + +function resolveEnvSplitPayload( + payload: string, + trailingArgv: string[], + depth: number, +): string[] | null { + const innerArgv = splitShellArgs(payload); + if (!innerArgv || innerArgv.length === 0) { + return null; + } + const carriedArgv = [...innerArgv, ...trailingArgv]; + return resolveEnvCarriedArgv(["env", ...carriedArgv], depth + 1) ?? carriedArgv; +} + +export type ParsedEnvInvocationPrelude = { + assignmentKeys: string[]; + commandIndex: number; + splitArgv?: string[]; + usesModifiers: boolean; +}; + +export function parseEnvInvocationPrelude( + argv: string[], + depth = 0, +): ParsedEnvInvocationPrelude | null { + if (depth > MAX_ENV_SPLIT_PAYLOAD_DEPTH || normalizeExecutableToken(argv[0] ?? "") !== "env") { + return null; + } + let usesModifiers = false; + const assignmentKeys: string[] = []; + for (let index = 1; index < argv.length; index += 1) { + const token = argv[index] ?? ""; + if (!token) { + return null; + } + if (isEnvAssignmentToken(token)) { + usesModifiers = true; + const delimiter = token.indexOf("="); + if (delimiter > 0) { + assignmentKeys.push(token.slice(0, delimiter)); + } + continue; + } + if (token === "--" || token === "-") { + return index + 1 < argv.length + ? { assignmentKeys, commandIndex: index + 1, usesModifiers } + : null; + } + if (token.startsWith("-")) { + const option = parseCarrierOptionToken(token, ENV_STANDALONE_OPTIONS, ENV_OPTIONS_WITH_VALUE); + if (!option) { + return null; + } + usesModifiers = true; + const splitStringOption = findParsedCarrierOption(option, ENV_SPLIT_STRING_OPTIONS); + if (splitStringOption) { + const payloadIndex = splitStringOption.inlineValue === undefined ? index + 1 : index; + const payload = splitStringOption.inlineValue ?? argv[payloadIndex]; + const trailingIndex = payloadIndex + 1; + const splitArgv = + typeof payload === "string" + ? resolveEnvSplitPayload(payload, argv.slice(trailingIndex), depth) + : null; + return splitArgv + ? { + assignmentKeys, + commandIndex: trailingIndex, + splitArgv, + usesModifiers, + } + : null; + } + const consumeNextValue = knownCarrierOptionConsumesNextValue(option, ENV_OPTIONS_WITH_VALUE); + if (consumeNextValue) { + index += 1; + } + continue; + } + return { assignmentKeys, commandIndex: index, usesModifiers }; + } + return null; +} + +export function envInvocationUsesModifiers(argv: string[]): boolean { + const parsed = parseEnvInvocationPrelude(argv); + return parsed?.usesModifiers ?? normalizeExecutableToken(argv[0] ?? "") === "env"; +} + +export function unwrapEnvInvocation(argv: string[]): string[] | null { + const parsed = parseEnvInvocationPrelude(argv); + return parsed ? (parsed.splitArgv ?? argv.slice(parsed.commandIndex)) : null; +} + +export function resolveEnvCarriedArgv(argv: string[], depth = 0): string[] | null { + const parsed = parseEnvInvocationPrelude(argv, depth); + return parsed ? (parsed.splitArgv ?? argv.slice(parsed.commandIndex)) : null; +} + +function resolveCommandBuiltinCarriedArgv(argv: string[]): string[] | null { + const executable = normalizeExecutableToken(argv[0] ?? ""); + if (executable !== "command" && executable !== "builtin") { + return null; + } + for (let index = 1; index < argv.length; index += 1) { + const token = argv[index] ?? ""; + if (token === "--") { + return argv.slice(index + 1); + } + if (!token.startsWith("-")) { + return argv.slice(index); + } + const normalized = optionName(token); + if (COMMAND_QUERY_OPTIONS.has(normalized)) { + return null; + } + if (!COMMAND_EXECUTING_OPTIONS.has(normalized)) { + return null; + } + } + return null; +} + +function resolveSudoLikeCarriedArgv(argv: string[]): string[] | null { + const executable = normalizeExecutableToken(argv[0] ?? ""); + const standaloneOptions = + executable === "sudo" + ? SUDO_STANDALONE_OPTIONS + : executable === "doas" + ? DOAS_STANDALONE_OPTIONS + : null; + const optionsWithValue = + executable === "sudo" + ? SUDO_OPTIONS_WITH_VALUE + : executable === "doas" + ? DOAS_OPTIONS_WITH_VALUE + : null; + if (!standaloneOptions || !optionsWithValue) { + return null; + } + for (let index = 1; index < argv.length; index += 1) { + const token = argv[index] ?? ""; + if (token === "--") { + return argv.slice(index + 1); + } + if (!token.startsWith("-")) { + return argv.slice(index); + } + const option = parseCarrierOptionToken( + token, + standaloneOptions, + optionsWithValue, + executable === "sudo" ? SUDO_NON_EXEC_OPTIONS : undefined, + ); + if (!option) { + return null; + } + const consumeNextValue = knownCarrierOptionConsumesNextValue( + option, + optionsWithValue, + executable === "sudo" ? SUDO_NON_EXEC_OPTIONS : undefined, + ); + if (consumeNextValue === null) { + return null; + } + if (consumeNextValue) { + index += 1; + } + } + return null; +} + +function resolveExecCarriedArgv(argv: string[]): string[] | null { + if (normalizeExecutableToken(argv[0] ?? "") !== "exec") { + return null; + } + for (let index = 1; index < argv.length; index += 1) { + const token = argv[index] ?? ""; + if (token === "--") { + return argv.slice(index + 1); + } + if (!token.startsWith("-")) { + return argv.slice(index); + } + const option = parseCarrierOptionToken(token, EXEC_STANDALONE_OPTIONS, EXEC_OPTIONS_WITH_VALUE); + if (!option) { + return null; + } + const consumeNextValue = knownCarrierOptionConsumesNextValue(option, EXEC_OPTIONS_WITH_VALUE); + if (consumeNextValue) { + index += 1; + } + } + return null; +} + +export function resolveCarrierCommandArgv( + argv: string[], + depth = 0, + options?: { includeExec?: boolean }, +): string[] | null { + const executable = normalizeExecutableToken(argv[0] ?? ""); + switch (executable) { + case "env": + return resolveEnvCarriedArgv(argv, depth); + case "command": + case "builtin": + return resolveCommandBuiltinCarriedArgv(argv); + case "sudo": + case "doas": + return resolveSudoLikeCarriedArgv(argv); + case "exec": + return options?.includeExec ? resolveExecCarriedArgv(argv) : null; + default: + return null; + } +} diff --git a/src/infra/dispatch-wrapper-resolution.ts b/src/infra/dispatch-wrapper-resolution.ts index c85fca596d6..5e7835af979 100644 --- a/src/infra/dispatch-wrapper-resolution.ts +++ b/src/infra/dispatch-wrapper-resolution.ts @@ -1,32 +1,15 @@ import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; -import { splitShellArgs } from "../utils/shell-argv.js"; +import { + envInvocationUsesModifiers, + parseEnvInvocationPrelude, + unwrapEnvInvocation, +} from "./command-carriers.js"; import { normalizeExecutableToken } from "./exec-wrapper-tokens.js"; +export { unwrapEnvInvocation } from "./command-carriers.js"; + export const MAX_DISPATCH_WRAPPER_DEPTH = 4; -const ENV_OPTIONS_WITH_VALUE = new Set([ - "-u", - "--unset", - "-c", - "--chdir", - "-s", - "--split-string", - "--default-signal", - "--ignore-signal", - "--block-signal", -]); -const ENV_INLINE_VALUE_PREFIXES = [ - "-u", - "-c", - "-s", - "--unset=", - "--chdir=", - "--split-string=", - "--default-signal=", - "--ignore-signal=", - "--block-signal=", -] as const; -const ENV_FLAG_OPTIONS = new Set(["-i", "--ignore-environment", "-0", "--null"]); const NICE_OPTIONS_WITH_VALUE = new Set(["-n", "--adjustment", "--priority"]); const CAFFEINATE_OPTIONS_WITH_VALUE = new Set(["-t", "-w"]); const STDBUF_OPTIONS_WITH_VALUE = new Set(["-i", "--input", "-o", "--output", "-e", "--error"]); @@ -83,19 +66,6 @@ function isKnownArchNameToken(token: string): boolean { type WrapperScanDirective = "continue" | "consume-next" | "stop" | "invalid"; -function isEnvAssignment(token: string): boolean { - return /^[A-Za-z_][A-Za-z0-9_]*=.*/.test(token); -} - -function hasEnvInlineValuePrefix(lower: string): boolean { - for (const prefix of ENV_INLINE_VALUE_PREFIXES) { - if (lower.startsWith(prefix)) { - return true; - } - } - return false; -} - function scanWrapperInvocation( argv: string[], params: { @@ -143,109 +113,6 @@ function scanWrapperInvocation( return argv.slice(commandIndex); } -export function unwrapEnvInvocation(argv: string[]): string[] | null { - const parsed = parseEnvInvocationPrelude(argv); - return parsed ? (parsed.splitArgv ?? argv.slice(parsed.commandIndex)) : null; -} - -type ParsedEnvInvocationPrelude = { - assignmentKeys: string[]; - commandIndex: number; - splitArgv?: string[]; -}; - -function splitEnvSplitStringPayload(payload: string, trailingArgv: string[]): string[] | null { - const splitArgv = splitShellArgs(payload); - return splitArgv && splitArgv.length > 0 ? [...splitArgv, ...trailingArgv] : null; -} - -function parseEnvInvocationPrelude(argv: string[]): ParsedEnvInvocationPrelude | null { - let idx = 1; - let expectsOptionValue = false; - const assignmentKeys: string[] = []; - while (idx < argv.length) { - const token = argv[idx]?.trim() ?? ""; - if (!token) { - idx += 1; - continue; - } - if (expectsOptionValue) { - expectsOptionValue = false; - idx += 1; - continue; - } - if (token === "--" || token === "-") { - idx += 1; - break; - } - if (isEnvAssignment(token)) { - const delimiter = token.indexOf("="); - if (delimiter > 0) { - assignmentKeys.push(token.slice(0, delimiter)); - } - idx += 1; - continue; - } - if (!token.startsWith("-") || token === "-") { - break; - } - const lower = normalizeLowercaseStringOrEmpty(token); - const [flag] = lower.split("=", 2); - if (flag === "-s" || flag === "--split-string") { - const payload = lower.includes("=") ? token.slice(token.indexOf("=") + 1) : argv[idx + 1]; - if (typeof payload !== "string") { - return null; - } - const trailingIndex = lower.includes("=") ? idx + 1 : idx + 2; - const splitArgv = splitEnvSplitStringPayload(payload, argv.slice(trailingIndex)); - return splitArgv - ? { - assignmentKeys, - commandIndex: trailingIndex, - splitArgv, - } - : null; - } - if (lower.startsWith("-s") && lower.length > 2) { - const splitArgv = splitEnvSplitStringPayload(token.slice(2), argv.slice(idx + 1)); - return splitArgv - ? { - assignmentKeys, - commandIndex: idx + 1, - splitArgv, - } - : null; - } - if (ENV_FLAG_OPTIONS.has(flag)) { - idx += 1; - continue; - } - if (ENV_OPTIONS_WITH_VALUE.has(flag)) { - if (lower.includes("=")) { - idx += 1; - continue; - } - expectsOptionValue = true; - idx += 1; - continue; - } - if (hasEnvInlineValuePrefix(lower)) { - idx += 1; - continue; - } - return null; - } - - if (expectsOptionValue || idx >= argv.length) { - return null; - } - - return { - assignmentKeys, - commandIndex: idx, - }; -} - export function extractEnvAssignmentKeysFromDispatchWrappers( argv: string[], maxDepth = MAX_DISPATCH_WRAPPER_DEPTH, @@ -268,50 +135,6 @@ export function extractEnvAssignmentKeysFromDispatchWrappers( return Array.from(new Set(assignmentKeys)).toSorted((a, b) => a.localeCompare(b)); } -function envInvocationUsesModifiers(argv: string[]): boolean { - let idx = 1; - let expectsOptionValue = false; - while (idx < argv.length) { - const token = argv[idx]?.trim() ?? ""; - if (!token) { - idx += 1; - continue; - } - if (expectsOptionValue) { - return true; - } - if (token === "--" || token === "-") { - idx += 1; - break; - } - if (isEnvAssignment(token)) { - return true; - } - if (!token.startsWith("-") || token === "-") { - break; - } - const lower = normalizeLowercaseStringOrEmpty(token); - const [flag] = lower.split("=", 2); - if (ENV_FLAG_OPTIONS.has(flag)) { - return true; - } - if (ENV_OPTIONS_WITH_VALUE.has(flag)) { - if (lower.includes("=")) { - return true; - } - expectsOptionValue = true; - idx += 1; - continue; - } - if (hasEnvInlineValuePrefix(lower)) { - return true; - } - return true; - } - - return false; -} - function unwrapDashOptionInvocation( argv: string[], params: {