Configuration management patterns including file formats, precedence, environment variables, and XDG directories. Use when implementing configuration systems for CLI applications.
Patterns and best practices for managing configuration in command-line applications.
The standard precedence order (lowest to highest priority):
use config::{Config as ConfigBuilder, Environment, File};
pub fn load_config(cli: &Cli) -> Result<Config> {
let mut builder = ConfigBuilder::builder()
// 1. Defaults
.set_default("port", 8080)?
.set_default("host", "localhost")?
.set_default("log_level", "info")?;
// 2. System config (if exists)
builder = builder
.add_source(File::with_name("/etc/myapp/config").required(false));
// 3. User config (if exists)
if let Some(config_dir) = dirs::config_dir() {
builder = builder.add_source(
File::from(config_dir.join("myapp/config.toml")).required(false)
);
}
// 4. Project config (if exists)
builder = builder
.add_source(File::with_name("myapp").required(false))
.add_source(File::with_name(".myapp").required(false));
// 5. CLI-specified config (if provided)
if let Some(config_path) = &cli.config {
builder = builder.add_source(File::from(config_path.as_ref()));
}
// 6. Environment variables
builder = builder.add_source(
Environment::with_prefix("MYAPP")
.separator("_")
.try_parsing(true)
);
// 7. CLI arguments (highest priority)
if let Some(port) = cli.port {
builder = builder.set_override("port", port)?;
}
Ok(builder.build()?.try_deserialize()?)
}
Clear, human-readable, good error messages.
# config.toml
[general]
port = 8080
host = "localhost"
log_level = "info"
[database]
url = "postgresql://localhost/mydb"
pool_size = 10
[features]
caching = true
metrics = false
[[servers]]
name = "primary"
address = "192.168.1.1"
[[servers]]
name = "backup"
address = "192.168.1.2"
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize)]
struct Config {
general: General,
database: Database,
features: Features,
servers: Vec<Server>,
}
#[derive(Debug, Deserialize, Serialize)]
struct General {
port: u16,
host: String,
log_level: String,
}
More concise, supports comments, complex structures.
# config.yaml
general:
port: 8080
host: localhost
log_level: info
database:
url: postgresql://localhost/mydb
pool_size: 10
features:
caching: true
metrics: false
servers:
- name: primary
address: 192.168.1.1
- name: backup
address: 192.168.1.2
Good for programmatic generation, less human-friendly.
{
"general": {
"port": 8080,
"host": "localhost",
"log_level": "info"
},
"database": {
"url": "postgresql://localhost/mydb",
"pool_size": 10
}
}
Follow the XDG Base Directory specification for cross-platform compatibility.
use directories::ProjectDirs;
pub struct AppPaths {
pub config_dir: PathBuf,
pub data_dir: PathBuf,
pub cache_dir: PathBuf,
pub state_dir: PathBuf,
}
impl AppPaths {
pub fn new(app_name: &str) -> Result<Self> {
let proj_dirs = ProjectDirs::from("com", "example", app_name)
.ok_or_else(|| anyhow!("Could not determine project directories"))?;
Ok(Self {
config_dir: proj_dirs.config_dir().to_path_buf(),
data_dir: proj_dirs.data_dir().to_path_buf(),
cache_dir: proj_dirs.cache_dir().to_path_buf(),
state_dir: proj_dirs.state_dir()
.unwrap_or_else(|| proj_dirs.data_dir())
.to_path_buf(),
})
}
pub fn config_file(&self) -> PathBuf {
self.config_dir.join("config.toml")
}
pub fn ensure_dirs(&self) -> Result<()> {
fs::create_dir_all(&self.config_dir)?;
fs::create_dir_all(&self.data_dir)?;
fs::create_dir_all(&self.cache_dir)?;
fs::create_dir_all(&self.state_dir)?;
Ok(())
}
}
Directory locations by platform:
| Platform | Config | Data | Cache | |----------|--------|------|-------| | Linux | ~/.config/myapp | ~/.local/share/myapp | ~/.cache/myapp | | macOS | ~/Library/Application Support/myapp | ~/Library/Application Support/myapp | ~/Library/Caches/myapp | | Windows | %APPDATA%\example\myapp | %APPDATA%\example\myapp | %LOCALAPPDATA%\example\myapp |
Use APPNAME_SECTION_KEY format:
MYAPP_DATABASE_URL=postgresql://localhost/db
MYAPP_LOG_LEVEL=debug
MYAPP_FEATURES_CACHING=true
MYAPP_PORT=9000
#[derive(Parser)]
struct Cli {
/// Database URL (env: MYAPP_DATABASE_URL)
#[arg(long, env = "MYAPP_DATABASE_URL")]
database_url: Option<String>,
/// Log level (env: MYAPP_LOG_LEVEL)
#[arg(long, env = "MYAPP_LOG_LEVEL", default_value = "info")]
log_level: String,
/// Port (env: MYAPP_PORT)
#[arg(long, env = "MYAPP_PORT", default_value = "8080")]
port: u16,
}
Never put secrets in config files. Use environment variables instead.
#[derive(Debug, Deserialize)]
struct Config {
pub host: String,
pub port: u16,
// Loaded from environment only
#[serde(skip)]
pub api_token: String,
}
impl Config {
pub fn load() -> Result<Self> {
let mut config: Config = /* load from file */;
// Sensitive data from env only
config.api_token = env::var("MYAPP_API_TOKEN")
.context("MYAPP_API_TOKEN environment variable required")?;
Ok(config)
}
}
Validate configuration early at load time:
#[derive(Debug, Deserialize)]
struct Config {
pub port: u16,
pub host: String,
pub workers: usize,
}
impl Config {
pub fn validate(&self) -> Result<()> {
// Port range
if !(1024..=65535).contains(&self.port) {
bail!("Port must be between 1024 and 65535, got {}", self.port);
}
// Workers
if self.workers == 0 {
bail!("Workers must be at least 1");
}
let max_workers = num_cpus::get() * 2;
if self.workers > max_workers {
bail!(
"Workers ({}) exceeds recommended maximum ({})",
self.workers,
max_workers
);
}
// Host validation
if self.host.is_empty() {
bail!("Host cannot be empty");
}
Ok(())
}
}
Provide a command to generate a default configuration file:
impl Config {
pub fn default_config() -> Self {
Self {
general: General {
port: 8080,
host: "localhost".to_string(),
log_level: "info".to_string(),
},
database: Database {
url: "postgresql://localhost/mydb".to_string(),
pool_size: 10,
},
features: Features {
caching: true,
metrics: false,
},
}
}
pub fn write_default(path: &Path) -> Result<()> {
let config = Self::default_config();
let toml = toml::to_string_pretty(&config)?;
// Add helpful comments
let content = format!(
"# Configuration file for myapp\n\
# See: https://example.com/docs/config\n\n\
{toml}"
);
fs::write(path, content)?;
Ok(())
}
}
CLI Command:
#[derive(Subcommand)]
enum Commands {
/// Generate a default configuration file
InitConfig {
/// Output path (default: ~/.config/myapp/config.toml)
#[arg(short, long)]
output: Option<PathBuf>,
},
}
fn handle_init_config(output: Option<PathBuf>) -> Result<()> {
let path = output.unwrap_or_else(|| {
AppPaths::new("myapp")
.unwrap()
.config_file()
});
if path.exists() {
bail!("Config file already exists: {}", path.display());
}
Config::write_default(&path)?;
println!("Created config file: {}", path.display());
Ok(())
}
Handle breaking changes in config format:
#[derive(Debug, Deserialize)]
struct ConfigV2 {
version: u32,
#[serde(flatten)]
data: ConfigData,
}
impl ConfigV2 {
pub fn load(path: &Path) -> Result<Self> {
let content = fs::read_to_string(path)?;
let mut config: ConfigV2 = toml::from_str(&content)?;
// Migrate from older versions
match config.version {
1 => {
eprintln!("Migrating config from v1 to v2...");
config = migrate_v1_to_v2(config)?;
// Optionally save migrated config
config.save(path)?;
}
2 => {}, // Current version
v => bail!("Unsupported config version: {}", v),
}
Ok(config)
}
}
Provide examples in help text:
#[derive(Subcommand)]
enum Commands {
/// Show configuration examples
ConfigExamples,
}
fn show_config_examples() {
println!("Configuration Examples:\n");
println!("1. Basic configuration (config.toml):");
println!("{}", r#"
[general]
port = 8080
host = "localhost"
"#);
println!("\n2. Environment variables:");
println!(" MYAPP_PORT=9000");
println!(" MYAPP_DATABASE_URL=postgresql://localhost/db");
println!("\n3. CLI override:");
println!(" myapp --port 9000 --host 0.0.0.0");
println!("\n4. Precedence (highest to lowest):");
println!(" CLI args > Env vars > Config file > Defaults");
}
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer