mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 09:38:39 +00:00
refactor(daemon): share quoted arg splitter
This commit is contained in:
36
src/daemon/arg-split.test.ts
Normal file
36
src/daemon/arg-split.test.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { splitArgsPreservingQuotes } from "./arg-split.js";
|
||||||
|
|
||||||
|
describe("splitArgsPreservingQuotes", () => {
|
||||||
|
it("splits on whitespace outside quotes", () => {
|
||||||
|
expect(splitArgsPreservingQuotes('/usr/bin/openclaw gateway start --name "My Bot"')).toEqual([
|
||||||
|
"/usr/bin/openclaw",
|
||||||
|
"gateway",
|
||||||
|
"start",
|
||||||
|
"--name",
|
||||||
|
"My Bot",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("supports systemd-style backslash escaping", () => {
|
||||||
|
expect(
|
||||||
|
splitArgsPreservingQuotes('openclaw --name "My \\"Bot\\"" --foo bar', {
|
||||||
|
escapeMode: "backslash",
|
||||||
|
}),
|
||||||
|
).toEqual(["openclaw", "--name", 'My "Bot"', "--foo", "bar"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("supports schtasks-style escaped quotes while preserving other backslashes", () => {
|
||||||
|
expect(
|
||||||
|
splitArgsPreservingQuotes('openclaw --path "C:\\\\Program Files\\\\OpenClaw"', {
|
||||||
|
escapeMode: "backslash-quote-only",
|
||||||
|
}),
|
||||||
|
).toEqual(["openclaw", "--path", "C:\\\\Program Files\\\\OpenClaw"]);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
splitArgsPreservingQuotes('openclaw --label "My \\"Quoted\\" Name"', {
|
||||||
|
escapeMode: "backslash-quote-only",
|
||||||
|
}),
|
||||||
|
).toEqual(["openclaw", "--label", 'My "Quoted" Name']);
|
||||||
|
});
|
||||||
|
});
|
||||||
48
src/daemon/arg-split.ts
Normal file
48
src/daemon/arg-split.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
export type ArgSplitEscapeMode = "none" | "backslash" | "backslash-quote-only";
|
||||||
|
|
||||||
|
export function splitArgsPreservingQuotes(
|
||||||
|
value: string,
|
||||||
|
options?: { escapeMode?: ArgSplitEscapeMode },
|
||||||
|
): string[] {
|
||||||
|
const args: string[] = [];
|
||||||
|
let current = "";
|
||||||
|
let inQuotes = false;
|
||||||
|
const escapeMode = options?.escapeMode ?? "none";
|
||||||
|
|
||||||
|
for (let i = 0; i < value.length; i++) {
|
||||||
|
const char = value[i];
|
||||||
|
if (escapeMode === "backslash" && char === "\\") {
|
||||||
|
if (i + 1 < value.length) {
|
||||||
|
current += value[i + 1];
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
escapeMode === "backslash-quote-only" &&
|
||||||
|
char === "\\" &&
|
||||||
|
i + 1 < value.length &&
|
||||||
|
value[i + 1] === '"'
|
||||||
|
) {
|
||||||
|
current += '"';
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (char === '"') {
|
||||||
|
inQuotes = !inQuotes;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!inQuotes && /\s/.test(char)) {
|
||||||
|
if (current) {
|
||||||
|
args.push(current);
|
||||||
|
current = "";
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
current += char;
|
||||||
|
}
|
||||||
|
if (current) {
|
||||||
|
args.push(current);
|
||||||
|
}
|
||||||
|
return args;
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import type { GatewayServiceRuntime } from "./service-runtime.js";
|
import type { GatewayServiceRuntime } from "./service-runtime.js";
|
||||||
|
import { splitArgsPreservingQuotes } from "./arg-split.js";
|
||||||
import { formatGatewayServiceDescription, resolveGatewayWindowsTaskName } from "./constants.js";
|
import { formatGatewayServiceDescription, resolveGatewayWindowsTaskName } from "./constants.js";
|
||||||
import { formatLine } from "./output.js";
|
import { formatLine } from "./output.js";
|
||||||
import { resolveGatewayStateDir } from "./paths.js";
|
import { resolveGatewayStateDir } from "./paths.js";
|
||||||
@@ -48,36 +49,9 @@ function resolveTaskUser(env: Record<string, string | undefined>): string | null
|
|||||||
}
|
}
|
||||||
|
|
||||||
function parseCommandLine(value: string): string[] {
|
function parseCommandLine(value: string): string[] {
|
||||||
const args: string[] = [];
|
// `buildTaskScript` only escapes quotes (`\"`).
|
||||||
let current = "";
|
// Keep all other backslashes literal so drive and UNC paths are preserved.
|
||||||
let inQuotes = false;
|
return splitArgsPreservingQuotes(value, { escapeMode: "backslash-quote-only" });
|
||||||
|
|
||||||
for (let i = 0; i < value.length; i++) {
|
|
||||||
const char = value[i];
|
|
||||||
// `buildTaskScript` only escapes quotes (`\"`).
|
|
||||||
// Keep all other backslashes literal so drive and UNC paths are preserved.
|
|
||||||
if (char === "\\" && i + 1 < value.length && value[i + 1] === '"') {
|
|
||||||
current += value[i + 1];
|
|
||||||
i++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (char === '"') {
|
|
||||||
inQuotes = !inQuotes;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!inQuotes && /\s/.test(char)) {
|
|
||||||
if (current) {
|
|
||||||
args.push(current);
|
|
||||||
current = "";
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
current += char;
|
|
||||||
}
|
|
||||||
if (current) {
|
|
||||||
args.push(current);
|
|
||||||
}
|
|
||||||
return args;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function readScheduledTaskCommand(env: Record<string, string | undefined>): Promise<{
|
export async function readScheduledTaskCommand(env: Record<string, string | undefined>): Promise<{
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { splitArgsPreservingQuotes } from "./arg-split.js";
|
||||||
|
|
||||||
function systemdEscapeArg(value: string): string {
|
function systemdEscapeArg(value: string): string {
|
||||||
if (!/[\\s"\\\\]/.test(value)) {
|
if (!/[\\s"\\\\]/.test(value)) {
|
||||||
return value;
|
return value;
|
||||||
@@ -63,38 +65,7 @@ export function buildSystemdUnit({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function parseSystemdExecStart(value: string): string[] {
|
export function parseSystemdExecStart(value: string): string[] {
|
||||||
const args: string[] = [];
|
return splitArgsPreservingQuotes(value, { escapeMode: "backslash" });
|
||||||
let current = "";
|
|
||||||
let inQuotes = false;
|
|
||||||
let escapeNext = false;
|
|
||||||
|
|
||||||
for (const char of value) {
|
|
||||||
if (escapeNext) {
|
|
||||||
current += char;
|
|
||||||
escapeNext = false;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (char === "\\\\") {
|
|
||||||
escapeNext = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (char === '"') {
|
|
||||||
inQuotes = !inQuotes;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!inQuotes && /\s/.test(char)) {
|
|
||||||
if (current) {
|
|
||||||
args.push(current);
|
|
||||||
current = "";
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
current += char;
|
|
||||||
}
|
|
||||||
if (current) {
|
|
||||||
args.push(current);
|
|
||||||
}
|
|
||||||
return args;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseSystemdEnvAssignment(raw: string): { key: string; value: string } | null {
|
export function parseSystemdEnvAssignment(raw: string): { key: string; value: string } | null {
|
||||||
|
|||||||
Reference in New Issue
Block a user