mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-19 18:39:20 -05:00
refactor(utils): share shell argv tokenizer
This commit is contained in:
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import type { ExecAllowlistEntry } from "./exec-approvals.js";
|
||||
import { splitShellArgs } from "../utils/shell-argv.js";
|
||||
|
||||
export const DEFAULT_SAFE_BINS = ["jq", "grep", "cut", "sort", "uniq", "head", "tail", "tr", "wc"];
|
||||
|
||||
@@ -582,75 +583,6 @@ export function isWindowsPlatform(platform?: string | null): boolean {
|
||||
return normalized.startsWith("win");
|
||||
}
|
||||
|
||||
function tokenizeShellSegment(segment: string): string[] | null {
|
||||
const tokens: string[] = [];
|
||||
let buf = "";
|
||||
let inSingle = false;
|
||||
let inDouble = false;
|
||||
let escaped = false;
|
||||
|
||||
const pushToken = () => {
|
||||
if (buf.length > 0) {
|
||||
tokens.push(buf);
|
||||
buf = "";
|
||||
}
|
||||
};
|
||||
|
||||
for (let i = 0; i < segment.length; i += 1) {
|
||||
const ch = segment[i];
|
||||
if (escaped) {
|
||||
buf += ch;
|
||||
escaped = false;
|
||||
continue;
|
||||
}
|
||||
if (!inSingle && !inDouble && ch === "\\") {
|
||||
escaped = true;
|
||||
continue;
|
||||
}
|
||||
if (inSingle) {
|
||||
if (ch === "'") {
|
||||
inSingle = false;
|
||||
} else {
|
||||
buf += ch;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (inDouble) {
|
||||
const next = segment[i + 1];
|
||||
if (ch === "\\" && isDoubleQuoteEscape(next)) {
|
||||
buf += next;
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if (ch === '"') {
|
||||
inDouble = false;
|
||||
} else {
|
||||
buf += ch;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (ch === "'") {
|
||||
inSingle = true;
|
||||
continue;
|
||||
}
|
||||
if (ch === '"') {
|
||||
inDouble = true;
|
||||
continue;
|
||||
}
|
||||
if (/\s/.test(ch)) {
|
||||
pushToken();
|
||||
continue;
|
||||
}
|
||||
buf += ch;
|
||||
}
|
||||
|
||||
if (escaped || inSingle || inDouble) {
|
||||
return null;
|
||||
}
|
||||
pushToken();
|
||||
return tokens;
|
||||
}
|
||||
|
||||
function parseSegmentsFromParts(
|
||||
parts: string[],
|
||||
cwd?: string,
|
||||
@@ -658,7 +590,7 @@ function parseSegmentsFromParts(
|
||||
): ExecCommandSegment[] | null {
|
||||
const segments: ExecCommandSegment[] = [];
|
||||
for (const raw of parts) {
|
||||
const argv = tokenizeShellSegment(raw);
|
||||
const argv = splitShellArgs(raw);
|
||||
if (!argv || argv.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
19
src/utils/shell-argv.test.ts
Normal file
19
src/utils/shell-argv.test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { splitShellArgs } from "./shell-argv.js";
|
||||
|
||||
describe("splitShellArgs", () => {
|
||||
it("splits whitespace and respects quotes", () => {
|
||||
expect(splitShellArgs(`qmd --foo "bar baz"`)).toEqual(["qmd", "--foo", "bar baz"]);
|
||||
expect(splitShellArgs(`qmd --foo 'bar baz'`)).toEqual(["qmd", "--foo", "bar baz"]);
|
||||
});
|
||||
|
||||
it("supports backslash escapes inside double quotes", () => {
|
||||
expect(splitShellArgs(String.raw`echo "a\"b"`)).toEqual(["echo", `a"b`]);
|
||||
expect(splitShellArgs(String.raw`echo "\$HOME"`)).toEqual(["echo", "$HOME"]);
|
||||
});
|
||||
|
||||
it("returns null for unterminated quotes", () => {
|
||||
expect(splitShellArgs(`echo "oops`)).toBeNull();
|
||||
expect(splitShellArgs(`echo 'oops`)).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,9 @@
|
||||
const DOUBLE_QUOTE_ESCAPES = new Set(["\\", '"', "$", "`", "\n", "\r"]);
|
||||
|
||||
function isDoubleQuoteEscape(next: string | undefined): next is string {
|
||||
return Boolean(next && DOUBLE_QUOTE_ESCAPES.has(next));
|
||||
}
|
||||
|
||||
export function splitShellArgs(raw: string): string[] | null {
|
||||
const tokens: string[] = [];
|
||||
let buf = "";
|
||||
@@ -32,6 +38,12 @@ export function splitShellArgs(raw: string): string[] | null {
|
||||
continue;
|
||||
}
|
||||
if (inDouble) {
|
||||
const next = raw[i + 1];
|
||||
if (ch === "\\" && isDoubleQuoteEscape(next)) {
|
||||
buf += next;
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if (ch === '"') {
|
||||
inDouble = false;
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user