Clap Subcommand Config File Loading

Define a clap CLI with typed subcommands, layered defaults, optional config file overrides, and environment variable fallbacks.

RustConfig Management

Code

snippet
use clap::{Arg, ArgAction, Command};
use std::collections::HashMap;

#[derive(Debug)]
struct SyncOptions {
    workers: u32,
    region: String,
    dry_run: bool,
}

fn build_cli() -> Command {
    Command::new("deployctl")
        .about("deployment control tool")
        .subcommand(
            Command::new("sync")
                .about("sync catalog to a region")
                .arg(
                    Arg::new("workers")
                        .short('w')
                        .long("workers")
                        .env("DEPLOY_WORKERS")
                        .default_value("4")
                        .value_parser(clap::value_parser!(u32).range(1..=64)),
                )
                .arg(
                    Arg::new("region")
                        .short('r')
                        .long("region")
                        .env("DEPLOY_REGION")
                        .default_value("eu"),
                )
                .arg(
                    Arg::new("dry-run")
                        .long("dry-run")
                        .action(ArgAction::SetTrue),
                ),
        )
        .subcommand(
            Command::new("status").about("show sync status"),
        )
}

fn load_file_overrides(path: &str) -> HashMap<String, String> {
    let mut map = HashMap::new();
    if let Ok(content) = std::fs::read_to_string(path) {
        for line in content.lines() {
            if let Some((key, value)) = line.split_once('=') {
                map.insert(key.trim().to_string(), value.trim().to_string());
            }
        }
    }
    map
}

fn resolve(matches: &clap::ArgMatches, file: &HashMap<String, String>) -> SyncOptions {
    let flag_workers: u32 = *matches.get_one("workers").unwrap_or(&4);

    // 显式传参 > 配置文件 > clap 默认值(含环境变量)
    let workers = if matches.value_source("workers")
        == Some(clap::builder::ValueSource::CommandLine)
    {
        flag_workers
    } else if let Some(from_file) = file.get("workers").and_then(|v| v.parse().ok()) {
        from_file
    } else {
        flag_workers
    };

    let region = matches
        .get_one::<String>("region")
        .cloned()
        .unwrap_or_else(|| "eu".into());

    SyncOptions {
        workers,
        region,
        dry_run: matches.get_flag("dry-run"),
    }
}

fn main() {
    let cli = build_cli();
    let matches = cli.get_matches();

    match matches.subcommand() {
        Some(("sync", sub)) => {
            let file = load_file_overrides("deployctl.conf");
            let options = resolve(sub, &file);
            println!("{options:?}");
        }
        Some(("status", _)) => println!("status: idle"),
        _ => unreachable!("clap 保证子命令必选"),
    }
}