OS Environment & Temporary Scopes
env.zig bridges .env files with the real OS process environment on Windows, Linux and macOS. Reads reuse Zig 0.17.0 std.process.Environ (case-insensitive on Windows, case-sensitive on POSIX); only set/unset use minimal custom bindings because std exposes no mutation API.
Quick Start
zig
const envMod = @import("env");
const runtime = envMod.runtime;
// Direct runtime access (cross-platform, process-global, thread-unsafe)
try runtime.set("MY_KEY", "my_value");
const v = runtime.get("MY_KEY"); // ?[]const u8, borrowed
try runtime.unset("MY_KEY");
// Env store with OS fallback
var env = envMod.Env.init(allocator, .{});
defer env.deinit();
try env.load(".env");
const host = env.getRuntime("HOST") orelse "localhost"; // Env first, then runtime
try env.loadRuntime(); // import all OS vars into Env
try env.loadRuntimeIfMissing(); // only missing keys
try env.loadRuntimeWithPrefix("APP_"); // APP_PORT -> PORT
try env.exportToRuntime(); // push Env -> OSImport / Export
zig
// Import everything
try env.loadRuntime();
// Import only keys with prefix, stripped
// OS: APP_PORT=8080 -> Env: PORT=8080
try env.loadRuntimeWithPrefix("APP_");
// Export
try env.exportToRuntime();
// Also via config:
var env2 = envMod.Env.init(allocator, .{ .exportToRuntime = true });
try env2.set("FOO", "bar"); // automatically sets OS env toogetRuntime / containsRuntime / requireRuntime
zig
// Checks Env first, then runtime (like shell $VAR fallback)
const url = env.getRuntime("DATABASE_URL");
const ok = env.containsRuntime("HOME");
const val = try env.requireRuntime("API_KEY"); // error.MissingRequired if absent
// With default
const port = env.getWithFallback("PORT", "3000");
// Direct runtime (bypass Env); get is borrowed, getAlloc is owned
const home = runtime.get("HOME");
const home2 = try runtime.getAlloc(allocator, "HOME");
const all = try runtime.getAll(allocator);
// free keys/values when doneProcess Env Map (for child processes)
zig
// Build an Environ.Map suitable for std.process.spawn / Child
var map = try env.toEnvironMap(allocator);
defer map.deinit();
try map.put("EXTRA", "value");
// Apply Env on top of an existing map
try env.applyToEnvironMap(&map);
// Then spawn with custom env:
// try std.process.spawn(io, .{ .argv = &.{ "myapp" }, .environ_map = &map });Temporary / Scoped Env ($env style)
OS environment is global. Scope saves original values and restores on deinit — perfect for tests and $env:FOO=bar shell-style isolation.
OS-level Scope
zig
{
var scope = try runtime.scope(allocator);
defer scope.deinit();
try scope.set("TMP_KEY", "temporary");
try scope.unset("REMOVE_ME");
// ... do work, spawn children, etc.
// original values restored on deinit
}
// TMP_KEY and REMOVE_ME are back to what they wereWith helper
zig
try runtime.Scope.with(allocator, &.{ .{ .key = "FOO", .value = "bar" } }, struct {
fn run() !void { std.debug.print("FOO={s}\n", .{runtime.get("FOO").?}); }
}.run);Snapshot / Restore
zig
var snap = try runtime.snapshot(allocator);
defer snap.deinit();
try runtime.set("A", "new");
try snap.restore(); // back to snapshotEnv-level Scope (in-memory)
zig
{
var s = env.scope(); // Env.EnvScope
defer s.deinit();
try s.set("PORT", "9090");
try s.unset("DEBUG");
// env.get("PORT") now 9090
}
// restoredexport Prefix
.env files may use shell-style export — it is ignored but accepted:
env
export DATABASE_URL=postgres://localhost/mydb
export PORT=8080Windows Notes
- Case-insensitive:
PATHandPathare the same. - Missing (
null) is distinct from empty (""); both are preserved. runtime.getis borrowed from a thread-local buffer resized per call (valid until the nextgeton the same thread); dupe to retain.Environ.createMap(PEB-locked) backs enumeration.
See Also
- Interpolation —
${VAR:-default},${VAR:+alt}, OS fallback - Configuration
- API Reference
