mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-22 22:44:13 +08:00
refactor: use industry-standard shell-quote for argument parsing
- Replace custom parseShellArgs with battle-tested shell-quote package - Simplify code by removing unnecessary -p filtering (Claude handles it) - Update tests to use shell-quote directly - Add example workflow showing claude_args usage This provides more robust argument parsing while reducing code complexity.
This commit is contained in:
@@ -5,10 +5,12 @@
|
|||||||
"name": "@anthropic-ai/claude-code-base-action",
|
"name": "@anthropic-ai/claude-code-base-action",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.10.1",
|
"@actions/core": "^1.10.1",
|
||||||
|
"shell-quote": "^1.8.3",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "^1.2.12",
|
"@types/bun": "^1.2.12",
|
||||||
"@types/node": "^20.0.0",
|
"@types/node": "^20.0.0",
|
||||||
|
"@types/shell-quote": "^1.7.5",
|
||||||
"prettier": "3.5.3",
|
"prettier": "3.5.3",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
},
|
},
|
||||||
@@ -31,12 +33,16 @@
|
|||||||
|
|
||||||
"@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="],
|
"@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="],
|
||||||
|
|
||||||
|
"@types/shell-quote": ["@types/shell-quote@1.7.5", "", {}, "sha512-+UE8GAGRPbJVQDdxi16dgadcBfQ+KG2vgZhV1+3A1XmHbmwcdwhCUwIdy+d3pAGrbvgRoVSjeI9vOWyq376Yzw=="],
|
||||||
|
|
||||||
"bun-types": ["bun-types@1.2.19", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-uAOTaZSPuYsWIXRpj7o56Let0g/wjihKCkeRqUBhlLVM/Bt+Fj9xTo+LhC1OV1XDaGkz4hNC80et5xgy+9KTHQ=="],
|
"bun-types": ["bun-types@1.2.19", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-uAOTaZSPuYsWIXRpj7o56Let0g/wjihKCkeRqUBhlLVM/Bt+Fj9xTo+LhC1OV1XDaGkz4hNC80et5xgy+9KTHQ=="],
|
||||||
|
|
||||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||||
|
|
||||||
"prettier": ["prettier@3.5.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw=="],
|
"prettier": ["prettier@3.5.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw=="],
|
||||||
|
|
||||||
|
"shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="],
|
||||||
|
|
||||||
"tunnel": ["tunnel@0.0.6", "", {}, "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="],
|
"tunnel": ["tunnel@0.0.6", "", {}, "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="],
|
||||||
|
|
||||||
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
||||||
|
|||||||
@@ -10,11 +10,13 @@
|
|||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.10.1"
|
"@actions/core": "^1.10.1",
|
||||||
|
"shell-quote": "^1.8.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "^1.2.12",
|
"@types/bun": "^1.2.12",
|
||||||
"@types/node": "^20.0.0",
|
"@types/node": "^20.0.0",
|
||||||
|
"@types/shell-quote": "^1.7.5",
|
||||||
"prettier": "3.5.3",
|
"prettier": "3.5.3",
|
||||||
"typescript": "^5.8.3"
|
"typescript": "^5.8.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { promisify } from "util";
|
|||||||
import { unlink, writeFile, stat } from "fs/promises";
|
import { unlink, writeFile, stat } from "fs/promises";
|
||||||
import { createWriteStream } from "fs";
|
import { createWriteStream } from "fs";
|
||||||
import { spawn } from "child_process";
|
import { spawn } from "child_process";
|
||||||
|
import { parse as parseShellArgs } from "shell-quote";
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
|
|
||||||
@@ -31,67 +32,6 @@ type PreparedConfig = {
|
|||||||
env: Record<string, string>;
|
env: Record<string, string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse shell-style arguments string into array.
|
|
||||||
* Handles quoted strings and escaping.
|
|
||||||
*/
|
|
||||||
function parseShellArgs(argsString?: string): string[] {
|
|
||||||
if (!argsString || argsString.trim() === "") {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const args: string[] = [];
|
|
||||||
let current = "";
|
|
||||||
let inSingleQuote = false;
|
|
||||||
let inDoubleQuote = false;
|
|
||||||
let escapeNext = false;
|
|
||||||
|
|
||||||
for (let i = 0; i < argsString.length; i++) {
|
|
||||||
const char = argsString[i];
|
|
||||||
|
|
||||||
if (escapeNext) {
|
|
||||||
current += char;
|
|
||||||
escapeNext = false;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (char === "\\") {
|
|
||||||
if (inSingleQuote) {
|
|
||||||
current += char;
|
|
||||||
} else {
|
|
||||||
escapeNext = true;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (char === "'" && !inDoubleQuote) {
|
|
||||||
inSingleQuote = !inSingleQuote;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (char === '"' && !inSingleQuote) {
|
|
||||||
inDoubleQuote = !inDoubleQuote;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (char === " " && !inSingleQuote && !inDoubleQuote) {
|
|
||||||
if (current) {
|
|
||||||
args.push(current);
|
|
||||||
current = "";
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
current += char;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (current) {
|
|
||||||
args.push(current);
|
|
||||||
}
|
|
||||||
|
|
||||||
return args;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseCustomEnvVars(claudeEnv?: string): Record<string, string> {
|
function parseCustomEnvVars(claudeEnv?: string): Record<string, string> {
|
||||||
if (!claudeEnv || claudeEnv.trim() === "") {
|
if (!claudeEnv || claudeEnv.trim() === "") {
|
||||||
return {};
|
return {};
|
||||||
@@ -172,16 +112,12 @@ export function prepareRunConfig(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parse and append custom arguments (these can override the above)
|
// Parse and append custom arguments (these can override the above)
|
||||||
if (options.claudeArgs) {
|
if (options.claudeArgs && options.claudeArgs.trim() !== "") {
|
||||||
const customArgs = parseShellArgs(options.claudeArgs);
|
const parsed = parseShellArgs(options.claudeArgs);
|
||||||
// Filter out -p flag since we handle prompt via pipe
|
// shell-quote returns an array that can contain strings and objects
|
||||||
const filteredArgs = customArgs.filter(
|
// We only want string arguments
|
||||||
(arg, index) =>
|
const customArgs = parsed.filter((arg): arg is string => typeof arg === "string");
|
||||||
arg !== "-p" &&
|
claudeArgs.push(...customArgs);
|
||||||
arg !== "--prompt" &&
|
|
||||||
(index === 0 || (customArgs[index - 1] !== "-p" && customArgs[index - 1] !== "--prompt")),
|
|
||||||
);
|
|
||||||
claudeArgs.push(...filteredArgs);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse custom environment variables
|
// Parse custom environment variables
|
||||||
|
|||||||
@@ -1,68 +1,9 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import { parse as parseShellArgs } from "shell-quote";
|
||||||
|
|
||||||
// Import the function directly from run-claude.ts for testing
|
describe("shell-quote parseShellArgs", () => {
|
||||||
// We'll need to export it first
|
|
||||||
function parseShellArgs(argsString?: string): string[] {
|
|
||||||
if (!argsString || argsString.trim() === "") {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const args: string[] = [];
|
|
||||||
let current = "";
|
|
||||||
let inSingleQuote = false;
|
|
||||||
let inDoubleQuote = false;
|
|
||||||
let escapeNext = false;
|
|
||||||
|
|
||||||
for (let i = 0; i < argsString.length; i++) {
|
|
||||||
const char = argsString[i];
|
|
||||||
|
|
||||||
if (escapeNext) {
|
|
||||||
current += char;
|
|
||||||
escapeNext = false;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (char === "\\") {
|
|
||||||
if (inSingleQuote) {
|
|
||||||
current += char;
|
|
||||||
} else {
|
|
||||||
escapeNext = true;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (char === "'" && !inDoubleQuote) {
|
|
||||||
inSingleQuote = !inSingleQuote;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (char === '"' && !inSingleQuote) {
|
|
||||||
inDoubleQuote = !inDoubleQuote;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (char === " " && !inSingleQuote && !inDoubleQuote) {
|
|
||||||
if (current) {
|
|
||||||
args.push(current);
|
|
||||||
current = "";
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
current += char;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (current) {
|
|
||||||
args.push(current);
|
|
||||||
}
|
|
||||||
|
|
||||||
return args;
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("parseShellArgs", () => {
|
|
||||||
test("should handle empty input", () => {
|
test("should handle empty input", () => {
|
||||||
expect(parseShellArgs("")).toEqual([]);
|
expect(parseShellArgs("")).toEqual([]);
|
||||||
expect(parseShellArgs(undefined)).toEqual([]);
|
|
||||||
expect(parseShellArgs(" ")).toEqual([]);
|
expect(parseShellArgs(" ")).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -116,4 +57,11 @@ describe("parseShellArgs", () => {
|
|||||||
"You are helpful",
|
"You are helpful",
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("should filter out non-string results", () => {
|
||||||
|
// shell-quote can return objects for operators like | > < etc
|
||||||
|
const result = parseShellArgs("echo hello");
|
||||||
|
const filtered = result.filter(arg => typeof arg === "string");
|
||||||
|
expect(filtered).toEqual(["echo", "hello"]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
40
claude-args-example.yml
Normal file
40
claude-args-example.yml
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
name: Claude with Custom Args Example
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
prompt:
|
||||||
|
description: "Prompt for Claude"
|
||||||
|
required: false
|
||||||
|
default: "Respond with a joke"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
claude-custom:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: anthropics/claude-code-action@v1
|
||||||
|
with:
|
||||||
|
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
|
|
||||||
|
# Option 1: Simple prompt with custom args
|
||||||
|
prompt: "Review this code and provide feedback"
|
||||||
|
claude_args: "--max-turns 3 --model claude-3-5-sonnet-latest"
|
||||||
|
|
||||||
|
# Option 2: Slash command with custom MCP config
|
||||||
|
# prompt: "/review"
|
||||||
|
# claude_args: "--mcp-config /workspace/custom-mcp.json --max-turns 5"
|
||||||
|
|
||||||
|
# Option 3: Override output format and add custom system prompt
|
||||||
|
# prompt: "Fix the failing tests"
|
||||||
|
# claude_args: "--output-format json --system-prompt 'You are an expert test fixer'"
|
||||||
|
|
||||||
|
# How it works:
|
||||||
|
# The action will execute: claude -p --verbose --output-format stream-json [your claude_args]
|
||||||
|
# Where the prompt is piped via stdin
|
||||||
|
|
||||||
|
# Benefits:
|
||||||
|
# - Full control over Claude CLI arguments
|
||||||
|
# - Use any Claude feature without waiting for action updates
|
||||||
|
# - Override defaults when needed
|
||||||
|
# - Combine with existing inputs or use standalone
|
||||||
Reference in New Issue
Block a user