fix(cli): 程序名只替换模板占位符,不再改写用户数据(M82 r2 复审 P2)

r1 把 `rename` 套在**插值后的整条文本**上,于是用户数据被改坏:账户名恰好是 `ull` 会变成
`unlucky-launcher`,路径 `D:/tmp/ull/.ULL/...` 会变成 `D:/tmp/unlucky-launcher/.ULL/...`,
回显的子命令 `ull` 也会被改名。

改成模板占位符:文案里写 `%prog%`,**只在渲染时**(print_report / print_error / 帮助)替换成
argv[0] 的 basename。占位符只存在于静态模板里,插值进来的账户名 / 路径 / 子命令碰不到它。
用 `%prog%` 而不是 `{prog}`:这些模板要活在 `format!` 里,`{prog}` 会被 format! 当成参数名
编译失败。

同时补上 r1 漏的反方向:**stdout 的正常输出**(Report.human 与 warnings)现在也做替换,
以前只有 stderr 的错误做了,两边不一致。

回归测试:`user_data_that_says_ull_is_never_rewritten` 断言「账户名 `ull`」「路径 `\ull\`」
「未知命令 `ull`」在 fill 之后逐字不变。

占位符替换覆盖 21 个文件的 63 处文案;测试夹具里形如 `ull-mcp-diagnose-<pid>` 的临时目录名
已回滚为字面量(那不是文案)。
This commit is contained in:
AskaEth committed 2026-11-02 08:08:13 +08:00
1 parent c9e6236a87
commit 2f8bbd1746
20 files changed
+99 -105

No files matched your search

+19 -19
View File
@@ -7,10 +7,10 @@
#[must_use]
pub const fn general() -> &'static str {
"\
ull · Unlucky-Launcher 命令行(D18)
%prog% · Unlucky-Launcher 命令行(D18)
用法:
ull [全局选项] <命令> [参数]
%prog% [全局选项] <命令> [参数]
命令:
instances list 列出实例与已安装版本
@@ -55,14 +55,14 @@ pub fn topic(name: &str) -> Option<&'static str> {
match name {
"instances" => Some(
"\
ull instances list
%prog% instances list
列出 profiles.json 里的实例与 versions/ 下已安装的版本。
--json 输出 {\"data\":{\"instances\":[…],\"installed_versions\":[…],\"data_root\":…}}",
),
"install" => Some(
"\
ull install <version> [--loader X] [--loader-version V] [--name N] [--dry-run]
%prog% install <version> [--loader X] [--loader-version V] [--name N] [--dry-run]
解析版本清单与继承链 → 组下载计划 → 落盘(versions/ assets/ libraries/)→ 登记实例
(profiles.json + config/instances/<id>.json)。已存在且校验通过的文件会跳过,
@@ -81,7 +81,7 @@ versions/<原版>-<加载器>-<版本>/<…>.json(inheritsFrom 原版 + 主类
),
"launch" => Some(
"\
ull launch <instance> [--dry-run] [--account <name>] [--java <path>] [--username <name>]
%prog% launch <instance> [--dry-run] [--account <name>] [--java <path>] [--username <name>]
[--max-memory <MB>]
<instance> 可以是实例 id、实例名,或直接是版本 id。
@@ -92,10 +92,10 @@ accessToken 一律脱敏(输出里出现的是 <redacted:token>)。",
),
"accounts" => Some(
"\
ull accounts list
ull accounts add offline <name>
ull accounts add msa
ull accounts remove <name>
%prog% accounts list
%prog% accounts add offline <name>
%prog% accounts add msa
%prog% accounts remove <name>
账户存在 <数据根>/.ULL/data/auth/:accounts.json(可公开元数据)与 credentials.dat(凭据)
**分文件**存放,先写凭据后写元数据、原子写、凭据文件 POSIX 侧 0600。
@@ -104,25 +104,25 @@ add msa 走微软 device-code(用户码打在 stderr,去 https://www.microso
需要先配置 ULL_MSA_CLIENT_ID(Azure 公共客户端 id)。
authlib-injector 账户的密码登录留给 GUI(密码进 argv 会泄露,CLI 不开这条通道)。",
),
"sessions" => Some("ull sessions list [--limit N] 列出会话索引(按开始时间倒序,缺省 20 条)"),
"logs" => Some("ull logs <sid> [--tail N] <sid> 允许唯一前缀;输出会话 JSONL 末尾 N 条事件(缺省 200)"),
"sessions" => Some("%prog% sessions list [--limit N] 列出会话索引(按开始时间倒序,缺省 20 条)"),
"logs" => Some("%prog% logs <sid> [--tail N] <sid> 允许唯一前缀;输出会话 JSONL 末尾 N 条事件(缺省 200)"),
"diag" => Some(
"ull diag <sid> [--tail N] 把会话的退出信号与日志喂给规则引擎(内嵌规则集),输出结构化诊断",
"%prog% diag <sid> [--tail N] 把会话的退出信号与日志喂给规则引擎(内嵌规则集),输出结构化诊断",
),
"config" => Some(
"\
ull config get [<key>] [--show-secret]
ull config set <key> <value>
%prog% config get [<key>] [--show-secret]
%prog% config set <key> <value>
读写 <数据根>/.ULL/config/launcher.json,走 core::setting 的 schema(类型/枚举校验、
原子写)。敏感键 get 时脱敏,--show-secret 才给明文;值先按 JSON 解析,失败按字符串。",
),
"java" => Some(
"\
ull java list 探测 JAVA_HOME / PATH / 托管目录 / 实例覆盖里的 Java
%prog% java list 探测 JAVA_HOME / PATH / 托管目录 / 实例覆盖里的 Java
并打印每个 JDK 的实际落点(managed 项带目录)
ull java install <major> [--vendor X] 安装托管 JDK(D13/D23):回退链查版本 → 下载 → 解压 → 验证
ull java vendors 列出四家发行版的镜像/支持版本/当前优先级顺序
%prog% java install <major> [--vendor X] 安装托管 JDK(D13/D23):回退链查版本 → 下载 → 解压 → 验证
%prog% java vendors 列出四家发行版的镜像/支持版本/当前优先级顺序
落点(D23 三模式,随 core.data_mode 与设置键 java.installRoot 走):
portable(默认) <数据根>/.ULL/data/java-managed/jdk-<major>-<vendor>/
@@ -137,8 +137,8 @@ ull java vendors 列出四家发行版的镜像/支持版
),
"agent" => Some(
"\
ull agent serve [--stdio] [--port N] [--host H] [--allow-write|--readonly]
ull agent config [--port N]
%prog% agent serve [--stdio] [--port N] [--host H] [--allow-write|--readonly]
%prog% agent config [--port N]
把 agent 的工具注册表挂成 **MCP server**(02 文档 §1 的第二形态:同一注册表、同一安全策略、
同一审计)。默认走 Streamable HTTP,只绑回环地址(127.0.0.1,默认端口 47823);
+6 -5
View File
@@ -92,7 +92,8 @@ fn parse_command(rest: &[String], globals: &Globals) -> Result<Command, CliError
"accounts" => accounts::parse_accounts(tail),
"agent" => agent::parse_agent(tail),
other => {
Err(CliError::usage(format!("未知命令 `{other}`")).with_hint("ull --help 查看命令列表"))
Err(CliError::usage(format!("未知命令 `{other}`"))
.with_hint("%prog% --help 查看命令列表"))
}
}
}
@@ -194,7 +195,7 @@ fn parse_diag(tail: &[String]) -> Result<Command, CliError> {
fn parse_config(tail: &[String]) -> Result<Command, CliError> {
let Some(sub) = tail.first().map(String::as_str) else {
return Err(CliError::usage("config 需要子命令 get|set")
.with_hint("ull config get <key> / ull config set <key> <value>"));
.with_hint("%prog% config get <key> / %prog% config set <key> <value>"));
};
let rest = tail.get(1..).unwrap_or_default();
match sub {
@@ -212,7 +213,7 @@ fn parse_config(tail: &[String]) -> Result<Command, CliError> {
let parsed = parse_flags(rest, &[], &[])?;
if parsed.positionals.len() != 2 {
return Err(CliError::usage(
"config set 需要键路径与值:ull config set <key> <value>",
"config set 需要键路径与值:%prog% config set <key> <value>",
));
}
Ok(Command::ConfigSet(ConfigSetArgs {
@@ -262,8 +263,8 @@ fn parse_java(tail: &[String]) -> Result<Command, CliError> {
fn require_single_positional(parsed: &ParsedArgs, usage: &str) -> Result<String, CliError> {
match parsed.positionals.as_slice() {
[only] => Ok(only.clone()),
[] => Err(CliError::usage(format!("缺少参数:ull {usage}"))),
_ => Err(CliError::usage(format!("参数过多:ull {usage}"))),
[] => Err(CliError::usage(format!("缺少参数:%prog% {usage}"))),
_ => Err(CliError::usage(format!("参数过多:%prog% {usage}"))),
}
}
+1 -1
View File
@@ -41,7 +41,7 @@ fn parse_add(rest: &[String]) -> Result<Command, CliError> {
let Some(kind) = rest.first().map(String::as_str) else {
return Err(
CliError::usage("accounts add 需要轨道:offline|msa").with_hint(
"ull accounts add offline <玩家名> / ull accounts add msa(设备码交互式登录)",
"%prog% accounts add offline <玩家名> / %prog% accounts add msa(设备码交互式登录)",
),
);
};
+1 -1
View File
@@ -16,7 +16,7 @@ use super::{ensure_no_positionals, parse_flags};
pub fn parse_agent(tail: &[String]) -> Result<Command, CliError> {
let Some(sub) = tail.first().map(String::as_str) else {
return Err(CliError::usage("agent 需要子命令 serve|config")
.with_hint("ull agent serve / ull agent config"));
.with_hint("%prog% agent serve / %prog% agent config"));
};
let rest = tail.get(1..).unwrap_or_default();
match sub {
+35 -46
View File
@@ -4,7 +4,18 @@
//! 「`<名字> --help`」这类提示不能写死 `ull` —— 用户敲的是 `unlucky-launcher`,把他指去敲
//! 一个可能不存在的名字只会让人困惑。
//!
//! 取不到(argv[0] 为空,某些 exec 场景)就回落 [`FALLBACK`],输出因此与老版本逐字相同。
//! 做法(M82 r2 定案):文案里放**占位符**,渲染时替换;**绝不对插值后的整条文本做词替换**。
//! 上一版按「独立词 `ull`」替换整条消息,于是用户数据被改坏过:账户名叫 `ull` 的会变成
//! `unlucky-launcher`,路径 `D:/tmp/ull/.ULL` 会变成 `D:/tmp/unlucky-launcher/.ULL`。
//! 占位符只在模板里出现,插值进来的用户名/路径碰不到它。
//!
//! 取不到 argv[0](某些 exec 场景)就回落 [`FALLBACK`],输出因此与老版本逐字相同。
/// 文案里的程序名占位符。
///
/// 用 `%prog%` 而不是 `{prog}`:这些模板要活在 `format!` 里,`{prog}` 会被 `format!` 当成
/// 参数名直接编译失败,而 `%prog%` 原样穿过去,到渲染时才替换。
pub const PLACEHOLDER: &str = "%prog%";
/// 取不到 argv[0] 时的回落名(也是 `crates/cli` 产出的那个 bin)。
pub const FALLBACK: &str = "ull";
@@ -46,43 +57,21 @@ pub fn name() -> String {
name_in(argv0.as_deref())
}
/// 把文本里**独立出现**的 `ull` 换成程序名;`program` 就是 `ull` 时原样返回。
/// 把模板里的 [`PLACEHOLDER`] 换成程序名。
///
/// 只换两侧都不是 ASCII 字母数字的 `ull`:`ull --help` 里的换,`null` / `pull` 里的不换。
/// 帮助文案因此不用为了改名重写成动态字符串。
/// **只认占位符**:文本里其它位置的 `ull`(账户名、路径段、回显的子命令)原样保留——这是
/// 与「按词替换整条文本」的分界线,也是本函数唯一要保证的事。
#[must_use]
pub fn rename(text: &str, program: &str) -> String {
if program == FALLBACK || !text.contains(FALLBACK) {
pub fn fill(text: &str) -> String {
if !text.contains(PLACEHOLDER) {
return text.to_owned();
}
let mut out = String::with_capacity(text.len() + program.len());
let mut copied = 0;
let mut index = 0;
while let Some(found) = text[index..].find(FALLBACK) {
let start = index + found;
let end = start + FALLBACK.len();
let before_ok = !text[..start]
.chars()
.next_back()
.is_some_and(|ch| ch.is_ascii_alphanumeric());
let after_ok = !text[end..]
.chars()
.next()
.is_some_and(|ch| ch.is_ascii_alphanumeric());
if before_ok && after_ok {
out.push_str(&text[copied..start]);
out.push_str(program);
copied = end;
}
index = end;
}
out.push_str(&text[copied..]);
out
text.replace(PLACEHOLDER, &name())
}
#[cfg(test)]
mod tests {
use super::{name_in, rename, FALLBACK};
use super::{fill, name_in, FALLBACK, PLACEHOLDER};
#[test]
fn argv0_is_reduced_to_a_bare_name() {
@@ -106,26 +95,26 @@ mod tests {
}
#[test]
fn rename_only_touches_standalone_occurrences() {
fn fill_replaces_only_the_placeholder() {
assert_eq!(
rename("ull --help 查看命令列表", "unlucky-launcher"),
"unlucky-launcher --help 查看命令列表"
);
// 别的词里的 ull 不动(防止把 null/pull 改成 n/p + 名字)
assert_eq!(
rename("value is null, pull it", "unlucky-launcher"),
"value is null, pull it"
);
// 中文紧邻不算字母数字,照换
assert_eq!(
rename("用法:ull [全局选项]", "unlucky-launcher"),
"用法:unlucky-launcher [全局选项]"
fill(&format!("{PLACEHOLDER} --help 查看命令列表")),
format!("{} --help 查看命令列表", super::name())
);
// 没有占位符就原样返回(不碰任何词)。
assert_eq!(fill("ull --help"), "ull --help");
}
#[test]
fn rename_is_identity_for_ull() {
let text = "ull --help";
assert_eq!(rename(text, FALLBACK), text);
fn user_data_that_says_ull_is_never_rewritten() {
// 回归:账户名恰好是 `ull` / 路径里有 `\ull\` / 回显的子命令是 `ull`,
// 一个字节都不能被改成程序名(M82 r1 的真缺陷)。
for text in [
"找不到账户 `ull`",
r"索引文件:D:/tmp/ull\.ULL\data\agent\sessions\index.jsonl",
"未知命令 `ull`",
"过滤词 null 与 pull 也不算",
] {
assert_eq!(fill(text), text, "{text} 被改了");
}
}
}
+2 -2
View File
@@ -28,8 +28,8 @@ pub fn list(ctx: &Ctx) -> Result<Report, CliError> {
let mut human = Vec::new();
if store.is_empty() {
human.push("还没有账户。".to_string());
human.push(" 离线:ull accounts add offline <玩家名>".to_string());
human.push(" 微软:ull accounts add msa(设备码交互式登录)".to_string());
human.push(" 离线:%prog% accounts add offline <玩家名>".to_string());
human.push(" 微软:%prog% accounts add msa(设备码交互式登录)".to_string());
} else {
human.push(format!("账户({} 个):", store.accounts().len()));
for meta in store.accounts() {
+1 -1
View File
@@ -122,7 +122,7 @@ pub fn config_report(ctx: &Ctx, port: Option<u16>) -> Result<Report, CliError> {
human.push("Cursor(.cursor/mcp.json):".to_string());
human.extend(pretty_lines(&samples.cursor));
human.push(
"启动:ull agent serve(默认 Streamable HTTP) / ull agent serve --stdio".to_string(),
"启动:%prog% agent serve(默认 Streamable HTTP) / %prog% agent serve --stdio".to_string(),
);
Ok(Report::new(data, human))
}
+1 -1
View File
@@ -53,7 +53,7 @@ fn get_one(ctx: &Ctx, key: &str, show_secret: bool) -> Result<Report, CliError>
let Some(value) = store.effective(key) else {
return Err(
CliError::user("unknown_key", format!("配置键 `{key}` 不在 schema 里"))
.with_hint("`ull config get`(不带键)可以看到全部生效键"),
.with_hint("`%prog% config get`(不带键)可以看到全部生效键"),
);
};
+5 -5
View File
@@ -59,7 +59,7 @@ pub fn resolve(
format!("有 {} 个账户,需要指定用哪个", many.len()),
)
.with_hint(format!(
"ull launch <instance> --account <名字>;候选:{}",
"%prog% launch <instance> --account <名字>;候选:{}",
names_of(&store)
)))
}
@@ -75,7 +75,7 @@ pub fn resolve(
"已选中账户 {},`--username` 只用于没有账户时的离线占位",
meta.display_name()
))
.with_hint("去掉 --username,或用 `ull accounts add offline <名字>` 建账户"));
.with_hint("去掉 --username,或用 `%prog% accounts add offline <名字>` 建账户"));
}
let mut notes = Vec::new();
@@ -103,7 +103,7 @@ fn offline_fallback(username: Option<&str>) -> Identity {
},
account: None,
notes: vec![
"没有账户,按离线占位启动(ull accounts add offline <名字> 可建账户)".to_string(),
"没有账户,按离线占位启动(%prog% accounts add offline <名字> 可建账户)".to_string(),
],
}
}
@@ -120,7 +120,7 @@ fn microsoft_identity(
"account_credentials",
format!("账户 {} 的微软凭据缺失或类型不符", meta.display_name()),
)
.with_hint("重新登录:ull accounts add msa"));
.with_hint("重新登录:%prog% accounts add msa"));
};
if dry_run {
@@ -193,7 +193,7 @@ fn names_of(store: &AccountStore) -> String {
fn unknown_account(key: &str, store: &AccountStore) -> CliError {
let hint = if store.is_empty() {
"还没有账户:ull accounts add offline <名字> 或 ull accounts add msa".to_string()
"还没有账户:%prog% accounts add offline <名字> 或 %prog% accounts add msa".to_string()
} else {
format!("可用账户:{}", names_of(store))
};
+1 -1
View File
@@ -44,7 +44,7 @@ pub(super) fn run(ctx: &Ctx, args: &InstallArgs, kind: LoaderKind) -> Result<Rep
let version = args.version.trim().to_string();
if version.is_empty() {
return Err(CliError::usage(
"install 需要版本 id:ull install <version> --loader fabric",
"install 需要版本 id:%prog% install <version> --loader fabric",
));
}
@@ -107,7 +107,7 @@ pub(super) fn execute(
),
)
.with_hint(format!(
"`ull java install {}` 装一个托管 JDK,或把 JAVA_HOME 指向可用的 JDK(托管目录 {})",
"`%prog% java install {}` 装一个托管 JDK,或把 JAVA_HOME 指向可用的 JDK(托管目录 {})",
plan.java_requirement.minimum,
managed.display()
)));
+2 -2
View File
@@ -113,7 +113,7 @@ pub(super) fn ensure_name_free(ctx: &Ctx, name: &str, version: &str) -> Result<(
"instance_exists",
format!("同名实例已存在:{name}(绑定版本 {})", profile.version_id),
)
.with_hint("用 --name 换个名字,或 `ull instances list` 看看现有实例")),
.with_hint("用 --name 换个名字,或 `%prog% instances list` 看看现有实例")),
None => Ok(()),
}
}
@@ -145,7 +145,7 @@ pub(super) fn failure_error(report: &InstallReport, ctx: &Ctx) -> CliError {
}
}
CliError::user(code, message).with_hint(format!(
"重跑 `ull install` 会续传并跳过已就绪文件;游戏根 {}",
"重跑 `%prog% install` 会续传并跳过已就绪文件;游戏根 {}",
ctx.game().root().display()
))
}
+2 -2
View File
@@ -37,7 +37,7 @@ pub(super) fn run(ctx: &Ctx, args: &InstallArgs) -> Result<Report, CliError> {
let version = args.version.trim().to_string();
if version.is_empty() {
return Err(CliError::usage(
"install 需要版本 id:ull install <version>",
"install 需要版本 id:%prog% install <version>",
));
}
let name = args
@@ -139,7 +139,7 @@ pub(super) fn plan_and_install(
format!("版本 {version} 装不全:{reasons}"),
)
.with_hint(
"旧版(pre-1.6)清单兜底待补;可先用官方启动器装好该版本再用 `ull launch`",
"旧版(pre-1.6)清单兜底待补;可先用官方启动器装好该版本再用 `%prog% launch`",
));
}
let sink = report::progress_sink(progress, json_progress, plan.file_count());
+1 -1
View File
@@ -59,7 +59,7 @@ pub fn list(ctx: &Ctx) -> Result<Report, CliError> {
ctx.data.launcher_home.display()
)];
if body.is_empty() {
human.push(" (空)用 `ull install <version>` 登记第一个实例".to_string());
human.push(" (空)用 `%prog% install <version>` 登记第一个实例".to_string());
}
human.extend(body);
if installed.is_empty() {
+2 -2
View File
@@ -111,7 +111,7 @@ pub fn list(ctx: &Ctx) -> Result<Report, CliError> {
let mut human = vec![format!("Java {} 个", candidates.len())];
if candidates.is_empty() {
human.push(
"(没找到可用 Java)用 `ull java install 21` 装一个,或设 JAVA_HOME / `ull launch --java <path>`"
"(没找到可用 Java)用 `%prog% java install 21` 装一个,或设 JAVA_HOME / `%prog% launch --java <path>`"
.to_string(),
);
}
@@ -209,7 +209,7 @@ pub fn install(ctx: &Ctx, major: u32, vendor: Option<JdkVendor>) -> Result<Repor
for note in installed.chain.degradations() {
human.push(format!("降级:{note}"));
}
human.push("`ull launch` 会自动优先用它".to_string());
human.push("`%prog% launch` 会自动优先用它".to_string());
Ok(Report::new(data, human))
}
+1 -1
View File
@@ -330,7 +330,7 @@ fn map_launch_error(error: &LaunchError, ctx: &Ctx) -> CliError {
fn instance_hint(profiles: &ProfileList) -> String {
if profiles.profiles.is_empty() {
"用 `ull instances list` 查看实例,或 `ull install <version>` 登记一个".to_string()
"用 `%prog% instances list` 查看实例,或 `%prog% install <version>` 登记一个".to_string()
} else {
let names: Vec<&str> = profiles
.profiles
+2 -2
View File
@@ -52,7 +52,7 @@ pub fn welcome() -> Report {
#[must_use]
pub fn help(topic: Option<&str>) -> Report {
let text = match topic.and_then(help::topic) {
Some(text) => program::rename(text, &program::name()),
Some(text) => program::fill(text),
None => general_help(),
};
let data = json!({
@@ -64,5 +64,5 @@ pub fn help(topic: Option<&str>) -> Report {
/// 总帮助,按被调用的二进制名自述(`ull` 自己跑时逐字不变)。
fn general_help() -> String {
program::rename(help::general(), &program::name())
program::fill(help::general())
}
+4 -4
View File
@@ -75,14 +75,14 @@ pub(crate) fn map_auth_error(error: unlucky_core::auth::AuthError) -> CliError {
let hint = match &error {
AuthError::Canceled => Some("登录被取消,可以重跑一次".to_string()),
AuthError::Timeout { .. } => {
Some("重新执行 `ull accounts add msa` 拿新的用户码".to_string())
Some("重新执行 `%prog% accounts add msa` 拿新的用户码".to_string())
}
AuthError::CredentialExpired => Some("重新登录:ull accounts add msa".to_string()),
AuthError::CredentialExpired => Some("重新登录:%prog% accounts add msa".to_string()),
AuthError::Unsupported(_) => {
Some("微软登录需要设置 ULL_MSA_CLIENT_ID(Azure 公共客户端 id)".to_string())
}
AuthError::Conflict { .. } => Some("用 `ull accounts list` 查看已有账户".to_string()),
AuthError::NotFound { .. } => Some("用 `ull accounts list` 查看账户名".to_string()),
AuthError::Conflict { .. } => Some("用 `%prog% accounts list` 查看已有账户".to_string()),
AuthError::NotFound { .. } => Some("用 `%prog% accounts list` 查看账户名".to_string()),
AuthError::Denied { code, .. } if code == "no_profile" || code == "no_license" => {
Some("去 minecraft.net 检查该账号的 Java 版拥有权与档案".to_string())
}
+1 -1
View File
@@ -57,7 +57,7 @@ pub fn list(ctx: &Ctx, limit: usize) -> Result<Report, CliError> {
index.display()
)];
if shown.is_empty() {
human.push(" (空)先用 `ull launch <实例>` 跑一局".to_string());
human.push(" (空)先用 `%prog% launch <实例>` 跑一局".to_string());
}
for entry in &shown {
human.push(format!(
+11 -7
View File
@@ -77,11 +77,11 @@ pub fn print_report(
}
if !quiet {
for line in &outcome.report.human {
writeln!(out, "{line}")?;
writeln!(out, "{}", fill_program(line))?;
}
}
for warning in &outcome.warnings {
writeln!(err, "提示: {warning}")?;
writeln!(err, "提示: {}", fill_program(warning))?;
}
Ok(())
}
@@ -101,16 +101,20 @@ pub fn print_error(
writeln!(out, "{}", serde_json::to_string_pretty(&error.to_json())?)?;
return Ok(());
}
writeln!(err, "错误: {}", rename_program(error.message()))?;
writeln!(err, "错误: {}", fill_program(error.message()))?;
if let Some(hint) = error.hint() {
writeln!(err, "提示: {}", rename_program(hint))?;
writeln!(err, "提示: {}", fill_program(hint))?;
}
Ok(())
}
/// 文案里的 `ull` 按被调用的二进制名显示(M82):`unlucky-launcher lst` 不该把人指去敲 `ull`。
fn rename_program(text: &str) -> String {
crate::cli::program::rename(text, &crate::cli::program::name())
/// 渲染文案里的程序名占位符(M82)。
///
/// **只替换占位符**,不对整条文本做词替换:消息里插值进来的账户名 / 路径 / 回显的子命令
/// 一个字节都不动(r1 曾把账户名 `ull` 改成程序名、把 `D:/tmp/ull/.ULL` 改成
/// `D:/tmp/unlucky-launcher/.ULL`)。
fn fill_program(text: &str) -> String {
crate::cli::program::fill(text)
}
/// 长任务的实时进度行(**只进 stderr**,不参与 [`Report`])。