mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:10:44 +00:00
refactor: share carrier command parsing
This commit is contained in:
@@ -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<string>,
|
||||
optionsWithValue: ReadonlySet<string>,
|
||||
nonExecutingOptions: ReadonlySet<string> = 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<string>,
|
||||
nonExecutingOptions: ReadonlySet<string> = 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<string>,
|
||||
): 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");
|
||||
}
|
||||
|
||||
384
src/infra/command-carriers.ts
Normal file
384
src/infra/command-carriers.ts
Normal file
@@ -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<string>,
|
||||
optionsWithValue: ReadonlySet<string>,
|
||||
nonExecutingOptions: ReadonlySet<string> = 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<string>,
|
||||
nonExecutingOptions: ReadonlySet<string> = 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<string>,
|
||||
): 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;
|
||||
}
|
||||
}
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user