Give each subcommand its own settings

Version 0.9.1 Updated Sep 04, 2026

Many CLIs have global options plus commands with different configuration. Derive OrthoConfig for each subcommand's argument struct and merge only the selected command:

use clap::{Parser, Subcommand};
use ortho_config::{OrthoConfig, OrthoResult, SubcmdConfigMerge};
use serde::{Deserialize, Serialize};

#[derive(Debug, Parser)]
#[command(name = "acme")]
struct Cli {
    #[command(subcommand)]
    command: Command,
}

#[derive(Debug, Subcommand)]
enum Command {
    Serve(ServeConfig),
}

#[derive(Debug, Default, Parser, Deserialize, Serialize, OrthoConfig)]
#[command(name = "serve")]
#[ortho_config(prefix = "ACME_SERVE_")]
struct ServeConfig {
    #[arg(long)]
    port: Option<u16>,
}

fn main() -> OrthoResult<()> {
    match Cli::parse().command {
        Command::Serve(cli) => {
            let config = cli.load_and_merge()?;
            println!("port={:?}", config.port);
        }
    }
    Ok(())
}

For an enum with many variants, derive SelectedSubcommandMerge and use load_globals_and_merge_selected_subcommand. The generated match keeps the entry point small. Add #[ortho_config(cli_default_as_absent)] to a field when a clap default should not override a value supplied by a file or environment variable.

When a subcommand needs the same hermetic merge boundary, pass a SharedScanEnvSource to load_and_merge_with_sources instead of using the process-backed load_and_merge. For the ServeConfig above, the call is:

Set ACME_SERVE_CMDS_SERVE_PORT to 9000, then bind the map with let environment = Arc::new( MapEnv::new().with_var("ACME_SERVE_CMDS_SERVE_PORT", "9000")); and call let config = cli.load_and_merge_with_sources(environment)?;.

This supplies the ACME_SERVE_CMDS_SERVE_PORT layer while preserving the existing CLI precedence. Import MapEnv and std::sync::Arc when using this pattern.

Some commands only parse arguments and do not participate in configuration loading. Keep those arguments in a separate clap-only type:

use clap::Parser;

#[derive(Debug, Parser)]
struct ParseOnlyArgs {
    #[arg(long)]
    format: Option<String>,
}

let args = ParseOnlyArgs::parse();
println!("format={:?}", args.format);

This parse-only workflow calls clap directly and does not load files or merge environment values. Because ParseOnlyArgs does not derive OrthoConfig, it has no load_and_merge or source-aware merge API. Use the OrthoConfig-derived ServeConfig above when a command needs configuration layering.