fix+test(#755): -p consumes exactly one token; flags after prompt text now parse normally

This commit is contained in:
YeonGyu-Kim
2026-05-26 21:27:39 +09:00
parent c70312bd04
commit 0e8a449ea9
3 changed files with 116 additions and 17 deletions

View File

@@ -742,6 +742,9 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
let mut base_commit: Option<String> = None;
let mut reasoning_effort: Option<String> = None;
let mut allow_broad_cwd = false;
// #755: -p prompt text captured as single token; remaining args continue
// flag parsing. None until `-p <text>` is seen.
let mut short_p_prompt: Option<String> = None;
let mut rest: Vec<String> = Vec::new();
let mut index = 0;
@@ -858,24 +861,40 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
index += 1;
}
"-p" => {
// Claw Code compat: -p "prompt" = one-shot prompt
let prompt = args[index + 1..].join(" ");
if prompt.trim().is_empty() {
// #753: same missing_prompt shape as claw prompt (no arg) fix in #750
return Err("missing_prompt: -p requires a prompt string.\nUsage: claw -p <text> or claw prompt <text>".to_string());
// Claw Code compat: -p "prompt" = one-shot prompt.
// #755: consume exactly one token so subsequent flags like
// --model/--output-format are parsed normally instead of
// being swallowed into the prompt string (#117).
let next = args.get(index + 1).map(|s| s.as_str());
match next {
None | Some("") => {
return Err("missing_prompt: -p requires a prompt string.\nUsage: claw -p <text> or claw prompt <text>".to_string());
}
Some(tok) if tok.starts_with('-') && tok != "--" => {
// Looks like a flag, not a prompt. Reject so the user
// knows to quote the literal text or use `--`.
return Err(format!(
"missing_prompt: -p requires a prompt string before flags; got `{tok}`.\nUsage: claw -p <text> --model sonnet or claw -p -- {tok} (literal)"
));
}
Some(tok) => {
// `--` sentinel: skip it and take the token after as literal
let (prompt_text, skip) = if tok == "--" {
match args.get(index + 2) {
Some(t) => (t.as_str(), 3usize),
None => return Err("missing_prompt: -p -- requires a prompt string after `--`.\nUsage: claw -p -- <text>".to_string()),
}
} else {
(tok, 2usize)
};
if prompt_text.trim().is_empty() {
return Err("missing_prompt: -p requires a non-empty prompt string.\nUsage: claw -p <text> or claw prompt <text>".to_string());
}
short_p_prompt = Some(prompt_text.to_string());
index += skip;
continue;
}
}
return Ok(CliAction::Prompt {
prompt,
model: resolve_model_alias_with_config(&model),
output_format,
allowed_tools: normalize_allowed_tools(&allowed_tool_values)?,
permission_mode: permission_mode_override
.unwrap_or_else(default_permission_mode),
compact,
base_commit: base_commit.clone(),
reasoning_effort: reasoning_effort.clone(),
allow_broad_cwd,
});
}
"--print" => {
// Claw Code compat: --print makes output non-interactive
@@ -965,6 +984,21 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
let allowed_tools = normalize_allowed_tools(&allowed_tool_values)?;
// #755: -p consumed exactly one token; dispatch now that all flags are parsed
if let Some(prompt) = short_p_prompt {
return Ok(CliAction::Prompt {
prompt,
model: resolve_model_alias_with_config(&model),
output_format,
allowed_tools,
permission_mode: permission_mode_override.unwrap_or_else(default_permission_mode),
compact,
base_commit,
reasoning_effort,
allow_broad_cwd,
});
}
if rest.is_empty() {
let permission_mode = permission_mode_override.unwrap_or_else(default_permission_mode);
// When stdin is not a terminal (pipe/redirect) and no prompt is given on the

View File

@@ -1504,6 +1504,69 @@ fn prompt_no_arg_json_error_kind_750() {
);
}
#[test]
fn short_p_flag_swallows_no_flags_755() {
// #755: `claw -p hello --output-format json` must parse --output-format json
// as a flag rather than swallowing it as part of the prompt. Before #755,
// args[index+1..].join(" ") consumed all remaining tokens into the prompt.
// After #755, -p consumes exactly one token and remaining flags are parsed.
// We verify by checking that the envelope IS JSON (meaning --output-format json
// was interpreted as a flag, not literal prompt text).
use std::process::Command;
let root = unique_temp_dir("short-p-flags");
fs::create_dir_all(&root).expect("temp dir");
let bin = env!("CARGO_BIN_EXE_claw");
// -p hello --output-format json: with no credentials, should fail with
// missing_credentials (not missing_prompt), proving --output-format json was parsed.
let output = Command::new(bin)
.current_dir(&root)
.args(["-p", "hello", "--output-format", "json"])
.env_remove("ANTHROPIC_API_KEY")
.env_remove("ANTHROPIC_AUTH_TOKEN")
.output()
.expect("claw -p should run");
assert!(
!output.status.success(),
"claw -p hello --output-format json must exit non-zero (no credentials)"
);
let raw = String::from_utf8_lossy(&output.stderr)
.lines()
.filter(|l| l.starts_with('{'))
.collect::<Vec<_>>()
.join("");
// Must be valid JSON (i.e. --output-format json was parsed, not swallowed)
let parsed: serde_json::Value = serde_json::from_str(&raw).unwrap_or_else(|_| {
panic!("--output-format json must be parsed as a flag, not prompt text; stderr: {raw}")
});
assert_eq!(
parsed["error_kind"], "missing_credentials",
"flags after -p prompt text must be parsed normally (#755); got: {parsed}"
);
// Also verify -p --model bogus is rejected as missing_prompt (flag-as-prompt guard)
let output2 = Command::new(bin)
.current_dir(&root)
.args(["--output-format", "json", "-p", "--model", "sonnet"])
.output()
.expect("claw -p flag-as-prompt should run");
let raw2 = String::from_utf8_lossy(&output2.stderr)
.lines()
.filter(|l| l.starts_with('{'))
.collect::<Vec<_>>()
.join("");
let parsed2: serde_json::Value = serde_json::from_str(&raw2)
.unwrap_or_else(|_| panic!("claw -p --model must emit JSON; got: {raw2}"));
assert_eq!(
parsed2["error_kind"], "missing_prompt",
"flag-like token after -p must be rejected as missing_prompt (#755): {parsed2}"
);
assert!(
parsed2["hint"].as_str().map_or(false, |h| !h.is_empty()),
"missing_prompt hint must be non-empty (#755)"
);
}
#[test]
fn short_p_flag_no_arg_json_error_kind_753() {
// #753: `claw --output-format json -p` (no prompt) must emit error_kind:"missing_prompt"