refactor(cli): 判据收敛到 unlucky_cli::cli,banner 按被调用的二进制名自称(M82 复审)
去重:`looks_like_cli(args) -> bool` 落在 `unlucky_cli::cli`,两个 GUI 的 main.rs 只调用它, 判据从两份变一份。cli crate 里**不**出现 GUI 的 flag 表——语义仍是「首个 token 是裸词就接管; 首个 token 是 flag 时只有解析出真命令才接管,解析失败不接管」。原来两个 main.rs 里的 5+5 条 单测合并成 cli crate 的一份(用例集按复审要求原样搬,含 --screenshot/--renderer/--diff/ --mock/--force-state/--scale-factor、--help/-h/--version/-V、10 个已知子命令、--json 前置、 未知子命令),两个 main.rs 的旧测试已删除。 banner:新增 cli::program 模块,从 argv[0] 的 basename 取程序名(剥目录、剥 .exe、空值回落 ull),把帮助/版本/错误提示里独立出现的 `ull` 换成它——`unlucky-launcher --version` 不再自称 `ull`,也不会把人指去敲 `ull --help`。`run()` 签名未改;`ull` 自己跑时输出逐字不变(替换是 恒等),打包脚本的 `ull --version` 冒烟不受影响。
This commit is contained in:
1 parent
fecff989e7
commit
c9e6236a87
9 files changed
+275
-190
No files matched your search
@@ -82,7 +82,9 @@ Binaries in the workspace, so you can tell them apart:
|
||||
`unlucky-launcher` and `ull` are not duplicates. The GUI binary needs the graphics stack when it
|
||||
opens a window (it `dlopen`s X11 / Wayland / GL, and links `libfontconfig` at load); `ull` needs
|
||||
nothing beyond glibc, so it runs on a server, a CI container or Termux where no window can exist.
|
||||
Pick `ull` when there is no desktop, `unlucky-launcher <command>` when there is.
|
||||
Pick `ull` when there is no desktop, `unlucky-launcher <command>` when there is. Both print the same
|
||||
CLI text and name themselves after the binary you invoked: `unlucky-launcher --help` starts with
|
||||
`unlucky-launcher ·`, `ull --help` starts with `ull ·`.
|
||||
|
||||
### Subcommand dispatch
|
||||
|
||||
|
||||
+3
-1
@@ -72,7 +72,9 @@ Unlucky Launcher(ULL)是一个用 Rust 写的免费开源 Minecraft: Java
|
||||
|
||||
`unlucky-launcher` 与 `ull` 不是重复品:开窗时 GUI 二进制要用图形栈(X11 / Wayland / GL 走
|
||||
`dlopen`,启动时直接链接 `libfontconfig`);`ull` 除了 glibc 什么都不需要,所以在没有窗口的服务器、
|
||||
CI 容器、Termux 上跑得起来。没有桌面就用 `ull`,有桌面就用 `unlucky-launcher <子命令>`。
|
||||
CI 容器、Termux 上跑得起来。没有桌面就用 `ull`,有桌面就用 `unlucky-launcher <子命令>`。两者打出来的
|
||||
CLI 文案是同一套,自称的名字跟着**你敲的那个二进制**走:`unlucky-launcher --help` 首行是
|
||||
`unlucky-launcher ·`,`ull --help` 首行是 `ull ·`。
|
||||
|
||||
### 子命令分发
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
//! - 被子命令命中(`instances` / `install` / `launch` / `accounts` / `sessions` / `logs` /
|
||||
//! `diag` / `config` / `java` / `agent`,或 `--help` / `--version`)→ 直接转给
|
||||
//! `unlucky_cli::run`,**不初始化任何 UI**(不 probe 后端、不扫字体、不碰图形库)。
|
||||
//! 子命令表不在这里抄一份:唯一真相源是 `crates/cli` 的解析器(见 [`is_cli_invocation`])。
|
||||
//! 子命令表不在这里抄一份:唯一真相源是 `crates/cli` 的解析器
|
||||
//! (`unlucky_cli::cli::looks_like_cli`,本文件只调用)。
|
||||
//! - 其余参数(`--software-renderer` / `--renderer` / `--theme` / `--screenshot*` /
|
||||
//! `--diff` …):开窗跑骨架,`--screenshot` 出图后退出;`--diff <a> <b>` 只读两张 PNG
|
||||
//! 做逐像素对比(D30 的 GPU/CPU 几何一致性检查),不开窗。
|
||||
@@ -45,8 +46,11 @@ fn main() -> ExitCode {
|
||||
/// 这条路径上**不构造窗口、不探后端、不扫字体、不依赖任何图形库**(这些库缺失时同一条命令照样
|
||||
/// 跑完,实测见 packaging/README.md §7.4)。`unlucky-launcher-iced launch …` 的耗时因此与首帧无关——
|
||||
/// 这句是结构性判断,**没有重量**(原因见 §10-8)。
|
||||
///
|
||||
/// 「这条命令行算不算 CLI 调用」的判据不在这里重复:`unlucky_cli::cli::looks_like_cli` 是唯一
|
||||
/// 一处(M82 复审去重),两个 GUI 二进制都只调它,改语义不会漂。
|
||||
fn run_cli_if_requested(args: &[String]) -> Option<ExitCode> {
|
||||
if !is_cli_invocation(args) {
|
||||
if !unlucky_cli::cli::looks_like_cli(args) {
|
||||
return None;
|
||||
}
|
||||
let mut stdout = std::io::stdout();
|
||||
@@ -57,30 +61,6 @@ fn run_cli_if_requested(args: &[String]) -> Option<ExitCode> {
|
||||
Some(ExitCode::from(u8::try_from(code).unwrap_or(2)))
|
||||
}
|
||||
|
||||
/// 这条命令行是 CLI 调用还是 GUI 启动?
|
||||
///
|
||||
/// 判据只有一条,**子命令表不在这里重复**:`crates/cli` 的解析器就是唯一真相源,
|
||||
/// 所以新增子命令不需要动这个文件(Slint 版 `crates/app/src/main.rs` 是同一份判据)。
|
||||
///
|
||||
/// - 首个 token 是裸词(不以 `-` 开头)→ 一定是子命令位置:GUI 不接受位置参数,
|
||||
/// 而 `--diff` / `--screenshot` 的取值都在 flag 之后。未知子命令也走这条路,
|
||||
/// 由 CLI 自己报「未知命令 + `--help`」并以 1 退出(静默落到 GUI 会让脚本用户困惑)。
|
||||
/// - 首个 token 是 flag → 只有 CLI 自己解析出一条真命令才算 CLI 调用:`--help` / `--version` /
|
||||
/// `--json instances list` 走 CLI;`--data-dir X` 解析成 `Welcome`(没有子命令)、
|
||||
/// `--renderer` / `--screenshot` / `--diff` 等 GUI 参数干脆解析失败,一律仍走 GUI。
|
||||
/// 注意「解析失败」在这里是 GUI,不是 CLI:`--screenshot out.png` 也会解析失败,把它判成
|
||||
/// CLI 就会把截图脚本打死(这是本函数据以正确的那条边界)。
|
||||
fn is_cli_invocation(args: &[String]) -> bool {
|
||||
match args.first() {
|
||||
None => false,
|
||||
Some(first) if !first.starts_with('-') => true,
|
||||
Some(_) => match unlucky_cli::cli::parse(args) {
|
||||
Ok(invocation) => !matches!(invocation.command, unlucky_cli::cli::Command::Welcome),
|
||||
Err(_) => false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// 开窗路径:解析 GUI 参数 → (`--diff` 走比对)→ 起 UI。
|
||||
fn run_gui() -> Result<ExitCode, String> {
|
||||
let options = Options::from_env()?;
|
||||
@@ -111,66 +91,3 @@ fn run_gui() -> Result<ExitCode, String> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::is_cli_invocation;
|
||||
|
||||
fn cli(args: &[&str]) -> bool {
|
||||
is_cli_invocation(&args.iter().map(|arg| (*arg).to_owned()).collect::<Vec<_>>())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_args_starts_the_gui() {
|
||||
assert!(!cli(&[]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gui_flags_keep_working() {
|
||||
// CI 与验收脚本在用这些参数,一条都不能被当成子命令吃掉。
|
||||
assert!(!cli(&["--software-renderer"]));
|
||||
assert!(!cli(&["--screenshot", "out.png", "--data-dir", "/d/tmp/x"]));
|
||||
assert!(!cli(&["--renderer=gpu", "--theme=dark"]));
|
||||
assert!(!cli(&["--scale-factor", "1.0"]));
|
||||
assert!(!cli(&["--force-state", "hover"]));
|
||||
assert!(!cli(&["--diff", "a.png", "b.png"]));
|
||||
// 只有 --data-dir 时 CLI 解析成 Welcome(没有子命令),仍是 GUI 启动。
|
||||
assert!(!cli(&["--data-dir", "/d/tmp/x"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn known_subcommands_go_to_the_cli() {
|
||||
for args in [
|
||||
vec!["instances", "list"],
|
||||
vec!["install", "1.20.1", "--dry-run"],
|
||||
vec!["launch", "1.20.1", "--dry-run"],
|
||||
vec!["accounts", "list"],
|
||||
vec!["sessions", "list"],
|
||||
vec!["logs", "abc", "--tail", "10"],
|
||||
vec!["diag", "abc"],
|
||||
vec!["config", "get"],
|
||||
vec!["java", "list"],
|
||||
vec!["agent", "config"],
|
||||
// 全局选项可以排在子命令前面
|
||||
vec!["--json", "instances", "list"],
|
||||
] {
|
||||
assert!(cli(&args), "{args:?} 应该交给 CLI");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help_and_version_are_cli_too() {
|
||||
// iced 版以前不认 --version(会直接开窗),现在由 CLI 报版本并退出。
|
||||
assert!(cli(&["--version"]));
|
||||
assert!(cli(&["--help"]));
|
||||
assert!(cli(&["-h"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_subcommand_is_left_to_the_cli_to_reject() {
|
||||
// 语义定案(README 两版同款):未知子命令 → CLI 报「未知命令 + ull --help」并以 1 退出,
|
||||
// 不静默落到 GUI(否则脚本用户会以为参数生效了)。
|
||||
assert!(cli(&["lst"]));
|
||||
assert!(cli(&["intsances", "list"]));
|
||||
}
|
||||
}
|
||||
+6
-89
@@ -16,7 +16,8 @@
|
||||
//! 一个二进制两种用法(M82):`unlucky-launcher` 不带子命令时起 GUI;带子命令
|
||||
//! (`instances` / `install` / `launch` / `accounts` / `sessions` / `logs` / `diag` /
|
||||
//! `config` / `java` / `agent`)时直接转给 `unlucky_cli::run`,**不初始化任何 UI**。
|
||||
//! 子命令表不在这里抄一份——唯一真相源是 `crates/cli` 的解析器(见 [`is_cli_invocation`])。
|
||||
//! 子命令表不在这里抄一份——唯一真相源是 `crates/cli` 的解析器(见
|
||||
//! `unlucky_cli::cli::looks_like_cli`)。
|
||||
//! 零 UI 依赖的 `ull` 仍然保留:它连 fontconfig 都不需要,给服务器 / CI / Termux 用。
|
||||
//!
|
||||
//! 架构分层(本文件只有一个职责:分发 + 把窗口拉起来):
|
||||
@@ -101,8 +102,11 @@ fn run_gui() -> Result<(), slint::PlatformError> {
|
||||
/// 这条路径上**不构造窗口、不探后端、不扫字体、不依赖任何图形库**(这些库缺失时同一条命令照样
|
||||
/// 跑完,实测见 packaging/README.md §7.4)。`unlucky-launcher launch …` 的耗时因此与首帧无关——
|
||||
/// 这句是结构性判断,**没有重量**(原因见 §10-8)。
|
||||
///
|
||||
/// 「这条命令行算不算 CLI 调用」的判据不在这里重复:`unlucky_cli::cli::looks_like_cli` 是唯一
|
||||
/// 一处(M82 复审去重),两个 GUI 二进制都只调它,改语义不会漂。
|
||||
fn run_cli_if_requested(args: &[String]) -> Option<ExitCode> {
|
||||
if !is_cli_invocation(args) {
|
||||
if !unlucky_cli::cli::looks_like_cli(args) {
|
||||
return None;
|
||||
}
|
||||
let mut stdout = std::io::stdout();
|
||||
@@ -113,30 +117,6 @@ fn run_cli_if_requested(args: &[String]) -> Option<ExitCode> {
|
||||
Some(ExitCode::from(u8::try_from(code).unwrap_or(2)))
|
||||
}
|
||||
|
||||
/// 这条命令行是 CLI 调用还是 GUI 启动?
|
||||
///
|
||||
/// 判据只有一条,**子命令表不在这里重复**:`crates/cli` 的解析器就是唯一真相源,
|
||||
/// 所以新增子命令不需要动这个文件(iced 版 `crates/app-iced/src/main.rs` 是同一份判据)。
|
||||
///
|
||||
/// - 首个 token 是裸词(不以 `-` 开头)→ 一定是子命令位置:GUI 不接受位置参数,
|
||||
/// 而 `--diff` / `--screenshot` 的取值都在 flag 之后。未知子命令也走这条路,
|
||||
/// 由 CLI 自己报「未知命令 + `--help`」并以 1 退出(静默落到 GUI 会让脚本用户困惑)。
|
||||
/// - 首个 token 是 flag → 只有 CLI 自己解析出一条真命令才算 CLI 调用:`--help` / `--version` /
|
||||
/// `--json instances list` 走 CLI;`--data-dir X` 解析成 `Welcome`(没有子命令)、
|
||||
/// `--software-renderer` / `--screenshot` / `--diff` 等 GUI 参数干脆解析失败,一律仍走 GUI。
|
||||
/// 注意「解析失败」在这里是 GUI,不是 CLI:`--screenshot out.png` 也会解析失败,把它判成
|
||||
/// CLI 就会把截图脚本打死(这是本函数据以正确的那条边界)。
|
||||
fn is_cli_invocation(args: &[String]) -> bool {
|
||||
match args.first() {
|
||||
None => false,
|
||||
Some(first) if !first.starts_with('-') => true,
|
||||
Some(_) => match unlucky_cli::cli::parse(args) {
|
||||
Ok(invocation) => !matches!(invocation.command, unlucky_cli::cli::Command::Welcome),
|
||||
Err(_) => false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Win7 老机 / 无可用 OpenGL 驱动时的软渲染兜底开关(01c §6 的 Slint 核心卖点)。
|
||||
fn apply_renderer_flag() {
|
||||
let software = std::env::args()
|
||||
@@ -146,66 +126,3 @@ fn apply_renderer_flag() {
|
||||
std::env::set_var("SLINT_BACKEND", "winit-software");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::is_cli_invocation;
|
||||
|
||||
fn cli(args: &[&str]) -> bool {
|
||||
is_cli_invocation(&args.iter().map(|arg| (*arg).to_owned()).collect::<Vec<_>>())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_args_starts_the_gui() {
|
||||
assert!(!cli(&[]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gui_flags_keep_working() {
|
||||
// CI 脚本与人工验收都在用这些参数,一条都不能被当成子命令吃掉。
|
||||
assert!(!cli(&["--software-renderer"]));
|
||||
assert!(!cli(&["--screenshot", "out.png", "--data-dir", "/d/tmp/x"]));
|
||||
assert!(!cli(&["--screenshot-theme", "dark"]));
|
||||
assert!(!cli(&["--renderer=gpu", "--theme=dark"]));
|
||||
assert!(!cli(&["--diff", "a.png", "b.png"]));
|
||||
assert!(!cli(&["--mock"]));
|
||||
// 只有 --data-dir 时 CLI 解析成 Welcome(没有子命令),仍是 GUI 启动。
|
||||
assert!(!cli(&["--data-dir", "/d/tmp/x"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn known_subcommands_go_to_the_cli() {
|
||||
for args in [
|
||||
vec!["instances", "list"],
|
||||
vec!["install", "1.20.1", "--dry-run"],
|
||||
vec!["launch", "1.20.1", "--dry-run"],
|
||||
vec!["accounts", "list"],
|
||||
vec!["sessions", "list"],
|
||||
vec!["logs", "abc", "--tail", "10"],
|
||||
vec!["diag", "abc"],
|
||||
vec!["config", "get"],
|
||||
vec!["java", "list"],
|
||||
vec!["agent", "config"],
|
||||
// 全局选项可以排在子命令前面
|
||||
vec!["--json", "instances", "list"],
|
||||
] {
|
||||
assert!(cli(&args), "{args:?} 应该交给 CLI");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help_and_version_are_cli_too() {
|
||||
// GUI 以前不认 --version(会直接开窗),现在由 CLI 报版本并退出。
|
||||
assert!(cli(&["--version"]));
|
||||
assert!(cli(&["--help"]));
|
||||
assert!(cli(&["-h"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_subcommand_is_left_to_the_cli_to_reject() {
|
||||
// 语义定案(README 两版同款):未知子命令 → CLI 报「未知命令 + ull --help」并以 1 退出,
|
||||
// 不静默落到 GUI(否则脚本用户会以为参数生效了)。
|
||||
assert!(cli(&["lst"]));
|
||||
assert!(cli(&["intsances", "list"]));
|
||||
}
|
||||
}
|
||||
@@ -9,9 +9,101 @@
|
||||
pub mod help;
|
||||
pub mod model;
|
||||
pub mod parse;
|
||||
pub mod program;
|
||||
|
||||
pub use model::{
|
||||
AccountsAddArgs, AccountsAddKind, AgentServeArgs, Command, ConfigGetArgs, ConfigSetArgs,
|
||||
DiagArgs, Globals, InstallArgs, Invocation, LaunchArgs, LogsArgs,
|
||||
};
|
||||
pub use parse::parse;
|
||||
|
||||
/// 这条命令行是 CLI 调用,还是「不该由 CLI 接管」(GUI 启动 / 只有全局选项)?
|
||||
///
|
||||
/// GUI 二进制(`unlucky-launcher` / `unlucky-launcher-iced`)用它做分发判据,`ull` 自己的
|
||||
/// `main` 不用——**判据只有这一处**,两个 GUI 各调一次,改语义不会漂。
|
||||
///
|
||||
/// 规则(刻意不列 GUI 的 flag 表:cli crate 不该知道 GUI 有什么参数):
|
||||
///
|
||||
/// - 首个 token 是裸词(不以 `-` 开头)→ 一定在子命令位置(GUI 不接受位置参数,`--diff` /
|
||||
/// `--screenshot` 的取值都排在 flag 之后)→ 接管。未知子命令也在这里,由 CLI 自己报错。
|
||||
/// - 首个 token 是 flag → 只有当 [`parse`] 解析出一条**真命令**时才算 CLI 调用:`--help` /
|
||||
/// `--version` / `--json instances list` 接管;`--data-dir X` 解析成 [`Command::Welcome`]
|
||||
/// (没有子命令)、GUI 专属参数(`--screenshot` / `--renderer` / `--diff` …)根本解析失败,
|
||||
/// 两种情况都不接管。**「解析失败」必须留在调用方**:`--screenshot out.png` 也会让本解析器
|
||||
/// 报错,把它判成 CLI 就会让 GUI 的截图通道失效。
|
||||
#[must_use]
|
||||
pub fn looks_like_cli(args: &[String]) -> bool {
|
||||
match args.first() {
|
||||
None => false,
|
||||
Some(first) if !first.starts_with('-') => true,
|
||||
Some(_) => match parse(args) {
|
||||
Ok(invocation) => !matches!(invocation.command, Command::Welcome),
|
||||
Err(_) => false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::looks_like_cli;
|
||||
|
||||
fn cli(args: &[&str]) -> bool {
|
||||
looks_like_cli(&args.iter().map(|arg| (*arg).to_owned()).collect::<Vec<_>>())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_args_is_not_a_cli_call() {
|
||||
assert!(!cli(&[]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gui_flags_are_never_taken_over() {
|
||||
// CI 与验收脚本在用这些参数,一条都不能被当成子命令吃掉。
|
||||
assert!(!cli(&["--software-renderer"]));
|
||||
assert!(!cli(&["--screenshot", "out.png", "--data-dir", "/d/tmp/x"]));
|
||||
assert!(!cli(&["--screenshot-theme", "dark"]));
|
||||
assert!(!cli(&["--renderer=gpu", "--theme=dark"]));
|
||||
assert!(!cli(&["--scale-factor", "1.0"]));
|
||||
assert!(!cli(&["--force-state", "hover"]));
|
||||
assert!(!cli(&["--diff", "a.png", "b.png"]));
|
||||
assert!(!cli(&["--mock"]));
|
||||
// 只有 --data-dir 时解析成 Welcome(没有子命令),仍是 GUI 启动。
|
||||
assert!(!cli(&["--data-dir", "/d/tmp/x"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn known_subcommands_are_taken_over() {
|
||||
for args in [
|
||||
vec!["instances", "list"],
|
||||
vec!["install", "1.20.1", "--dry-run"],
|
||||
vec!["launch", "1.20.1", "--dry-run"],
|
||||
vec!["accounts", "list"],
|
||||
vec!["sessions", "list"],
|
||||
vec!["logs", "abc", "--tail", "10"],
|
||||
vec!["diag", "abc"],
|
||||
vec!["config", "get"],
|
||||
vec!["java", "list"],
|
||||
vec!["agent", "config"],
|
||||
// 全局选项可以排在子命令前面
|
||||
vec!["--json", "instances", "list"],
|
||||
] {
|
||||
assert!(cli(&args), "{args:?} 应该交给 CLI");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help_and_version_are_taken_over() {
|
||||
assert!(cli(&["--version"]));
|
||||
assert!(cli(&["--help"]));
|
||||
assert!(cli(&["-h"]));
|
||||
assert!(cli(&["-V"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_subcommands_are_left_to_the_cli_to_reject() {
|
||||
// 语义定案(README 两版同款):未知子命令 → CLI 报「未知命令 + <名字> --help」并以 1 退出,
|
||||
// 不静默落到 GUI(否则脚本用户会以为参数生效了)。
|
||||
assert!(cli(&["lst"]));
|
||||
assert!(cli(&["intsances", "list"]));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
//! 程序名:CLI 输出里自称什么,取决于**被调用的那个二进制**叫什么。
|
||||
//!
|
||||
//! GUI 二进制(`unlucky-launcher`)与 `ull` 共用同一套命令行(M82),所以 banner / 用法行 /
|
||||
//! 「`<名字> --help`」这类提示不能写死 `ull` —— 用户敲的是 `unlucky-launcher`,把他指去敲
|
||||
//! 一个可能不存在的名字只会让人困惑。
|
||||
//!
|
||||
//! 取不到(argv[0] 为空,某些 exec 场景)就回落 [`FALLBACK`],输出因此与老版本逐字相同。
|
||||
|
||||
/// 取不到 argv[0] 时的回落名(也是 `crates/cli` 产出的那个 bin)。
|
||||
pub const FALLBACK: &str = "ull";
|
||||
|
||||
/// 从 argv[0] 求程序名:剥目录、剥 `.exe`、空值回落。
|
||||
///
|
||||
/// Windows 上是 `C:\…\unlucky-launcher.exe`,unix 上是 `./target/release/unlucky-launcher`,
|
||||
/// 两种都要得到 `unlucky-launcher`。
|
||||
#[must_use]
|
||||
pub fn name_in(argv0: Option<&str>) -> String {
|
||||
let Some(raw) = argv0 else {
|
||||
return FALLBACK.to_owned();
|
||||
};
|
||||
let base = raw.rsplit(['/', '\\']).next().unwrap_or(raw);
|
||||
let base = strip_exe(base);
|
||||
if base.is_empty() {
|
||||
FALLBACK.to_owned()
|
||||
} else {
|
||||
base.to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
fn strip_exe(name: &str) -> &str {
|
||||
match name
|
||||
.strip_suffix(".exe")
|
||||
.or_else(|| name.strip_suffix(".EXE"))
|
||||
{
|
||||
Some(stem) => stem,
|
||||
None => name,
|
||||
}
|
||||
}
|
||||
|
||||
/// 当前进程的程序名。
|
||||
#[must_use]
|
||||
pub fn name() -> String {
|
||||
let argv0 = std::env::args_os()
|
||||
.next()
|
||||
.map(|arg| arg.to_string_lossy().into_owned());
|
||||
name_in(argv0.as_deref())
|
||||
}
|
||||
|
||||
/// 把文本里**独立出现**的 `ull` 换成程序名;`program` 就是 `ull` 时原样返回。
|
||||
///
|
||||
/// 只换两侧都不是 ASCII 字母数字的 `ull`:`ull --help` 里的换,`null` / `pull` 里的不换。
|
||||
/// 帮助文案因此不用为了改名重写成动态字符串。
|
||||
#[must_use]
|
||||
pub fn rename(text: &str, program: &str) -> String {
|
||||
if program == FALLBACK || !text.contains(FALLBACK) {
|
||||
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
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{name_in, rename, FALLBACK};
|
||||
|
||||
#[test]
|
||||
fn argv0_is_reduced_to_a_bare_name() {
|
||||
assert_eq!(
|
||||
name_in(Some(r"C:\Program Files\ULL\unlucky-launcher.exe")),
|
||||
"unlucky-launcher"
|
||||
);
|
||||
assert_eq!(
|
||||
name_in(Some("./target/release/unlucky-launcher")),
|
||||
"unlucky-launcher"
|
||||
);
|
||||
assert_eq!(name_in(Some("ull")), "ull");
|
||||
assert_eq!(name_in(Some("/usr/local/bin/ull.exe")), "ull");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_or_missing_argv0_falls_back() {
|
||||
assert_eq!(name_in(None), FALLBACK);
|
||||
assert_eq!(name_in(Some("")), FALLBACK);
|
||||
assert_eq!(name_in(Some("/usr/bin/")), FALLBACK);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_only_touches_standalone_occurrences() {
|
||||
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 [全局选项]"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_is_identity_for_ull() {
|
||||
let text = "ull --help";
|
||||
assert_eq!(rename(text, FALLBACK), text);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
use crate::cli::help;
|
||||
use crate::cli::{help, program};
|
||||
use crate::os;
|
||||
use crate::output::Report;
|
||||
|
||||
@@ -12,18 +12,20 @@ pub const LAUNCHER_NAME: &str = "UnluckyLauncher";
|
||||
/// 版本 + 系统摘要(`ull --version`,D19 §5-2 要求附带 os 摘要)。
|
||||
///
|
||||
/// 探测走 `core::platform::os_info::current()`(进程内缓存),未达基线时附带升级指引。
|
||||
/// 自称的名字取 argv[0](M82):GUI 二进制调进来时是 `unlucky-launcher`,`ull` 自己跑时仍是 `ull`。
|
||||
#[must_use]
|
||||
pub fn version() -> Report {
|
||||
let info = os::detect();
|
||||
let invoked_as = program::name();
|
||||
let data = json!({
|
||||
"name": "ull",
|
||||
"name": invoked_as,
|
||||
"package": env!("CARGO_PKG_NAME"),
|
||||
"version": env!("CARGO_PKG_VERSION"),
|
||||
"os": os::to_json(&info),
|
||||
});
|
||||
let mut human = vec![
|
||||
format!(
|
||||
"ull {}({})",
|
||||
"{invoked_as} {}({})",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
env!("CARGO_PKG_NAME")
|
||||
),
|
||||
@@ -38,20 +40,29 @@ pub fn version() -> Report {
|
||||
pub fn welcome() -> Report {
|
||||
let version_report = version();
|
||||
let mut human = version_report.human;
|
||||
let text = general_help();
|
||||
human.push(String::new());
|
||||
human.extend(help::general().lines().map(str::to_string));
|
||||
human.extend(text.lines().map(str::to_string));
|
||||
let mut data = version_report.data;
|
||||
data["help"] = json!(help::general());
|
||||
data["help"] = json!(text);
|
||||
Report::new(data, human)
|
||||
}
|
||||
|
||||
/// 帮助(`ull help <命令>` 给该命令,未知主题退回总帮助)。
|
||||
#[must_use]
|
||||
pub fn help(topic: Option<&str>) -> Report {
|
||||
let text = topic.and_then(help::topic).unwrap_or_else(help::general);
|
||||
let text = match topic.and_then(help::topic) {
|
||||
Some(text) => program::rename(text, &program::name()),
|
||||
None => general_help(),
|
||||
};
|
||||
let data = json!({
|
||||
"topic": topic,
|
||||
"text": text,
|
||||
});
|
||||
Report::new(data, text.lines().map(str::to_string).collect())
|
||||
}
|
||||
|
||||
/// 总帮助,按被调用的二进制名自述(`ull` 自己跑时逐字不变)。
|
||||
fn general_help() -> String {
|
||||
program::rename(help::general(), &program::name())
|
||||
}
|
||||
@@ -101,13 +101,18 @@ pub fn print_error(
|
||||
writeln!(out, "{}", serde_json::to_string_pretty(&error.to_json())?)?;
|
||||
return Ok(());
|
||||
}
|
||||
writeln!(err, "错误: {}", error.message())?;
|
||||
writeln!(err, "错误: {}", rename_program(error.message()))?;
|
||||
if let Some(hint) = error.hint() {
|
||||
writeln!(err, "提示: {hint}")?;
|
||||
writeln!(err, "提示: {}", rename_program(hint))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 文案里的 `ull` 按被调用的二进制名显示(M82):`unlucky-launcher lst` 不该把人指去敲 `ull`。
|
||||
fn rename_program(text: &str) -> String {
|
||||
crate::cli::program::rename(text, &crate::cli::program::name())
|
||||
}
|
||||
|
||||
/// 长任务的实时进度行(**只进 stderr**,不参与 [`Report`])。
|
||||
///
|
||||
/// 命令的正经输出仍然全部走 [`Report`](stdout、可断言的形状);下载/安装这类分钟级任务
|
||||
|
||||
@@ -11,7 +11,13 @@ fn version_json_has_os_summary() {
|
||||
assert_eq!(code, 0);
|
||||
let value = parse_json(&stdout);
|
||||
assert_eq!(value["ok"], true);
|
||||
assert_eq!(value["data"]["name"], "ull");
|
||||
// M82:自称的名字跟着被调用的二进制走(test harness 里是测试二进制名,`ull` 里是 `ull`);
|
||||
// argv[0] → 名字的映射由 cli::program 的单测钉死。
|
||||
assert_eq!(
|
||||
value["data"]["name"],
|
||||
unlucky_cli::cli::program::name(),
|
||||
"{stdout}"
|
||||
);
|
||||
assert_eq!(value["data"]["version"], env!("CARGO_PKG_VERSION"));
|
||||
// D19/D21:os 对象来自 core::platform::os_info(family + 两档 support)。
|
||||
assert!(value["data"]["os"]["family"].is_string());
|
||||
@@ -23,7 +29,9 @@ fn version_json_has_os_summary() {
|
||||
fn welcome_without_subcommand_prints_version_and_help() {
|
||||
let (code, stdout, _) = run_cli(&[]);
|
||||
assert_eq!(code, 0);
|
||||
assert!(stdout.contains("ull "), "{stdout}");
|
||||
// banner 用被调用的二进制名(M82),不写死 `ull`。
|
||||
let banner = format!("{} ", unlucky_cli::cli::program::name());
|
||||
assert!(stdout.starts_with(&banner), "{stdout}");
|
||||
assert!(stdout.contains("用法:"), "{stdout}");
|
||||
|
||||
let (code, stdout, _) = run_cli(&["--json"]);
|
||||
|
||||
Reference in new issue
Block a user