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:
km-anthropic
2025-08-07 15:18:00 -07:00
parent dfcaac854e
commit b6238ad00e
5 changed files with 65 additions and 133 deletions

View File

@@ -5,10 +5,12 @@
"name": "@anthropic-ai/claude-code-base-action",
"dependencies": {
"@actions/core": "^1.10.1",
"shell-quote": "^1.8.3",
},
"devDependencies": {
"@types/bun": "^1.2.12",
"@types/node": "^20.0.0",
"@types/shell-quote": "^1.7.5",
"prettier": "3.5.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/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=="],
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"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=="],
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],

View File

@@ -10,11 +10,13 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@actions/core": "^1.10.1"
"@actions/core": "^1.10.1",
"shell-quote": "^1.8.3"
},
"devDependencies": {
"@types/bun": "^1.2.12",
"@types/node": "^20.0.0",
"@types/shell-quote": "^1.7.5",
"prettier": "3.5.3",
"typescript": "^5.8.3"
}

View File

@@ -4,6 +4,7 @@ import { promisify } from "util";
import { unlink, writeFile, stat } from "fs/promises";
import { createWriteStream } from "fs";
import { spawn } from "child_process";
import { parse as parseShellArgs } from "shell-quote";
const execAsync = promisify(exec);
@@ -31,67 +32,6 @@ type PreparedConfig = {
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> {
if (!claudeEnv || claudeEnv.trim() === "") {
return {};
@@ -172,16 +112,12 @@ export function prepareRunConfig(
}
// Parse and append custom arguments (these can override the above)
if (options.claudeArgs) {
const customArgs = parseShellArgs(options.claudeArgs);
// Filter out -p flag since we handle prompt via pipe
const filteredArgs = customArgs.filter(
(arg, index) =>
arg !== "-p" &&
arg !== "--prompt" &&
(index === 0 || (customArgs[index - 1] !== "-p" && customArgs[index - 1] !== "--prompt")),
);
claudeArgs.push(...filteredArgs);
if (options.claudeArgs && options.claudeArgs.trim() !== "") {
const parsed = parseShellArgs(options.claudeArgs);
// shell-quote returns an array that can contain strings and objects
// We only want string arguments
const customArgs = parsed.filter((arg): arg is string => typeof arg === "string");
claudeArgs.push(...customArgs);
}
// Parse custom environment variables

View File

@@ -1,68 +1,9 @@
import { describe, expect, test } from "bun:test";
import { parse as parseShellArgs } from "shell-quote";
// Import the function directly from run-claude.ts for testing
// 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", () => {
describe("shell-quote parseShellArgs", () => {
test("should handle empty input", () => {
expect(parseShellArgs("")).toEqual([]);
expect(parseShellArgs(undefined)).toEqual([]);
expect(parseShellArgs(" ")).toEqual([]);
});
@@ -116,4 +57,11 @@ describe("parseShellArgs", () => {
"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
View 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