BeigeHornet151/zig-dotenv
A lightweight .env file parser for Zig - Load environment variables from .env files with zero dependencies
A simple and powerful .env
file parser for Zig applications. Load configuration from files, modify it at runtime, and get type-safe values without any external dependencies.
This version adds a ton of stuff people actually asked for while keeping everything backward compatible. Your v1.0 code will work fine, but now you can do way more cool things.
.env
then .env.local
with proper precedence${VARIABLE}
syntax like other dotenv librariesAdd to your build.zig.zon
:
.{
.name = "your-project",
.version = "1.0.0",
.dependencies = .{
.dotenv = .{
.url = "https://github.com/BeigeHornet151/zig-dotenv/archive/refs/tags/v2.0.0.tar.gz",
.hash = "...", // Use zig fetch to get the real hash
},
},
}
const std = @import("std");
const dotenv = @import("dotenv");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// Load .env file
var env = try dotenv.DotEnv.loadFile(allocator, ".env");
defer env.deinit();
// Get string values (works exactly like v1.0)
const db_url = env.get("DATABASE_URL") orelse "sqlite://memory";
const api_key = env.get("API_KEY") orelse "no-key";
std.debug.print("Database: {s}\n", .{db_url});
std.debug.print("API Key: {s}\n", .{api_key});
}
Stop manually parsing strings. The library does it for you:
// Boolean values - understands lots of formats
const debug = env.getBool("DEBUG") orelse false; // "true", "yes", "on", "1"
const cache = env.getBool("ENABLE_CACHE") orelse true; // "false", "no", "off", "0"
// Numbers with type safety
const port = env.getInt("PORT", u16) orelse 8080; // Parses to u16
const workers = env.getInt("WORKERS", u8) orelse 4; // Parses to u8
const timeout = env.getFloat("TIMEOUT", f32) orelse 30.0; // Parses to f32
// Arrays from comma-separated values
if (try env.getArray("ALLOWED_HOSTS", ",", allocator)) |hosts| {
defer env.freeArray(allocator, hosts);
for (hosts) |host| {
std.debug.print("Allowed host: {s}\n", .{host});
}
}
Change config while your app is running:
var env = dotenv.DotEnv.init(allocator);
defer env.deinit();
// Add new values
try env.put("API_URL", "https://api.example.com");
try env.put("RETRY_COUNT", "3");
// Update existing values
try env.put("DEBUG", "true");
// Check if keys exist
if (env.has("DATABASE_URL")) {
std.debug.print("Database configured!\n", .{});
}
// Remove values you don't need
const removed = env.remove("TEMP_TOKEN");
if (removed) {
std.debug.print("Removed temporary token\n", .{});
}
Load multiple env files with smart precedence:
// Load .env first, then .env.local (local wins on conflicts)
const files = [_][]const u8{ ".env", ".env.local" };
var env = try dotenv.DotEnv.loadFiles(allocator, &files);
defer env.deinit();
// Or use the convenience function
var env = try dotenv.loadDefault(allocator); // Loads .env + .env.local
defer env.deinit();
Reference other variables in your .env file:
# .env file
BASE_URL=https://api.example.com
API_VERSION=v1
USERS_API=${BASE_URL}/${API_VERSION}/users
ORDERS_API=${BASE_URL}/${API_VERSION}/orders
var env = try dotenv.DotEnv.loadFileWithExpansion(allocator, ".env");
defer env.deinit();
const users_api = env.get("USERS_API").?; // "https://api.example.com/v1/users"
Helpful methods for managing config:
// Get all variable names
const all_keys = try env.keys(allocator);
defer env.freeKeys(allocator, all_keys);
// Stats about your config
std.debug.print("Total variables: {d}\n", .{env.count()});
std.debug.print("Is empty: {}\n", .{env.isEmpty()});
// Get with fallback values
const log_level = env.getWithDefault("LOG_LEVEL", "info");
const max_retries = env.getWithDefault("MAX_RETRIES", "3");
Copy this to get started:
# App Configuration
APP_NAME="My Awesome App"
VERSION=1.0.0
DEBUG=true
ENVIRONMENT=development
# Server Settings
PORT=8080
HOST=localhost
WORKERS=4
TIMEOUT=30.0
# Database
DATABASE_URL=postgres://localhost:5432/myapp
DATABASE_POOL_SIZE=10
DATABASE_SSL=true
# API Configuration
API_KEY=your_secret_key_here
API_BASE_URL=https://api.example.com
API_VERSION=v1
API_RATE_LIMIT=100
# Feature Flags
ENABLE_CACHE=yes
ENABLE_LOGGING=on
MAINTENANCE_MODE=off
# Arrays (comma-separated)
ALLOWED_ORIGINS=localhost,127.0.0.1,*.example.com
[email protected],[email protected]
# Variable Expansion
API_ENDPOINT=${API_BASE_URL}/${API_VERSION}
HEALTH_CHECK=${API_BASE_URL}/health
The parser is pretty flexible:
Boolean values : true
, false
, yes
, no
, on
, off
, 1
, 0
(case insensitive)
Quoted values : Both "double quotes"
and 'single quotes'
work and get stripped
Comments : Lines starting with #
are ignored
Variable expansion : Use ${VARIABLE_NAME}
to reference other variables
Whitespace : Leading and trailing spaces are automatically trimmed
V2.0 gives you way better error messages:
var env = dotenv.DotEnv.loadFile(allocator, ".env") catch |err| switch (err) {
dotenv.DotEnvError.FileNotFound => {
std.log.warn("No .env file found - using defaults\n", .{});
return dotenv.DotEnv.init(allocator);
},
dotenv.DotEnvError.ParseError => {
std.log.err("Your .env file has syntax errors\n", .{});
return err;
},
else => return err,
};
Check out the examples folder:
# Copy the example env file
cp examples/.env.example .env
# Run the basic example
zig build example-basic
# Run the dynamic configuration example
zig build example-dynamic
Run the test suite:
zig build test # Run all tests
zig build test-basic # Just basic functionality
zig build test-types # Just type conversions
zig build test-api # Just the new v2.0 methods
loadFile(allocator, path)
- Load a single .env file
loadFiles(allocator, paths)
- Load multiple files with precedence
loadFileWithExpansion(allocator, path)
- Load with variable expansion
loadDefault(allocator)
- Convenience function for .env + .env.local
get(key)
- Get string value or null
getBool(key)
- Get boolean value or null
getInt(key, type)
- Get integer value or null
getFloat(key, type)
- Get float value or null
getArray(key, delimiter, allocator)
- Get array value or null
getWithDefault(key, default)
- Get value with fallback
put(key, value)
- Add or update a key-value pair
remove(key)
- Remove a key, returns true if it existed
has(key)
- Check if a key exists
count()
- Number of variables loaded
isEmpty()
- True if no variables are loaded
keys(allocator)
- Get all variable names (remember to free)
deinit()
- Free all memory (always call this)
Your existing code works fine. But you can upgrade it easily:
// Old v1.0 way
const debug_str = env.get("DEBUG") orelse "false";
const debug = std.mem.eql(u8, debug_str, "true");
// New v2.0 way
const debug = env.getBool("DEBUG") orelse false;
// Old v1.0 way
const port_str = env.get("PORT") orelse "8080";
const port = std.fmt.parseInt(u16, port_str, 10) catch 8080;
// New v2.0 way
const port = env.getInt("PORT", u16) orelse 8080;
Want to help? Here's how:
zig build check
to make sure everything worksMIT License - do whatever you want with it.
See CHANGELOG.md for what changed between versions.