09 / 10 · 20 分钟
加上子命令
cargo run -- brief 打出带均速的简报。
你的项目里会动到:Cargo.toml · src/main.rs
这一章不新建文件,也不改 lib.rs。给 Cargo.toml 加上 clap,再重写 main.rs。库仍然负责文本,二进制只解析参数、打印、把错误写到 stderr。
和 serde 放在同一个 [dependencies] 里。版本同样锁死。
clap = { version = "=4.5.23", features = ["derive"] }子命令是一个 enum。增加一种用法,就增加一个变体,编译器会要求 match 写到它。没有参数时打印帮助并以非 0 退出,所以 arg_required_else_help = true。省略 --file 时用灯塔山脊样例。
这一章有 brief、story、export、check。report 下一章再加。
#[derive(Subcommand)]
enum Commands {
/// 一行简报。省略 --file 时使用灯塔山脊样例
Brief {
#[arg(short, long)]
file: Option<PathBuf>,
},
/// 叙述简报
Story {
#[arg(short, long)]
file: Option<PathBuf>,
},
/// 把灯塔山脊样例写成 JSON
Export {
#[arg(short, long, default_value = "lantern.json")]
out: PathBuf,
},
/// 检查日志能否通过不变量
Check {
#[arg(short, long)]
file: Option<PathBuf>,
},
}错误写成 fieldlog: 开头,写到 stderr。这样能分清是程序的失败,还是 shell 的失败。check 成功时打印 ok 灯塔山脊 5 天 口粮剩余 2。
cargo run -- brief
cargo run -- story
cargo run -- export --out lantern.json
cargo run -- check --file lantern.json这一章结束时的 main.rs。还没有 report 子命令。
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use clap::{Parser, Subcommand};
use fieldlog::briefing::{Briefing, NarrativeBrief, TerseBrief};
use fieldlog::error::FieldError;
use fieldlog::model::Journal;
use fieldlog::sample::lantern_traverse;
use fieldlog::store::{self, load};
#[derive(Parser)]
#[command(
name = "fieldlog",
version,
about = "把一次穿越记成可以测试的野外日志",
arg_required_else_help = true
)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
// region: commands
#[derive(Subcommand)]
enum Commands {
/// 一行简报。省略 --file 时使用灯塔山脊样例
Brief {
#[arg(short, long)]
file: Option<PathBuf>,
},
/// 叙述简报
Story {
#[arg(short, long)]
file: Option<PathBuf>,
},
/// 把灯塔山脊样例写成 JSON
Export {
#[arg(short, long, default_value = "lantern.json")]
out: PathBuf,
},
/// 检查日志能否通过不变量
Check {
#[arg(short, long)]
file: Option<PathBuf>,
},
}
// endregion: commands
fn main() -> ExitCode {
match run(Cli::parse()) {
Ok(()) => ExitCode::SUCCESS,
Err(err) => {
eprintln!("fieldlog: {err}");
ExitCode::from(1)
}
}
}
fn run(cli: Cli) -> Result<(), FieldError> {
match cli.command {
Commands::Brief { file } => {
let journal = open(file.as_deref())?;
print!("{}", TerseBrief.render(&journal));
}
Commands::Story { file } => {
let journal = open(file.as_deref())?;
print!("{}", NarrativeBrief.render(&journal));
}
Commands::Export { out } => {
store::save(&lantern_traverse(), &out)?;
println!("已写出 {}", out.display());
}
Commands::Check { file } => {
let journal = open(file.as_deref())?;
println!(
"ok {} {} 天 口粮剩余 {}",
journal.expedition,
journal.days().len(),
journal.rations_remaining()
);
}
}
Ok(())
}
fn open(file: Option<&Path>) -> Result<Journal, FieldError> {
match file {
Some(path) => load(path),
None => Ok(lantern_traverse()),
}
}灯塔山脊 2026-09-18 到 2026-09-22 · 5 天 47.2 km · 爬升 2240 m · 15 小时 30 分 · 均速 3.0 km/h 口粮剩余 2 顿 咖啡只够 2 个早晨,还差 70 g。 最长远的一天:2026-09-19 风口营地(12.1 km) 最陡的一天:2026-09-21 灯塔石(爬升 890 m)
检查点
cargo test --test journey
brief、story、export、check 通过。不存在的文件让进程失败,stderr 以 fieldlog: 开头。不带子命令时,帮助里能看到 brief 和 story。