mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
660 lines
19 KiB
TypeScript
660 lines
19 KiB
TypeScript
import path from "node:path";
|
|
import {
|
|
POSIX_INLINE_COMMAND_FLAGS,
|
|
POWERSHELL_INLINE_COMMAND_FLAGS,
|
|
resolveInlineCommandMatch,
|
|
} from "./shell-inline-command.js";
|
|
|
|
export const MAX_DISPATCH_WRAPPER_DEPTH = 4;
|
|
|
|
const WINDOWS_EXE_SUFFIX = ".exe";
|
|
|
|
const POSIX_SHELL_WRAPPER_NAMES = ["ash", "bash", "dash", "fish", "ksh", "sh", "zsh"] as const;
|
|
const WINDOWS_CMD_WRAPPER_NAMES = ["cmd"] as const;
|
|
const POWERSHELL_WRAPPER_NAMES = ["powershell", "pwsh"] as const;
|
|
const SHELL_MULTIPLEXER_WRAPPER_NAMES = ["busybox", "toybox"] as const;
|
|
const DISPATCH_WRAPPER_NAMES = [
|
|
"chrt",
|
|
"doas",
|
|
"env",
|
|
"ionice",
|
|
"nice",
|
|
"nohup",
|
|
"setsid",
|
|
"stdbuf",
|
|
"sudo",
|
|
"taskset",
|
|
"timeout",
|
|
] as const;
|
|
|
|
function withWindowsExeAliases(names: readonly string[]): string[] {
|
|
const expanded = new Set<string>();
|
|
for (const name of names) {
|
|
expanded.add(name);
|
|
expanded.add(`${name}${WINDOWS_EXE_SUFFIX}`);
|
|
}
|
|
return Array.from(expanded);
|
|
}
|
|
|
|
function stripWindowsExeSuffix(value: string): string {
|
|
return value.endsWith(WINDOWS_EXE_SUFFIX) ? value.slice(0, -WINDOWS_EXE_SUFFIX.length) : value;
|
|
}
|
|
|
|
export const POSIX_SHELL_WRAPPERS = new Set(POSIX_SHELL_WRAPPER_NAMES);
|
|
export const WINDOWS_CMD_WRAPPERS = new Set(withWindowsExeAliases(WINDOWS_CMD_WRAPPER_NAMES));
|
|
export const POWERSHELL_WRAPPERS = new Set(withWindowsExeAliases(POWERSHELL_WRAPPER_NAMES));
|
|
export const DISPATCH_WRAPPER_EXECUTABLES = new Set(withWindowsExeAliases(DISPATCH_WRAPPER_NAMES));
|
|
|
|
const POSIX_SHELL_WRAPPER_CANONICAL = new Set<string>(POSIX_SHELL_WRAPPER_NAMES);
|
|
const WINDOWS_CMD_WRAPPER_CANONICAL = new Set<string>(WINDOWS_CMD_WRAPPER_NAMES);
|
|
const POWERSHELL_WRAPPER_CANONICAL = new Set<string>(POWERSHELL_WRAPPER_NAMES);
|
|
const SHELL_MULTIPLEXER_WRAPPER_CANONICAL = new Set<string>(SHELL_MULTIPLEXER_WRAPPER_NAMES);
|
|
const DISPATCH_WRAPPER_CANONICAL = new Set<string>(DISPATCH_WRAPPER_NAMES);
|
|
const SHELL_WRAPPER_CANONICAL = new Set<string>([
|
|
...POSIX_SHELL_WRAPPER_NAMES,
|
|
...WINDOWS_CMD_WRAPPER_NAMES,
|
|
...POWERSHELL_WRAPPER_NAMES,
|
|
]);
|
|
|
|
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 STDBUF_OPTIONS_WITH_VALUE = new Set(["-i", "--input", "-o", "--output", "-e", "--error"]);
|
|
const TIMEOUT_FLAG_OPTIONS = new Set(["--foreground", "--preserve-status", "-v", "--verbose"]);
|
|
const TIMEOUT_OPTIONS_WITH_VALUE = new Set(["-k", "--kill-after", "-s", "--signal"]);
|
|
const TRANSPARENT_DISPATCH_WRAPPERS = new Set(["nice", "nohup", "stdbuf", "timeout"]);
|
|
|
|
type ShellWrapperKind = "posix" | "cmd" | "powershell";
|
|
|
|
type ShellWrapperSpec = {
|
|
kind: ShellWrapperKind;
|
|
names: ReadonlySet<string>;
|
|
};
|
|
|
|
const SHELL_WRAPPER_SPECS: ReadonlyArray<ShellWrapperSpec> = [
|
|
{ kind: "posix", names: POSIX_SHELL_WRAPPER_CANONICAL },
|
|
{ kind: "cmd", names: WINDOWS_CMD_WRAPPER_CANONICAL },
|
|
{ kind: "powershell", names: POWERSHELL_WRAPPER_CANONICAL },
|
|
];
|
|
|
|
export type ShellWrapperCommand = {
|
|
isWrapper: boolean;
|
|
command: string | null;
|
|
};
|
|
|
|
export function basenameLower(token: string): string {
|
|
const win = path.win32.basename(token);
|
|
const posix = path.posix.basename(token);
|
|
const base = win.length < posix.length ? win : posix;
|
|
return base.trim().toLowerCase();
|
|
}
|
|
|
|
export function normalizeExecutableToken(token: string): string {
|
|
return stripWindowsExeSuffix(basenameLower(token));
|
|
}
|
|
|
|
export function isDispatchWrapperExecutable(token: string): boolean {
|
|
return DISPATCH_WRAPPER_CANONICAL.has(normalizeExecutableToken(token));
|
|
}
|
|
|
|
export function isShellWrapperExecutable(token: string): boolean {
|
|
return SHELL_WRAPPER_CANONICAL.has(normalizeExecutableToken(token));
|
|
}
|
|
|
|
function normalizeRawCommand(rawCommand?: string | null): string | null {
|
|
const trimmed = rawCommand?.trim() ?? "";
|
|
return trimmed.length > 0 ? trimmed : null;
|
|
}
|
|
|
|
function findShellWrapperSpec(baseExecutable: string): ShellWrapperSpec | null {
|
|
const canonicalBase = stripWindowsExeSuffix(baseExecutable);
|
|
for (const spec of SHELL_WRAPPER_SPECS) {
|
|
if (spec.names.has(canonicalBase)) {
|
|
return spec;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export type ShellMultiplexerUnwrapResult =
|
|
| { kind: "not-wrapper" }
|
|
| { kind: "blocked"; wrapper: string }
|
|
| { kind: "unwrapped"; wrapper: string; argv: string[] };
|
|
|
|
export function unwrapKnownShellMultiplexerInvocation(
|
|
argv: string[],
|
|
): ShellMultiplexerUnwrapResult {
|
|
const token0 = argv[0]?.trim();
|
|
if (!token0) {
|
|
return { kind: "not-wrapper" };
|
|
}
|
|
const wrapper = normalizeExecutableToken(token0);
|
|
if (!SHELL_MULTIPLEXER_WRAPPER_CANONICAL.has(wrapper)) {
|
|
return { kind: "not-wrapper" };
|
|
}
|
|
|
|
let appletIndex = 1;
|
|
if (argv[appletIndex]?.trim() === "--") {
|
|
appletIndex += 1;
|
|
}
|
|
const applet = argv[appletIndex]?.trim();
|
|
if (!applet || !isShellWrapperExecutable(applet)) {
|
|
return { kind: "blocked", wrapper };
|
|
}
|
|
|
|
const unwrapped = argv.slice(appletIndex);
|
|
if (unwrapped.length === 0) {
|
|
return { kind: "blocked", wrapper };
|
|
}
|
|
return { kind: "unwrapped", wrapper, argv: unwrapped };
|
|
}
|
|
|
|
export 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;
|
|
}
|
|
|
|
type WrapperScanDirective = "continue" | "consume-next" | "stop" | "invalid";
|
|
|
|
function scanWrapperInvocation(
|
|
argv: string[],
|
|
params: {
|
|
separators?: ReadonlySet<string>;
|
|
onToken: (token: string, lowerToken: string) => WrapperScanDirective;
|
|
adjustCommandIndex?: (commandIndex: number, argv: string[]) => number | null;
|
|
},
|
|
): string[] | null {
|
|
let idx = 1;
|
|
let expectsOptionValue = false;
|
|
while (idx < argv.length) {
|
|
const token = argv[idx]?.trim() ?? "";
|
|
if (!token) {
|
|
idx += 1;
|
|
continue;
|
|
}
|
|
if (expectsOptionValue) {
|
|
expectsOptionValue = false;
|
|
idx += 1;
|
|
continue;
|
|
}
|
|
if (params.separators?.has(token)) {
|
|
idx += 1;
|
|
break;
|
|
}
|
|
const directive = params.onToken(token, token.toLowerCase());
|
|
if (directive === "stop") {
|
|
break;
|
|
}
|
|
if (directive === "invalid") {
|
|
return null;
|
|
}
|
|
if (directive === "consume-next") {
|
|
expectsOptionValue = true;
|
|
}
|
|
idx += 1;
|
|
}
|
|
if (expectsOptionValue) {
|
|
return null;
|
|
}
|
|
const commandIndex = params.adjustCommandIndex ? params.adjustCommandIndex(idx, argv) : idx;
|
|
if (commandIndex === null || commandIndex >= argv.length) {
|
|
return null;
|
|
}
|
|
return argv.slice(commandIndex);
|
|
}
|
|
|
|
export function unwrapEnvInvocation(argv: string[]): string[] | null {
|
|
return scanWrapperInvocation(argv, {
|
|
separators: new Set(["--", "-"]),
|
|
onToken: (token, lower) => {
|
|
if (isEnvAssignment(token)) {
|
|
return "continue";
|
|
}
|
|
if (!token.startsWith("-") || token === "-") {
|
|
return "stop";
|
|
}
|
|
const [flag] = lower.split("=", 2);
|
|
if (ENV_FLAG_OPTIONS.has(flag)) {
|
|
return "continue";
|
|
}
|
|
if (ENV_OPTIONS_WITH_VALUE.has(flag)) {
|
|
return lower.includes("=") ? "continue" : "consume-next";
|
|
}
|
|
if (hasEnvInlineValuePrefix(lower)) {
|
|
return "continue";
|
|
}
|
|
return "invalid";
|
|
},
|
|
});
|
|
}
|
|
|
|
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 = token.toLowerCase();
|
|
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;
|
|
}
|
|
// Unknown env flags are treated conservatively as modifiers.
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function unwrapNiceInvocation(argv: string[]): string[] | null {
|
|
return unwrapDashOptionInvocation(argv, {
|
|
onFlag: (flag, lower) => {
|
|
if (/^-\d+$/.test(lower)) {
|
|
return "continue";
|
|
}
|
|
if (NICE_OPTIONS_WITH_VALUE.has(flag)) {
|
|
return lower.includes("=") || lower !== flag ? "continue" : "consume-next";
|
|
}
|
|
if (lower.startsWith("-n") && lower.length > 2) {
|
|
return "continue";
|
|
}
|
|
return "invalid";
|
|
},
|
|
});
|
|
}
|
|
|
|
function unwrapNohupInvocation(argv: string[]): string[] | null {
|
|
return scanWrapperInvocation(argv, {
|
|
separators: new Set(["--"]),
|
|
onToken: (token, lower) => {
|
|
if (!token.startsWith("-") || token === "-") {
|
|
return "stop";
|
|
}
|
|
return lower === "--help" || lower === "--version" ? "continue" : "invalid";
|
|
},
|
|
});
|
|
}
|
|
|
|
function unwrapDashOptionInvocation(
|
|
argv: string[],
|
|
params: {
|
|
onFlag: (flag: string, lowerToken: string) => WrapperScanDirective;
|
|
adjustCommandIndex?: (commandIndex: number, argv: string[]) => number | null;
|
|
},
|
|
): string[] | null {
|
|
return scanWrapperInvocation(argv, {
|
|
separators: new Set(["--"]),
|
|
onToken: (token, lower) => {
|
|
if (!token.startsWith("-") || token === "-") {
|
|
return "stop";
|
|
}
|
|
const [flag] = lower.split("=", 2);
|
|
return params.onFlag(flag, lower);
|
|
},
|
|
adjustCommandIndex: params.adjustCommandIndex,
|
|
});
|
|
}
|
|
|
|
function unwrapStdbufInvocation(argv: string[]): string[] | null {
|
|
return unwrapDashOptionInvocation(argv, {
|
|
onFlag: (flag, lower) => {
|
|
if (!STDBUF_OPTIONS_WITH_VALUE.has(flag)) {
|
|
return "invalid";
|
|
}
|
|
return lower.includes("=") ? "continue" : "consume-next";
|
|
},
|
|
});
|
|
}
|
|
|
|
function unwrapTimeoutInvocation(argv: string[]): string[] | null {
|
|
return unwrapDashOptionInvocation(argv, {
|
|
onFlag: (flag, lower) => {
|
|
if (TIMEOUT_FLAG_OPTIONS.has(flag)) {
|
|
return "continue";
|
|
}
|
|
if (TIMEOUT_OPTIONS_WITH_VALUE.has(flag)) {
|
|
return lower.includes("=") ? "continue" : "consume-next";
|
|
}
|
|
return "invalid";
|
|
},
|
|
adjustCommandIndex: (commandIndex, currentArgv) => {
|
|
// timeout consumes a required duration token before the wrapped command.
|
|
const wrappedCommandIndex = commandIndex + 1;
|
|
return wrappedCommandIndex < currentArgv.length ? wrappedCommandIndex : null;
|
|
},
|
|
});
|
|
}
|
|
|
|
export type DispatchWrapperUnwrapResult =
|
|
| { kind: "not-wrapper" }
|
|
| { kind: "blocked"; wrapper: string }
|
|
| { kind: "unwrapped"; wrapper: string; argv: string[] };
|
|
|
|
export type DispatchWrapperExecutionPlan = {
|
|
argv: string[];
|
|
wrappers: string[];
|
|
policyBlocked: boolean;
|
|
blockedWrapper?: string;
|
|
};
|
|
|
|
function blockDispatchWrapper(wrapper: string): DispatchWrapperUnwrapResult {
|
|
return { kind: "blocked", wrapper };
|
|
}
|
|
|
|
function unwrapDispatchWrapper(
|
|
wrapper: string,
|
|
unwrapped: string[] | null,
|
|
): DispatchWrapperUnwrapResult {
|
|
return unwrapped
|
|
? { kind: "unwrapped", wrapper, argv: unwrapped }
|
|
: blockDispatchWrapper(wrapper);
|
|
}
|
|
|
|
export function unwrapKnownDispatchWrapperInvocation(argv: string[]): DispatchWrapperUnwrapResult {
|
|
const token0 = argv[0]?.trim();
|
|
if (!token0) {
|
|
return { kind: "not-wrapper" };
|
|
}
|
|
const wrapper = normalizeExecutableToken(token0);
|
|
switch (wrapper) {
|
|
case "env":
|
|
return unwrapDispatchWrapper(wrapper, unwrapEnvInvocation(argv));
|
|
case "nice":
|
|
return unwrapDispatchWrapper(wrapper, unwrapNiceInvocation(argv));
|
|
case "nohup":
|
|
return unwrapDispatchWrapper(wrapper, unwrapNohupInvocation(argv));
|
|
case "stdbuf":
|
|
return unwrapDispatchWrapper(wrapper, unwrapStdbufInvocation(argv));
|
|
case "timeout":
|
|
return unwrapDispatchWrapper(wrapper, unwrapTimeoutInvocation(argv));
|
|
case "chrt":
|
|
case "doas":
|
|
case "ionice":
|
|
case "setsid":
|
|
case "sudo":
|
|
case "taskset":
|
|
return blockDispatchWrapper(wrapper);
|
|
default:
|
|
return { kind: "not-wrapper" };
|
|
}
|
|
}
|
|
|
|
export function unwrapDispatchWrappersForResolution(
|
|
argv: string[],
|
|
maxDepth = MAX_DISPATCH_WRAPPER_DEPTH,
|
|
): string[] {
|
|
const plan = resolveDispatchWrapperExecutionPlan(argv, maxDepth);
|
|
return plan.argv;
|
|
}
|
|
|
|
function isSemanticDispatchWrapperUsage(wrapper: string, argv: string[]): boolean {
|
|
if (wrapper === "env") {
|
|
return envInvocationUsesModifiers(argv);
|
|
}
|
|
return !TRANSPARENT_DISPATCH_WRAPPERS.has(wrapper);
|
|
}
|
|
|
|
function blockedDispatchWrapperPlan(params: {
|
|
argv: string[];
|
|
wrappers: string[];
|
|
blockedWrapper: string;
|
|
}): DispatchWrapperExecutionPlan {
|
|
return {
|
|
argv: params.argv,
|
|
wrappers: params.wrappers,
|
|
policyBlocked: true,
|
|
blockedWrapper: params.blockedWrapper,
|
|
};
|
|
}
|
|
|
|
export function resolveDispatchWrapperExecutionPlan(
|
|
argv: string[],
|
|
maxDepth = MAX_DISPATCH_WRAPPER_DEPTH,
|
|
): DispatchWrapperExecutionPlan {
|
|
let current = argv;
|
|
const wrappers: string[] = [];
|
|
for (let depth = 0; depth < maxDepth; depth += 1) {
|
|
const unwrap = unwrapKnownDispatchWrapperInvocation(current);
|
|
if (unwrap.kind === "blocked") {
|
|
return blockedDispatchWrapperPlan({
|
|
argv: current,
|
|
wrappers,
|
|
blockedWrapper: unwrap.wrapper,
|
|
});
|
|
}
|
|
if (unwrap.kind !== "unwrapped" || unwrap.argv.length === 0) {
|
|
break;
|
|
}
|
|
wrappers.push(unwrap.wrapper);
|
|
if (isSemanticDispatchWrapperUsage(unwrap.wrapper, current)) {
|
|
return blockedDispatchWrapperPlan({
|
|
argv: current,
|
|
wrappers,
|
|
blockedWrapper: unwrap.wrapper,
|
|
});
|
|
}
|
|
current = unwrap.argv;
|
|
}
|
|
if (wrappers.length >= maxDepth) {
|
|
const overflow = unwrapKnownDispatchWrapperInvocation(current);
|
|
if (overflow.kind === "blocked" || overflow.kind === "unwrapped") {
|
|
return blockedDispatchWrapperPlan({
|
|
argv: current,
|
|
wrappers,
|
|
blockedWrapper: overflow.wrapper,
|
|
});
|
|
}
|
|
}
|
|
return { argv: current, wrappers, policyBlocked: false };
|
|
}
|
|
|
|
function hasEnvManipulationBeforeShellWrapperInternal(
|
|
argv: string[],
|
|
depth: number,
|
|
envManipulationSeen: boolean,
|
|
): boolean {
|
|
if (depth >= MAX_DISPATCH_WRAPPER_DEPTH) {
|
|
return false;
|
|
}
|
|
|
|
const token0 = argv[0]?.trim();
|
|
if (!token0) {
|
|
return false;
|
|
}
|
|
|
|
const dispatchUnwrap = unwrapKnownDispatchWrapperInvocation(argv);
|
|
if (dispatchUnwrap.kind === "blocked") {
|
|
return false;
|
|
}
|
|
if (dispatchUnwrap.kind === "unwrapped") {
|
|
const nextEnvManipulationSeen =
|
|
envManipulationSeen || (dispatchUnwrap.wrapper === "env" && envInvocationUsesModifiers(argv));
|
|
return hasEnvManipulationBeforeShellWrapperInternal(
|
|
dispatchUnwrap.argv,
|
|
depth + 1,
|
|
nextEnvManipulationSeen,
|
|
);
|
|
}
|
|
|
|
const shellMultiplexerUnwrap = unwrapKnownShellMultiplexerInvocation(argv);
|
|
if (shellMultiplexerUnwrap.kind === "blocked") {
|
|
return false;
|
|
}
|
|
if (shellMultiplexerUnwrap.kind === "unwrapped") {
|
|
return hasEnvManipulationBeforeShellWrapperInternal(
|
|
shellMultiplexerUnwrap.argv,
|
|
depth + 1,
|
|
envManipulationSeen,
|
|
);
|
|
}
|
|
|
|
const wrapper = findShellWrapperSpec(normalizeExecutableToken(token0));
|
|
if (!wrapper) {
|
|
return false;
|
|
}
|
|
const payload = extractShellWrapperPayload(argv, wrapper);
|
|
if (!payload) {
|
|
return false;
|
|
}
|
|
return envManipulationSeen;
|
|
}
|
|
|
|
export function hasEnvManipulationBeforeShellWrapper(argv: string[]): boolean {
|
|
return hasEnvManipulationBeforeShellWrapperInternal(argv, 0, false);
|
|
}
|
|
|
|
function extractPosixShellInlineCommand(argv: string[]): string | null {
|
|
return extractInlineCommandByFlags(argv, POSIX_INLINE_COMMAND_FLAGS, { allowCombinedC: true });
|
|
}
|
|
|
|
function extractCmdInlineCommand(argv: string[]): string | null {
|
|
const idx = argv.findIndex((item) => {
|
|
const token = item.trim().toLowerCase();
|
|
return token === "/c" || token === "/k";
|
|
});
|
|
if (idx === -1) {
|
|
return null;
|
|
}
|
|
const tail = argv.slice(idx + 1);
|
|
if (tail.length === 0) {
|
|
return null;
|
|
}
|
|
const cmd = tail.join(" ").trim();
|
|
return cmd.length > 0 ? cmd : null;
|
|
}
|
|
|
|
function extractPowerShellInlineCommand(argv: string[]): string | null {
|
|
return extractInlineCommandByFlags(argv, POWERSHELL_INLINE_COMMAND_FLAGS);
|
|
}
|
|
|
|
function extractInlineCommandByFlags(
|
|
argv: string[],
|
|
flags: ReadonlySet<string>,
|
|
options: { allowCombinedC?: boolean } = {},
|
|
): string | null {
|
|
return resolveInlineCommandMatch(argv, flags, options).command;
|
|
}
|
|
|
|
function extractShellWrapperPayload(argv: string[], spec: ShellWrapperSpec): string | null {
|
|
switch (spec.kind) {
|
|
case "posix":
|
|
return extractPosixShellInlineCommand(argv);
|
|
case "cmd":
|
|
return extractCmdInlineCommand(argv);
|
|
case "powershell":
|
|
return extractPowerShellInlineCommand(argv);
|
|
}
|
|
}
|
|
|
|
function extractShellWrapperCommandInternal(
|
|
argv: string[],
|
|
rawCommand: string | null,
|
|
depth: number,
|
|
): ShellWrapperCommand {
|
|
if (depth >= MAX_DISPATCH_WRAPPER_DEPTH) {
|
|
return { isWrapper: false, command: null };
|
|
}
|
|
|
|
const token0 = argv[0]?.trim();
|
|
if (!token0) {
|
|
return { isWrapper: false, command: null };
|
|
}
|
|
|
|
const dispatchUnwrap = unwrapKnownDispatchWrapperInvocation(argv);
|
|
if (dispatchUnwrap.kind === "blocked") {
|
|
return { isWrapper: false, command: null };
|
|
}
|
|
if (dispatchUnwrap.kind === "unwrapped") {
|
|
return extractShellWrapperCommandInternal(dispatchUnwrap.argv, rawCommand, depth + 1);
|
|
}
|
|
|
|
const shellMultiplexerUnwrap = unwrapKnownShellMultiplexerInvocation(argv);
|
|
if (shellMultiplexerUnwrap.kind === "blocked") {
|
|
return { isWrapper: false, command: null };
|
|
}
|
|
if (shellMultiplexerUnwrap.kind === "unwrapped") {
|
|
return extractShellWrapperCommandInternal(shellMultiplexerUnwrap.argv, rawCommand, depth + 1);
|
|
}
|
|
|
|
const base0 = normalizeExecutableToken(token0);
|
|
const wrapper = findShellWrapperSpec(base0);
|
|
if (!wrapper) {
|
|
return { isWrapper: false, command: null };
|
|
}
|
|
|
|
const payload = extractShellWrapperPayload(argv, wrapper);
|
|
if (!payload) {
|
|
return { isWrapper: false, command: null };
|
|
}
|
|
|
|
return { isWrapper: true, command: rawCommand ?? payload };
|
|
}
|
|
|
|
export function extractShellWrapperInlineCommand(argv: string[]): string | null {
|
|
const extracted = extractShellWrapperCommandInternal(argv, null, 0);
|
|
return extracted.isWrapper ? extracted.command : null;
|
|
}
|
|
|
|
export function extractShellWrapperCommand(
|
|
argv: string[],
|
|
rawCommand?: string | null,
|
|
): ShellWrapperCommand {
|
|
return extractShellWrapperCommandInternal(argv, normalizeRawCommand(rawCommand), 0);
|
|
}
|