rust101-fieldlog

09 / 10 · 20 分钟

加上子命令

cargo run -- brief 打出带均速的简报。

你的项目里会动到:Cargo.toml · src/main.rs

这一章不新建文件,也不改 lib.rs。给 Cargo.toml 加上 clap,再重写 main.rs。库仍然负责文本,二进制只解析参数、打印、把错误写到 stderr。

和 serde 放在同一个 [dependencies] 里。版本同样锁死。

Cargo.toml
clap = { version = "=4.5.23", features = ["derive"] }

子命令是一个 enum。增加一种用法,就增加一个变体,编译器会要求 match 写到它。没有参数时打印帮助并以非 0 退出,所以 arg_required_else_help = true。省略 --file 时用灯塔山脊样例。

这一章有 brief、story、export、check。report 下一章再加。

src/main.rs
#[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 子命令。

src/main.rs
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()),
    }
}
cargo run -- brief
灯塔山脊
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)

检查点

在你的 fieldlog 目录
cargo test --test journey

brief、story、export、check 通过。不存在的文件让进程失败,stderr 以 fieldlog: 开头。不带子命令时,帮助里能看到 brief 和 story。