initial
This commit is contained in:
commit
a78e6b3cc9
7 changed files with 394 additions and 0 deletions
53
.gitignore
vendored
Normal file
53
.gitignore
vendored
Normal 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
21
LICENSE
Normal 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
86
README.md
Normal 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
52
build.zig
Normal 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
19
build.zig.zon
Normal 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
161
src/dotenv.zig
Normal 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
2
src/main.zig
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const zigdotenv = @import("zigdotenv");
|
||||||
Loading…
Add table
Add a link
Reference in a new issue