# clapfig
Rich, layered configuration for Rust CLI apps. Define a struct, point at your files, and go.
**clapfig** orchestrates configuration from multiple sources — config files, environment variables, and CLI flags — through a builder API that takes a few lines to set up. Built on [confique](https://github.com/LukasKalbertodt/confique) for struct-driven defaults and commented template generation.
## Features
- **Struct as source of truth** — define settings as a Rust struct with defaults and `///` doc comments
- **Layered merge** — defaults < config files < env vars < CLI flags, every layer sparse
- **Multi-path file search** — platform config dir, home, cwd, or any path, in precedence order
- **Prefix-based env vars** — `MYAPP__DATABASE__URL` maps to `database.url` automatically
- **Clap override** — map individual clap args to config keys in one call each
- **Strict mode** — unknown keys in config files error with file path, key name, and line number (on by default)
- **Template generation** — `config gen` emits a commented sample config derived from the struct's doc comments
- **Config subcommand** — drop-in `config gen|get|set` commands for clap
- **Persistence** — `config set` patches values in place, preserving file comments
## Quick Start
Define your config with confique's `Config` derive:
```rust
use confique::Config;
use serde::{Serialize, Deserialize};
#[derive(Config, Serialize, Deserialize, Debug)]
pub struct AppConfig {
/// The host address to bind to.
#[config(default = "127.0.0.1")]
pub host: String,
/// The port number.
#[config(default = 8080)]
pub port: u16,
/// Database settings.
#[config(nested)]
pub database: DbConfig,
}
#[derive(Config, Serialize, Deserialize, Debug)]
pub struct DbConfig {
/// Connection string URL.
pub url: Option<String>,
/// Connection pool size.
#[config(default = 10)]
pub pool_size: usize,
}
```
Load it in one line:
```rust
use clapfig::Clapfig;
fn main() -> anyhow::Result<()> {
let config: AppConfig = Clapfig::builder()
.app_name("myapp")
.load()?;
println!("Listening on {}:{}", config.host, config.port);
Ok(())
}
```
That `app_name("myapp")` call sets sensible defaults:
- Searches for `myapp.toml` in the platform config directory
- Merges env vars prefixed with `MYAPP__`
- Fills in `#[config(default)]` values for anything not provided
## Setup
### Defaults from `app_name`
| File name | `{app_name}.toml` |
| Search paths | Platform config dir (via [`directories`](https://docs.rs/directories)) |
| Env prefix | `{APP_NAME}` (uppercased) |
### Builder methods
```rust
use clapfig::{Clapfig, SearchPath};
let config: AppConfig = Clapfig::builder()
// Required — sets defaults for file_name, search_paths, env_prefix
.app_name("myapp")
// Optional overrides
.file_name("settings.toml") // override config file name
.search_paths(vec![SearchPath::Platform, SearchPath::Cwd]) // replace default search paths
.add_search_path(SearchPath::Cwd) // append a path without replacing
.env_prefix("MY_APP") // override env var prefix
.no_env() // disable env var loading entirely
.strict(false) // disable strict mode (allow unknown keys)
.cli_override("host", some_value) // override a key from a CLI arg
.load()?;
```
### Search Paths
```rust
use clapfig::{Clapfig, SearchPath};
let config: AppConfig = Clapfig::builder()
.app_name("myapp")
.search_paths(vec![
SearchPath::Platform, // ~/.config/myapp/ on Linux
// ~/Library/Application Support/myapp/ on macOS
SearchPath::Home(".myapp"), // ~/.myapp/
SearchPath::Cwd, // ./
SearchPath::Path("/etc/myapp".into()), // explicit absolute path
])
.load()?;
```
Files load in order. **Later paths override earlier ones.** A `myapp.toml` in `./` overrides one in `~/.config/myapp/`, which overrides compiled-in defaults.
If a file doesn't exist at a given path, it's silently skipped.
### Strict Mode
Strict mode is **on by default**. If a config file contains a key that doesn't match any field in your struct, loading fails with a clear error including the file path, key name, and line number:
```
Unknown key 'typo_key' in /home/user/.config/myapp/myapp.toml (line 5)
```
Disable it with `.strict(false)` if you want to allow extra keys.
## Environment Variables
With env prefix `MYAPP`, variables map via double-underscore nesting:
| `MYAPP__HOST` | `host` |
| `MYAPP__PORT` | `port` |
| `MYAPP__DATABASE__URL` | `database.url` |
| `MYAPP__DATABASE__POOL_SIZE` | `database.pool_size` |
`__` (double underscore) separates nesting levels. Single `_` within a segment is literal (part of the field name).
Disable env loading entirely with `.no_env()`.
## Clap Integration
### CLI Overrides
#### Auto-matching
If your clap struct derives `Serialize`, `cli_overrides_from` auto-matches fields by name against config keys:
```rust
use clap::Parser;
use serde::Serialize;
#[derive(Parser, Serialize)]
struct Cli {
#[command(subcommand)]
#[serde(skip)]
command: Commands,
#[arg(long)]
host: Option<String>,
#[arg(long)]
port: Option<i64>,
#[arg(long)]
db_url: Option<String>,
}
fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
let config: AppConfig = Clapfig::builder()
.app_name("myapp")
.cli_overrides_from(&cli) // auto-matches host, port
.cli_override("database.url", cli.db_url) // manual: name doesn't match
.load()?;
Ok(())
}
```
`cli_overrides_from(source)` serializes the source, skips `None` values, and keeps only keys that match a config field. Non-matching fields (`command`, `db_url`) are silently ignored. Works with any `Serialize` type — structs, `HashMap`s, etc.
#### Manual overrides
For fields where the CLI name differs from the config key, use `cli_override`:
```rust
.cli_override("database.url", cli.db_url)
```
`cli_override(key, value)` takes `Option<V>` where `V: Into<toml::Value>` — `None` is silently skipped. Dot notation addresses nested keys.
Both methods compose freely and push to the same override list. Later calls take precedence.
Supported value types: `String`, `&str`, `i64`, `i32`, `i8`, `u8`, `u32`, `f64`, `f32`, `bool`.
### Config Subcommand
Add config management to your CLI by nesting `clapfig::ConfigArgs`:
```rust
use clap::Subcommand;
use clapfig::{Clapfig, ConfigArgs, ConfigResult};
#[derive(Subcommand)]
enum Commands {
/// Run the application
Run,
/// Manage configuration
Config(ConfigArgs),
}
fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
match cli.command {
Commands::Config(args) => {
let action = args.into_action();
let result = Clapfig::builder::<AppConfig>()
.app_name("myapp")
.handle(&action)?;
match result {
ConfigResult::Template(t) => print!("{t}"),
ConfigResult::KeyValue { key, value, doc } => {
for line in &doc { println!("# {line}"); }
println!("{key} = {value}");
}
ConfigResult::ValueSet { key, value } => {
println!("Set {key} = {value}");
}
}
}
Commands::Run => {
let config: AppConfig = Clapfig::builder()
.app_name("myapp")
.cli_override("host", cli.host)
.cli_override("port", cli.port)
.load()?;
println!("Listening on {}:{}", config.host, config.port);
}
}
Ok(())
}
```
This gives your users:
```sh
myapp config gen # print commented sample config to stdout
myapp config gen -o myapp.toml # write to file
myapp config get database.url # print the resolved value of a key
myapp config set port 3000 # persist a value to the user's config file
```
## Template Generation
`config gen` produces a commented TOML file derived from your struct's `///` doc comments:
```toml
# The host address to bind to.
# Default: "127.0.0.1"
#host = "127.0.0.1"
# The port number.
# Default: 8080
#port = 8080
[database]
# Connection string URL.
#url =
# Connection pool size.
# Default: 10
#pool_size = 10
```
The template stays in sync with code — it's generated from the same struct. Change a doc comment or a default, the template reflects it.
## Layer Precedence
```
Compiled defaults #[config(default = ...)]
↑ overridden by
Config files search paths in order, later paths win
↑ overridden by
Environment vars MYAPP__KEY
↑ overridden by
CLI overrides .cli_override()
```
Every layer is **sparse**. You only specify the keys you want to override. Unset keys fall through to the next layer down.
## Persistence
`config set <key> <value>` writes to the primary config file (first resolved search path by default).
- If the file exists, the key is patched in place using [`toml_edit`](https://docs.rs/toml_edit), **preserving existing comments and formatting**.
- If the file doesn't exist, a fresh config is created from the generated template with the target key set.
## Full Example
```rust
use clap::{Parser, Subcommand};
use confique::Config;
use serde::{Serialize, Deserialize};
use clapfig::{Clapfig, ConfigArgs, ConfigResult, SearchPath};
// -- Config struct --
#[derive(Config, Serialize, Deserialize, Debug)]
pub struct AppConfig {
/// The host address to bind to.
#[config(default = "127.0.0.1")]
pub host: String,
/// The port number.
#[config(default = 8080)]
pub port: u16,
/// Database settings.
#[config(nested)]
pub database: DbConfig,
}
#[derive(Config, Serialize, Deserialize, Debug)]
pub struct DbConfig {
/// Connection string URL.
pub url: Option<String>,
/// Connection pool size.
#[config(default = 10)]
pub pool_size: usize,
}
// -- CLI --
#[derive(Parser, Serialize)]
#[command(name = "myapp")]
struct Cli {
#[command(subcommand)]
#[serde(skip)]
command: Commands,
#[arg(long, global = true)]
host: Option<String>,
#[arg(long, global = true)]
port: Option<i64>,
}
#[derive(Subcommand)]
enum Commands {
Run,
Config(ConfigArgs),
}
// -- Main --
fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
match cli.command {
Commands::Config(args) => {
Clapfig::builder::<AppConfig>()
.app_name("myapp")
.add_search_path(SearchPath::Cwd)
.handle_and_print(&args.into_action())?;
}
Commands::Run => {
let config: AppConfig = Clapfig::builder()
.app_name("myapp")
.add_search_path(SearchPath::Cwd)
.cli_overrides_from(&cli)
.load()?;
println!("Listening on {}:{}", config.host, config.port);
if let Some(url) = &config.database.url {
println!("Database: {}", url);
}
}
}
Ok(())
}
```