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.
Merging a selected subcommand enum
When the root CLI parses into a Commands enum, it is possible to derive
ortho_config_macros::SelectedSubcommandMerge and import the
SelectedSubcommandMerge trait from ortho_config to merge the selected
variant in one call, instead of matching only to call load_and_merge() per
branch.
Variants that rely on cli_default_as_absent (because they use
default_value_t) should be annotated with #[ortho_subcommand(with_matches)]
so the merge can consult ArgMatches and treat clap defaults as absent.
To load the global configuration and merge the selected subcommand in one
expression, use load_globals_and_merge_selected_subcommand and supply a
global loader as a closure.
use clap::{CommandFactory, FromArgMatches, Parser, Subcommand};
use ortho_config::{SelectedSubcommandMerge, load_globals_and_merge_selected_subcommand};
#[derive(Parser)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand, ortho_config_macros::SelectedSubcommandMerge)]
enum Commands {
#[ortho_subcommand(with_matches)]
Greet(GreetArgs),
Run(RunArgs),
}
// Placeholder types for the example; real subcommands define fields and derive
// `OrthoConfig`.
struct GreetArgs;
struct RunArgs;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = Cli::command();
let matches = cmd.get_matches();
let cli = Cli::from_arg_matches(&matches)?;
let (_globals, _merged) = load_globals_and_merge_selected_subcommand(
&matches,
cli.command,
|| Ok::<_, std::io::Error>(()),
)?;
Ok(())
}
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 resolved by
load_global_config, which now reuses
HelloWorldCli::compose_layers_from_iter to collect defaults, discovered files
and environment variables before applying CLI overrides. When callers pass
-s/--salutation, the helper clears earlier vector contributions, so CLI input
replaces file or environment values. 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_selected() is invoked on the derived Commands enum.
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.
Treating clap defaults as absent
Non‑Option fields annotated with #[arg(default_value_t = ...)] normally
override configuration files and environment variables because clap always
populates them. The cli_default_as_absent attribute changes this behaviour:
when the user does not explicitly provide a value on the command line, the
field is excluded from the CLI layer so that file and environment values take
precedence.
Add the attribute alongside the matching default attribute:
#[derive(Parser, Deserialize, Serialize, OrthoConfig)]
#[ortho_config(prefix = "APP_")]
struct GreetArgs {
#[arg(long, default_value_t = String::from("!"))]
#[ortho_config(default = String::from("!"), cli_default_as_absent)]
punctuation: String,
}
Precedence with the attribute (lowest to highest):
- Struct default (
#[ortho_config(default = ...)]) - Configuration file
- Environment variable
- Explicit CLI override (e.g.
--punctuation "?")
Without cli_default_as_absent, the clap default would always beat the file
and environment layers. With the attribute, calling greet without
--punctuation allows a [cmds.greet] punctuation = "?" file entry or
APP_CMDS_GREET_PUNCTUATION=? environment variable to win.
When using this attribute, pass the ArgMatches so the crate can inspect
value_source():
let matches = GreetArgs::command().get_matches();
let cli = GreetArgs::from_arg_matches(&matches)?;
let merged = cli.load_and_merge_with_matches(&matches)?;
Clap's value_source() uses argument IDs (the field identifier unless
#[arg(id = "...")] overrides it). This behaviour requires the serde_json
feature (enabled by default).
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.