This commit is contained in:
Lucas F. 2025-12-25 15:00:11 -03:00
commit a78e6b3cc9
7 changed files with 394 additions and 0 deletions

53
.gitignore vendored Normal file
View file

@ -0,0 +1,53 @@
# Zig build artifacts
zig-out/
zig-cache/
.zig-cache/
# Generated PDFs (test outputs)
*.pdf
# Test data files
*.json
!build.json
*.pdm
*.bin
*.png
*.sh
*.ndjson
# HTML test files
*.html
# Build binaries
*.o
*.so
*.dylib
*.dll
*.exe
# IDE and editor files
.vscode/
.idea/
*.swp
*.swo
*~
.DS_Store
.claude/
# Temporary files
*.tmp
*.temp
*.log
# Test directories
data/
output/
services/
template_configs/
# Backup files
*.bak
*.backup
# OS specific
Thumbs.db

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Lucas F.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

86
README.md Normal file
View file

@ -0,0 +1,86 @@
# zig-dotenv
A simple and lightweight library for loading `.env` files in Zig projects.
- **Zero dependencies** — only the Zig standard library
- **Safe** — proper error handling and automatic memory cleanup
- **Convenient**`get`, `getOr`, and `require` methods for easy access
- **Well-tested** — full unit test coverage
## Installation
Add the package to your project with `zig fetch`:
```bash
zig fetch --save https://git.lucasf.xyz/public/zig-dotenv/archive/0.1.0.tar.gz
```
Then, in your `build.zig`:
```zig
const std = @import("std");
pub fn build(b: *std.Build) void {
...
const exe = b.addExecutable(.{ ... });
...
const dotenv_dep = b.dependency("zigdotenv", .{});
exe.root_module.addImport("zigdotenv", dotenv_dep.module("zigdotenv"));
}
```
## Basic Usage
```zig
const std = @import("std");
const dotenv = @import("zigdotenv");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// Load the .env file (fails on unexpected errors)
var env = try dotenv.DotEnv.load(allocator, ".env");
defer env.deinit();
// Simple access
if (env.get("API_KEY")) |key| {
std.debug.print("API_KEY = {s}\n", .{key});
}
// With fallback default
const debug = env.getOr("DEBUG", "false");
std.debug.print("DEBUG = {s}\n", .{debug});
// Required variable (fails if missing)
const database_url = try env.require("DATABASE_URL");
std.debug.print("DB URL = {s}\n", .{database_url});
}
```
## Supported format
The parser supports the standard `.env` format:
```env
# Comments are ignored
API_KEY=supersecret123
DATABASE_URL=postgres://localhost:5432/myapp
# Spaces are trimmed
DEBUG = true
# Single or double quotes are stripped
QUOTED="value with spaces"
SINGLE='another value'
# Empty value
EMPTY=
```
## License
MIT © Lucas F.
___
Feito com ❤️ em Zig. Enjoy! 🚀

52
build.zig Normal file
View file

@ -0,0 +1,52 @@
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const mod = b.addModule("zigdotenv", .{
.root_source_file = b.path("src/dotenv.zig"),
.target = target,
});
const exe = b.addExecutable(.{
.name = "zigdotenv",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "zigdotenv", .module = mod },
},
}),
});
b.installArtifact(exe);
const run_step = b.step("run", "Run the app");
const run_cmd = b.addRunArtifact(exe);
run_step.dependOn(&run_cmd.step);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
}
const mod_tests = b.addTest(.{
.root_module = mod,
});
const run_mod_tests = b.addRunArtifact(mod_tests);
const exe_tests = b.addTest(.{
.root_module = exe.root_module,
});
const run_exe_tests = b.addRunArtifact(exe_tests);
const test_step = b.step("test", "Run tests");
test_step.dependOn(&run_mod_tests.step);
test_step.dependOn(&run_exe_tests.step);
}

19
build.zig.zon Normal file
View file

@ -0,0 +1,19 @@
.{
.name = .zigdotenv,
.version = "0.0.0",
.fingerprint = 0x626f95eeb8bf233a, // Changing this has security and trust implications.
.minimum_zig_version = "0.15.2",
.dependencies = .{
.zigdotenv = .{
.url = "git+https://git.lucasf.xyz/public/zig-dotenv.git?ref=0.1.0#dca4323e56b705264233336af6b886d20b259532",
.hash = "zigdotenv-0.0.0-OiO_uOEpAACmZDzDibmfis8H5n39abPf4vQy5pNGu6OX",
},
},
.paths = .{
"build.zig",
"build.zig.zon",
"src",
"LICENSE",
"README.md",
},
}

161
src/dotenv.zig Normal file
View file

@ -0,0 +1,161 @@
const std = @import("std");
/// Structure that holds environment variables loaded from .env file
pub const DotEnv = struct {
map: std.StringHashMap([]const u8),
allocator: std.mem.Allocator,
/// Loads the .env file from the given path and returns a DotEnv instance
pub fn load(allocator: std.mem.Allocator, path: []const u8) !DotEnv {
var map = std.StringHashMap([]const u8).init(allocator);
const file = std.fs.cwd().openFile(path, .{}) catch |err| switch (err) {
error.FileNotFound => return error.DotEnvFileNotFound,
else => return err,
};
defer file.close();
const max_size = 10 * 1024 * 1024; // 10MB max
const content = try file.readToEndAlloc(allocator, max_size);
defer allocator.free(content);
var lines = std.mem.splitScalar(u8, content, '\n');
while (lines.next()) |raw_line| {
var line = std.mem.trim(u8, raw_line, " \t\r\n");
// skip empty lines and comments
if (line.len == 0 or line[0] == '#') continue;
// find the first '=' character
if (std.mem.indexOfScalar(u8, line, '=')) |eq_pos| {
const key = std.mem.trim(u8, line[0..eq_pos], " \t");
var value = std.mem.trim(u8, line[eq_pos + 1 ..], " \t\r\n");
// optionally remove surrounding single or double quotes
if (value.len >= 2 and ((value[0] == '"' and value[value.len - 1] == '"') or
(value[0] == '\'' and value[value.len - 1] == '\'')))
{
value = value[1 .. value.len - 1];
}
// Duplicate key and value to own them in the map
const key_dup = try allocator.dupe(u8, key);
errdefer allocator.free(key_dup);
const value_dup = try allocator.dupe(u8, value);
errdefer allocator.free(value_dup);
try map.put(key_dup, value_dup);
}
}
return DotEnv{
.map = map,
.allocator = allocator,
};
}
/// Frees all allocated memory (keys and values)
pub fn deinit(self: *DotEnv) void {
var it = self.map.iterator();
while (it.next()) |entry| {
self.allocator.free(entry.key_ptr.*);
self.allocator.free(entry.value_ptr.*);
}
self.map.deinit();
}
/// Returns the value for key, or null if not present
pub fn get(self: DotEnv, key: []const u8) ?[]const u8 {
return self.map.get(key);
}
/// Returns the value for key, or a default value if not present
pub fn getOr(self: DotEnv, key: []const u8, default: []const u8) []const u8 {
return self.get(key) orelse default;
}
/// Returns the value for a key, or errors if the required variable is missing
pub fn require(self: DotEnv, key: []const u8) ![]const u8 {
return self.get(key) orelse error.MissingRequiredEnvVar;
}
};
pub const Error = error{ DotEnvFileNotFound, MissingRequiredEnvVar };
//
// UNIT TESTS
//
test "load .env file - basic functionality" {
const allocator = std.testing.allocator;
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const content =
\\API_KEY=12345
\\DEBUG=true
\\NAME=zig tester
\\#COMMENT=ignored
\\ SPACED = value with spaces
\\QUOTED="quoted value"
\\EMPTY=
;
try tmp.dir.writeFile(.{ .data = content, .sub_path = "test.env" });
const tmp_path = try tmp.dir.realpathAlloc(allocator, ".");
defer allocator.free(tmp_path);
const full_path = try std.fs.path.join(allocator, &[_][]const u8{
tmp_path,
"test.env",
});
defer allocator.free(full_path);
var env = try DotEnv.load(allocator, full_path);
defer env.deinit();
try std.testing.expectEqualStrings("12345", env.require("API_KEY") catch unreachable);
try std.testing.expectEqualStrings("true", env.get("DEBUG").?);
try std.testing.expectEqualStrings("zig tester", env.get("NAME").?);
try std.testing.expectEqualStrings("value with spaces", env.get("SPACED").?);
try std.testing.expectEqualStrings("quoted value", env.get("QUOTED").?);
try std.testing.expect(env.get("COMMENT") == null);
try std.testing.expectEqualStrings("", env.get("EMPTY").?);
}
test "require() errors on missing required var" {
const allocator = std.testing.allocator;
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
try tmp.dir.writeFile(.{ .data = "OTHER=123", .sub_path = "empty.env" });
const tmp_path = try tmp.dir.realpathAlloc(allocator, ".");
defer allocator.free(tmp_path);
const full_path = try std.fs.path.join(allocator, &[_][]const u8{ tmp_path, "empty.env" });
defer allocator.free(full_path);
var env = try DotEnv.load(allocator, full_path);
defer env.deinit();
try std.testing.expectError(error.MissingRequiredEnvVar, env.require("MISSING"));
}
test "missing file should return error" {
const allocator = std.testing.allocator;
try std.testing.expectError(error.DotEnvFileNotFound, DotEnv.load(allocator, "this-file-not-exists.env"));
}
test "getOr() returns default on missing key" {
const allocator = std.testing.allocator;
var env: DotEnv = DotEnv{ .allocator = allocator, .map = std.StringHashMap([]const u8).init(allocator) };
try std.testing.expectEqualStrings("default-value", env.getOr("NOT EXIST", "default-value"));
try std.testing.expect(env.get("NOT EXIST") == null);
}

2
src/main.zig Normal file
View file

@ -0,0 +1,2 @@
const std = @import("std");
const zigdotenv = @import("zigdotenv");