Subcommand configuration

Version 0.6.0 Updated Nov 12, 2025

Many CLI applications use clap subcommands to perform different operations. OrthoConfig supports per‑subcommand defaults via a dedicated cmds namespace. The helper function load_and_merge_subcommand_for loads defaults for a specific subcommand and merges them beneath the CLI values. The merged struct is returned as a new instance; the original cli struct remains unchanged. CLI fields left unset (None) do not override environment or file defaults, avoiding accidental loss of configuration.

How it works

When a struct derives OrthoConfig, it also implements the associated prefix() method. This method returns the configured prefix string. load_and_merge_subcommand_for(prefix, cli_struct) uses this prefix to build a cmds.<subcommand> section name for the configuration file and an PREFIX_CMDS_SUBCOMMAND_ prefix for environment variables. Configuration is loaded in the same order as global configuration (defaults → file → environment → CLI), but only values in the [cmds.<subcommand>] section or environment variables beginning with PREFIX_CMDS_<SUBCOMMAND>_ are considered.

Example

Suppose an application has a pr subcommand that accepts a reference argument and a repo global option. With OrthoConfig the argument structures might be defined as follows:

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

#[derive(Parser, Deserialize, Serialize, Debug, OrthoConfig, Clone, Default)]
#[ortho_config(prefix = "VK")]               // all variables start with VK
pub struct GlobalArgs {
    pub repo: Option<String>,
}

#[derive(Parser, Deserialize, Serialize, Debug, OrthoConfig, Clone, Default)]
#[ortho_config(prefix = "VK")]               // subcommands share the same prefix
pub struct PrArgs {
    #[arg(required = true)]
    pub reference: Option<String>,            // optional for merging defaults but required on the CLI
}

fn main() -> Result<(), ortho_config::OrthoError> {
    let cli_pr = PrArgs::parse();
    // Merge defaults from [cmds.pr] and VK_CMDS_PR_* over CLI
    let merged_pr = cli_pr.load_and_merge()?;
    println!("PrArgs after merging: {:#?}", merged_pr);
    Ok(())
}

A configuration file might include:

[cmds.pr]
reference = "https://github.com/leynos/mxd/pull/31"

[cmds.issue]
reference = "https://github.com/leynos/mxd/issues/7"

and environment variables could override these defaults:

VK_CMDS_PR_REFERENCE=https://github.com/owner/repo/pull/42
VK_CMDS_ISSUE_REFERENCE=https://github.com/owner/repo/issues/101

Within the vk example repository, the global --repo option is provided via the GlobalArgs struct. A developer can set this globally using the environment variable VK_REPO without passing --repo on every invocation. Subcommands pr and issue load their defaults from the cmds namespace and environment variables. If the reference field is missing in the defaults, the tool continues using the CLI value instead of exiting with an error.

Hello world walkthrough

https://github.com/leynos/ortho-config/tree/main/examples/hello_world

The hello_world example crate demonstrates these patterns in a compact setting. Global options such as --recipient or --salutation are parsed via load_global_config, which layers configuration files and environment variables beneath any CLI overrides. The greet subcommand adds optional behaviour like a preamble (--preamble "Good morning") or custom punctuation while reusing the merged global configuration. The take-leave subcommand combines switches and optional arguments (--wave, --gift, --channel email, --remind-in 15) alongside greeting adjustments (--preamble "Until next time", --punctuation ?) to describe how the farewell should unfold. Each subcommand struct derives OrthoConfig so defaults from [cmds.greet] or [cmds.take-leave] merge automatically when load_and_merge() is called.

Behavioural tests in examples/hello_world/tests exercise scenarios such as hello_world greet --preamble "Good morning" and running hello_world --is-excited take-leave with --gift biscuits, --remind-in 15, --channel email, and --wave. These end-to-end checks verify that CLI arguments override configuration files and that validation errors surface cleanly when callers provide blank strings or conflicting switches.

Sample configuration files live in examples/hello_world/config. The baseline.toml defaults underpin both the automated tests and the demo scripts, while overrides.toml extends the baseline to demonstrate inheritance by adjusting the recipient and salutation. The paired scripts/demo.sh and scripts/demo.cmd helpers copy these files into a temporary directory before running cargo run -p hello_world, illustrating how file defaults, environment variables, and CLI arguments override one another without mutating the working tree.

Dispatching with `clap‑dispatch`

The clap‑dispatch crate can be combined with OrthoConfig to simplify subcommand execution. Each subcommand struct implements a trait defining the action to perform. An enum of subcommands is annotated with #[clap_dispatch(fn run(...))], and the load_and_merge_subcommand_for function can be called on each variant before dispatching. See the Subcommand Configuration section of the OrthoConfig README for a complete example.