Build the first layered CLI

Version 0.9.1 Updated Sep 04, 2026

Start with one struct. The prefix is used for environment variables and for the default file-discovery names. A trailing underscore is conventional and keeps names such as ACME_PORT easy to read.

use ortho_config::{OrthoConfig, OrthoResult};
use serde::{Deserialize, Serialize};

#[derive(Debug, Deserialize, Serialize, OrthoConfig)]
#[ortho_config(prefix = "ACME_")]
struct Config {
    #[ortho_config(default = String::from("127.0.0.1"))]
    host: String,

    #[ortho_config(default = 8080, cli_short = 'p')]
    port: u16,

    #[ortho_config(default = String::from("info"))]
    log_level: String,
}

fn main() -> OrthoResult<()> {
    let config = Config::load()?;
    println!(
        "host={} port={} log_level={}",
        config.host, config.port, config.log_level
    );
    Ok(())
}

That one definition provides three spellings for each field:

Rust field Command line Environment TOML
host --host ACME_HOST host
port --port or -p ACME_PORT port
log_level --log-level ACME_LOG_LEVEL log_level

Table 1: Rust fields and their command-line, environment, and TOML names.

Values are merged from lowest to highest precedence:

  1. #[ortho_config(default = ...)] values;
  2. configuration files;
  3. environment variables; and
  4. command-line arguments.

This means a checked-in file can provide team defaults, an environment variable can adapt them for a deployment, and a one-off CLI option can override both.