econf loads strongly typed configuration values from environment variables or from files referenced by environment variables. This repository contains three sibling implementations with one shared behavior spec:
go/: Go modulerust/: Rust workspace with runtime crate and derive macrozig/: Zig packagespec/: shared behavior and conformance scenarios
For a config type and field, econf builds an environment variable name by converting both names to uppercase snake case and joining them with _.
MyConfig.Host -> MY_CONFIG_HOST
MyConfig.KeyListNUM -> MY_CONFIG_KEY_LIST_NUM
Each field may be loaded from either:
MY_CONFIG_HOST=value
MY_CONFIG_HOST_FILE=/path/to/file-containing-value
When both are present, *_FILE wins. File contents are trimmed before parsing. Missing or empty values leave the field unchanged. Fields whose names start with _ are skipped.
Supported baseline type families are strings, booleans, signed integers, floats, and slices/vectors of those scalar types. Unsigned integers are intentionally outside the shared baseline.
All implementations accept the same boolean tokens: 1/0 exactly, plus case-insensitive t, true, y, yes, f, false, n, no. Anything else is a parse error.
import econf "github.com/liuchong/econf/go"
type MyConfig struct {
Host string
Port int
Enabled bool
Names []string
_secret string
}
func main() {
cfg := &MyConfig{}
econf.SetFields(cfg)
}Go parsing errors panic, matching the original package behavior. See go/README.md.
Published on crates.io as ec (easy ENV config):
cargo add ecuse ec::{load, Econf};
#[derive(Default, Econf)]
struct MyConfig {
host: String,
port: i32,
enabled: bool,
names: Vec<String>,
_secret: String,
}
fn main() -> ec::Result<()> {
let mut cfg = MyConfig::default();
load(&mut cfg)?;
Ok(())
}Rust returns Result for parse and file errors. See rust/README.md.
const std = @import("std");
const econf = @import("econf");
const MyConfig = struct {
host: []const u8 = "",
port: i32 = 0,
enabled: bool = false,
names: []const []const u8 = &.{},
};
pub fn main() !void {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const allocator = arena.allocator();
var cfg = MyConfig{};
try econf.load(&cfg, allocator, .{});
}Zig returns errors and uses the caller's allocator for loaded strings and slices. See zig/README.md.
The shared scenarios live in spec/cases/*.json and are executed directly by a conformance runner in each implementation, so adding or changing a case applies to Go, Rust, and Zig at once. Scenarios cover scalar fields, boolean tokens, file override, slices, skipped _ fields, custom separators, and parse errors.
Run the full suite:
./scripts/conformance.shSee spec/conformance.md for the case schema and runner locations.