Why dotcfg?
Most config crates either lock you into a fixed directory strategy or only handle reading. dotcfg gives you full control:
- Choose
~/.toolname/,~/.config/toolname/, or any custom directory — your call - TOML, JSON or YAML — feature-gated, include only what you need
- Load the whole config or get/set individual keys without touching the rest
- Read and write keys as real types — numbers, bools, arrays, structs — not just strings
- Returns
Noneif config doesn't exist — no magic, no forced defaults unless you want them
Comparison with confy
| Feature | dotcfg |
confy |
|---|---|---|
Dot-dir support (~/.app/) |
✅ Built-in default | ❌ (XDG/Native only) |
XDG directory (~/.config/app/) |
✅ via .xdg() |
✅ |
Custom directory (at_dir) |
✅ any path, exactly as given | ❌ |
Project discovery (find_in_ancestors) |
✅ walks from current dir, nearest wins | ❌ |
| Ad-hoc key get/set by string path | ✅ (cfg.get("user.name")) |
❌ (Full struct only) |
| Typed key get/set | ✅ (cfg.get_as::<u16>("port")) |
❌ (Full struct only) |
| Full struct load/save | ✅ | ✅ |
| Missing file handling | ✅ Flexible (None, default, or error) |
⚠️ Forces file creation with Default |
| Multiple formats compiled in | ✅ (TOML, JSON, YAML together) | ❌ Only 1 format can be compiled in |
Installation
# TOML only (default)
= "0.3"
# JSON only
= { = "0.3", = false, = ["json"] }
# YAML only
= { = "0.3", = false, = ["yaml"] }
# All three
= { = "0.3", = ["json", "yaml"] }
Quick Start
use DotCfg;
use ;
Directory Strategy
// ~/.mytool/config.toml (default — like .cargo, .ssh)
let cfg = new;
// ~/.config/mytool/config.toml (XDG standard, cross-platform)
let cfg = new.xdg;
// explicit dot (same as default)
let cfg = new.dot;
// Custom directory, exactly as given, any path
let cfg = new.at_dir;
// -> /tmp/my-test-dir/config.toml
// Project-local dot dir inside the repo
let cfg = new.at_dir;
// -> /my/project/.mytool/config.toml
// Project discovery walks up from current dir to find .mytool, similar to how git finds .git
let project: = new.yaml.find_in_ancestors?;
// checks .mytool/config.yaml in current dir, then parents, returns Some with Custom dir or None
// nearest wins, no fallback to home, you decide what to do if None is returned
// Same but from explicit dir (useful for tests)
let project = new.yaml.find_in_ancestors_from?;
Format
// TOML (default)
let cfg = new;
// JSON (needs `json` feature)
let cfg = new.json;
// YAML (needs `yaml` feature)
let cfg = new.yaml;
// → ~/.mytool/config.yaml
// Custom filename
let cfg = new.json.filename;
// → ~/.mytool/settings.json (or ~/.config/... with .xdg())
Loading
// Returns None if file doesn't exist — you decide what to do
let config: = cfg.load?;
// Returns an error if file doesn't exist — useful for CLIs that require setup
let config: Config = cfg.load_or_error?;
// Creates the file with Default values if it doesn't exist — opt-in
let config: Config = cfg.load_or_default?;
Per-key Get & Set
Get or set individual keys without loading or overwriting the whole config.
Supports flat and nested (one level: section.field) keys:
// Flat
cfg.get?;
cfg.set?;
// Nested → [user] table in TOML / nested object in JSON / nested mapping in YAML
cfg.get?;
cfg.set?;
set creates the file/dir if missing and preserves all other keys. Values are stored as strings.
Typed Get & Set
You can get a value from the config as a specific type you choose — the methods are get_as and set_val (like get/set but typed).
Use when this config must be a real type. For example, you want port to always be a positive number:
// “port must be a number” — store and get it as u16 directly
cfg.set_val?;
let port: u16 = cfg.get_as?; // no parse::<u16>(), type-safe
// same for other types — bool, Vec, or a whole struct
cfg.set_val?;
let enabled: bool = cfg.get_as?;
cfg.set_val?;
let plugins: = cfg.get_as?;
get_as infers the type you ask for (u16, bool, Vec<String>, or a whole struct like Server { host, port }), so you don’t parse strings yourself. A mismatch (e.g., get_as::<u16> on "alice") returns a clear error.
These work like get/set: set_val creates the file/dir if missing, handles section.field, and preserves other keys.
Environment Variable Overrides
You can let environment variables override the config file — the method is with_env_prefix.
Use when you want MYAPP_PORT=9000 to take precedence over port in the file without changing the file:
let cfg = new.with_env_prefix;
// MYAPP_PORT if set, otherwise `port` from the file
let port: u16 = cfg.get_as?;
// MYAPP_USER_NAME for `user.name`, MYAPP_PLUGINS=fmt,lint for `Vec<String>`
get returns the raw env string, get_as parses it (bool as true/false/1/0, numbers, Vec as comma-separated). set/set_val still write only to the file.
Clap Integration
Combine dotcfg with clap to support standard CLI configuration precedence:
CLI argument flag > Config file on disk > Default fallback.
use Parser;
use DotCfg;
use ;
See examples/clap_cli.rs for a full runnable example.
Other Utilities
cfg.exists?; // does config file exist?
cfg.dir?; // config directory path
cfg.file_path?; // full file path
cfg.delete_file?; // delete file, keep dir
cfg.delete_dir?; // delete entire dir
Documentation
The full guides and examples are available at https://dotcfg.pxxl.click.
For the generated Rust API reference, see https://docs.rs/dotcfg.
Features
| Feature | Default | Description |
|---|---|---|
toml |
✅ | TOML format support |
json |
❌ | JSON format support |
yaml |
❌ | YAML format support |
License
Dual-licensed under MIT OR Apache-2.0. See LICENSE-MIT and LICENSE-APACHE.