diff --git a/crates/cli/src/error.rs b/crates/cli/src/error.rs index ca2f138..9416e32 100644 --- a/crates/cli/src/error.rs +++ b/crates/cli/src/error.rs @@ -56,6 +56,14 @@ pub struct CliError { hint: Option, } +/// 把模板里的程序名占位符渲染掉(M82 r3)。 +/// +/// 渲染放在**构造时**,而不是打印时:错误有两条出口(人类可读 stderr、`--json` 信封), +/// 只在打印处渲染会漏掉 JSON 那条(r2 就是这么漏的,`%prog%` 直接出现在了 JSON 里)。 +fn render(text: impl Into) -> String { + crate::cli::program::fill(&text.into()) +} + impl CliError { /// 用法错误(参数不对)。 #[must_use] @@ -63,7 +71,7 @@ impl CliError { Self { kind: ErrorKind::User, code: "usage", - message: message.into(), + message: render(message), hint: None, } } @@ -74,7 +82,7 @@ impl CliError { Self { kind: ErrorKind::User, code, - message: message.into(), + message: render(message), hint: None, } } @@ -85,7 +93,7 @@ impl CliError { Self { kind: ErrorKind::Internal, code, - message: message.into(), + message: render(message), hint: None, } } @@ -99,7 +107,7 @@ impl CliError { /// 补一句提示。 #[must_use] pub fn with_hint(mut self, hint: impl Into) -> Self { - self.hint = Some(hint.into()); + self.hint = Some(render(hint)); self } diff --git a/crates/cli/src/output.rs b/crates/cli/src/output.rs index 6affb07..f08ba20 100644 --- a/crates/cli/src/output.rs +++ b/crates/cli/src/output.rs @@ -20,10 +20,13 @@ pub struct Report { } impl Report { - /// 构造。 + /// 构造(人类可读行在这里就把程序名占位符渲染掉,见 [`crate::cli::program::PLACEHOLDER`])。 #[must_use] pub fn new(data: Value, human: Vec) -> Self { - Self { data, human } + Self { + data, + human: human.into_iter().map(|line| fill_program(&line)).collect(), + } } } @@ -46,10 +49,16 @@ impl Outcome { } } - /// 带上环境提示。 + /// 带上环境提示(同样在构造时渲染占位符,JSON 与人类可读两条出口才不会分叉)。 #[must_use] pub fn with_warnings(report: Report, warnings: Vec) -> Self { - Self { report, warnings } + Self { + report, + warnings: warnings + .into_iter() + .map(|line| fill_program(&line)) + .collect(), + } } } @@ -77,11 +86,11 @@ pub fn print_report( } if !quiet { for line in &outcome.report.human { - writeln!(out, "{}", fill_program(line))?; + writeln!(out, "{line}")?; } } for warning in &outcome.warnings { - writeln!(err, "提示: {}", fill_program(warning))?; + writeln!(err, "提示: {warning}")?; } Ok(()) } @@ -101,9 +110,9 @@ pub fn print_error( writeln!(out, "{}", serde_json::to_string_pretty(&error.to_json())?)?; return Ok(()); } - writeln!(err, "错误: {}", fill_program(error.message()))?; + writeln!(err, "错误: {}", error.message())?; if let Some(hint) = error.hint() { - writeln!(err, "提示: {}", fill_program(hint))?; + writeln!(err, "提示: {hint}")?; } Ok(()) } @@ -134,7 +143,10 @@ pub fn progress_line(text: &str) { #[cfg(test)] mod tests { - use super::progress_line; + use super::{print_report, progress_line, Outcome, Report}; + use crate::cli::program::{self, PLACEHOLDER}; + use crate::error::CliError; + use serde_json::json; #[test] fn progress_line_never_panics() { @@ -142,4 +154,46 @@ mod tests { progress_line("测试进度 1/2"); progress_line(""); } + + #[test] + fn json_error_envelope_never_leaks_the_placeholder() { + // r2 的回归:JSON 分支直接序列化未渲染的 message/hint,`%prog%` 原样漏给脚本。 + let error = CliError::usage(format!("{PLACEHOLDER} 需要一个值")) + .with_hint(format!("{PLACEHOLDER} --help 查看命令列表")); + let envelope = serde_json::to_string(&error.to_json()).expect("序列化"); + assert!(!envelope.contains(PLACEHOLDER), "{envelope}"); + assert!(envelope.contains(&program::name()), "{envelope}"); + } + + #[test] + fn json_warnings_never_leak_the_placeholder() { + // warnings 是第二条 JSON 出口(以前连人类可读那条都没走到)。 + let report = Report::new( + json!({ "ok": true }), + vec![format!("{PLACEHOLDER} instances list 看看现有实例")], + ); + let outcome = + Outcome::with_warnings(report, vec![format!("用 {PLACEHOLDER} install 装版本")]); + let mut out: Vec = Vec::new(); + let mut err: Vec = Vec::new(); + print_report(&outcome, true, false, &mut out, &mut err).expect("写 JSON"); + let text = String::from_utf8_lossy(&out).into_owned(); + assert!(!text.contains(PLACEHOLDER), "{text}"); + assert!(text.contains(&program::name()), "{text}"); + } + + #[test] + fn human_lines_render_the_placeholder_too() { + let report = Report::new( + json!({}), + vec![format!("用 `{PLACEHOLDER} install` 登记实例")], + ); + assert_eq!( + report.human[0], + format!("用 `{} install` 登记实例", program::name()) + ); + // 用户数据不受影响:账户名恰好叫 ull 的那种 + let report = Report::new(json!({}), vec!["找不到账户 `ull`".to_string()]); + assert_eq!(report.human[0], "找不到账户 `ull`"); + } }