diff --git a/.env b/.env
new file mode 100644
index 0000000..f63c83e
--- /dev/null
+++ b/.env
@@ -0,0 +1,8 @@
+# This is just used for testing of course
+PASSWORD_ENV="I AM ALIVE!!"
+THING_ENV=""
+# A_LONG_WORD_SEPERATED_BY_STUFF_HERE=""
+stuff_and_things_env="true"
+maybe_env="123"
+
+
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..2f9d6e8
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+.zig-cache/
+zig-out/
+.zigmod
+deps.zig
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..07cb272
--- /dev/null
+++ b/README.md
@@ -0,0 +1,41 @@
+
+
+# Apprunner
+
+
+
+Zdotenv is a simple .env parser and a port of godotenv and ruby dotenv, but with a smidge more simplicity.
+
+### Usage:
+Add zdotenv to your zig project:
+```
+zig fetch --save https://github.com/BitlyTwiser/zdotenv/archive/refs/tags/0.1.0.tar.gz
+```
+
+Zdotenv has 2 pathways:
+
+1. Absolute path to .env
+- Expects an absolute path to the .env (unix systems expect a preceding / in the path)
+```
+const z = try Zdotenv.init(std.heap.page_allocator);
+// Must be an absolute path!
+try z.loadFromFile("/home//Documents/gitclones/zdotenv/test-env.env");
+```
+
+2. relaltive path:
+- Expects the .env to be placed alongside the calling binary
+```
+const z = try Zdotenv.init(std.heap.page_allocator);
+try z.load();
+```
+
+## C usage:
+Zig (at the time of this writing) does not have a solid way of directly adjusting the env variables. Doing things like:
+```
+ var env_map = std.process.getEnvMap(std.heap.page_allocator);
+ env_map.put("t", "val");
+```
+
+will only adjust the env map for the scope of this execution (i.e. scope of the current calling function). After function exit, the map goes back to its previous state.
+
+Using the package is as simple as the above code examples. import below using zig zon, load the .env, and access the variables as needed using std.process.EnvMap :)
\ No newline at end of file
diff --git a/assets/zdotenv.png b/assets/zdotenv.png
new file mode 100644
index 0000000..0cf6ec3
Binary files /dev/null and b/assets/zdotenv.png differ
diff --git a/build.zig b/build.zig
new file mode 100644
index 0000000..7ebfa89
--- /dev/null
+++ b/build.zig
@@ -0,0 +1,32 @@
+const std = @import("std");
+
+pub fn build(b: *std.Build) void {
+ // We build a binary for testing and the actual module for use
+ const target = b.standardTargetOptions(.{});
+
+ const optimize = b.standardOptimizeOption(.{});
+
+ const exe = b.addExecutable(.{
+ .name = "zdotenv",
+ .root_source_file = b.path("src/main.zig"),
+ .target = target,
+ .optimize = optimize,
+ });
+ exe.linkLibC();
+ b.installArtifact(exe);
+
+ // Module setup
+ _ = b.addModule("zdotenv", .{ .root_source_file = b.path("src/main.zig") });
+ var lib_tests = b.addTest(.{
+ .root_source_file = b.path("src/main.zig"),
+ .optimize = optimize,
+ });
+
+ const test_step = b.step("test", "Run library tests");
+ test_step.dependOn(&lib_tests.step);
+
+ // Export the library module
+ _ = b.addModule("zdotenv", .{
+ .root_source_file = b.path("src/lib.zig"),
+ });
+}
diff --git a/build.zig.zon b/build.zig.zon
new file mode 100644
index 0000000..3674729
--- /dev/null
+++ b/build.zig.zon
@@ -0,0 +1,72 @@
+.{
+ // This is the default name used by packages depending on this one. For
+ // example, when a user runs `zig fetch --save `, this field is used
+ // as the key in the `dependencies` table. Although the user can choose a
+ // different name, most users will stick with this provided value.
+ //
+ // It is redundant to include "zig" in this name because it is already
+ // within the Zig package namespace.
+ .name = "zdotenv",
+
+ // This is a [Semantic Version](https://semver.org/).
+ // In a future version of Zig it will be used for package deduplication.
+ .version = "0.0.0",
+
+ // This field is optional.
+ // This is currently advisory only; Zig does not yet do anything
+ // with this value.
+ //.minimum_zig_version = "0.11.0",
+
+ // This field is optional.
+ // Each dependency must either provide a `url` and `hash`, or a `path`.
+ // `zig build --fetch` can be used to fetch all dependencies of a package, recursively.
+ // Once all dependencies are fetched, `zig build` no longer requires
+ // internet connectivity.
+ .dependencies = .{
+ // See `zig fetch --save ` for a command-line interface for adding dependencies.
+ //.example = .{
+ // // When updating this field to a new URL, be sure to delete the corresponding
+ // // `hash`, otherwise you are communicating that you expect to find the old hash at
+ // // the new URL.
+ // .url = "https://example.com/foo.tar.gz",
+ //
+ // // This is computed from the file contents of the directory of files that is
+ // // obtained after fetching `url` and applying the inclusion rules given by
+ // // `paths`.
+ // //
+ // // This field is the source of truth; packages do not come from a `url`; they
+ // // come from a `hash`. `url` is just one of many possible mirrors for how to
+ // // obtain a package matching this `hash`.
+ // //
+ // // Uses the [multihash](https://multiformats.io/multihash/) format.
+ // .hash = "...",
+ //
+ // // When this is provided, the package is found in a directory relative to the
+ // // build root. In this case the package's hash is irrelevant and therefore not
+ // // computed. This field and `url` are mutually exclusive.
+ // .path = "foo",
+
+ // // When this is set to `true`, a package is declared to be lazily
+ // // fetched. This makes the dependency only get fetched if it is
+ // // actually used.
+ // .lazy = false,
+ //},
+ },
+
+ // Specifies the set of files and directories that are included in this package.
+ // Only files and directories listed here are included in the `hash` that
+ // is computed for this package. Only files listed here will remain on disk
+ // when using the zig package manager. As a rule of thumb, one should list
+ // files required for compilation plus any license(s).
+ // Paths are relative to the build root. Use the empty string (`""`) to refer to
+ // the build root itself.
+ // A directory listed here means that all files within, recursively, are included.
+ .paths = .{
+ "build.zig",
+ "build.zig.zon",
+ "src",
+ // For example...
+ //"LICENSE",
+ //"README.md",
+ },
+}
diff --git a/src/dotenv.zig b/src/dotenv.zig
new file mode 100644
index 0000000..0484385
--- /dev/null
+++ b/src/dotenv.zig
@@ -0,0 +1,184 @@
+const std = @import("std");
+const zdotenv = @import("lib.zig");
+const assert = std.debug.assert;
+
+const file_location_type = union(enum) {
+ relative,
+ absolute,
+};
+
+const FileError = error{ FileNotFound, FileNotAbsolute, GenericError };
+
+// Zig fails to have a native way to do this, so we call the setenv C library
+extern fn setenv(name: [*:0]const u8, value: [*:0]const u8, overwrite: i32) c_int;
+
+/// Zdontenv is the primary interface for loading env values
+pub const Zdotenv = struct {
+ allocator: std.mem.Allocator,
+ file: ?std.fs.File,
+
+ const env_path_relative = ".env";
+ const Self = @This();
+
+ pub fn init(allocator: std.mem.Allocator) !Self {
+ return Self{
+ .allocator = allocator,
+ .file = null,
+ };
+ }
+
+ pub fn deinit(self: Self) void {
+ if (self.file) |file| {
+ file.close();
+ }
+ }
+
+ /// Load from a specific file on disk. Must be absolute path to a location on disk
+ pub fn loadFromFile(self: *Self, filename: []const u8) !void {
+ const file = self.readFile(filename, .absolute) catch |e| {
+ switch (e) {
+ error.FileNotFound => {
+ std.debug.print("file {s} does not exist. Please ensure the file exists and try again\n", .{filename});
+ },
+ error.FileNotAbsolute => {
+ std.debug.print("given filepath {s} is not absolute. Filepath must start with / and be an absolute path on Postix systems\n", .{filename});
+ },
+ else => {
+ std.debug.print("error opening env file. Please check the file exists and try again\n", .{});
+ },
+ }
+
+ return;
+ };
+ // defer file.close();
+
+ // Set file
+ self.file = file;
+
+ // This will load the data into the environment of the calling program
+ try self.parseAndLoadEnv();
+ }
+
+ // Load will just load the default .env at location of the calling binary (i.e. expects a .env to be located next to main func call)
+ pub fn load(self: *Self) !void {
+ const file = self.readFile(env_path_relative, .relative) catch |e| {
+ switch (e) {
+ error.FileNotFound => {
+ std.debug.print(".env file does not exist in current directory. Please ensure the file exists and try again\n", .{});
+ },
+ else => {
+ std.debug.print("error opening .env file. Please check the file exists and try again\n", .{});
+ },
+ }
+
+ return;
+ };
+ // defer file.close();
+
+ //Set file
+ self.file = file;
+
+ // This will load the data into the environment of the calling program
+ try self.parseAndLoadEnv();
+ }
+
+ fn parseAndLoadEnv(self: *Self) !void {
+ var parser = try zdotenv.Parser.init(self.allocator, self.file.?);
+ defer parser.deinit();
+
+ var env_map = try parser.parse();
+
+ var iter = env_map.iterator();
+
+ while (iter.next()) |entry| {
+ // Dupe strings with terminating zero for C
+ const key_z = try self.allocator.dupeZ(u8, entry.key_ptr.*);
+ const value_z = try self.allocator.dupeZ(u8, entry.value_ptr.*);
+ if (setenv(key_z, value_z, 1) != 0) {
+ std.debug.print("Failed to set env var\n", .{});
+ return;
+ }
+ }
+ }
+
+ // Simple wrapper for opening a file passing the memory allocation to the caller. Caller MUST dealloc memory!
+ fn readFile(self: *Self, filename: []const u8, typ: file_location_type) FileError!std.fs.File {
+ _ = self;
+
+ switch (typ) {
+ .relative => {
+ return std.fs.cwd().openFile(filename, .{ .mode = .read_only }) catch |e| {
+ switch (e) {
+ error.FileNotFound => {
+ return FileError.FileNotFound;
+ },
+ else => {
+ return FileError.GenericError;
+ },
+ }
+ return;
+ };
+ },
+ .absolute => {
+ if (!std.fs.path.isAbsolute(filename)) return error.FileNotAbsolute;
+ return std.fs.openFileAbsolute(filename, .{ .mode = .read_only }) catch |e| {
+ switch (e) {
+ error.FileNotFound => {
+ return FileError.FileNotFound;
+ },
+ else => {
+ return FileError.GenericError;
+ },
+ }
+
+ return;
+ };
+ },
+ }
+ }
+};
+
+test "loading env from absolute file location" {
+ var z = try Zdotenv.init(std.heap.page_allocator);
+ // Must be an absolute path!
+ try z.loadFromFile("/home/butterz/Documents/gitclones/zdotenv/test-env.env");
+}
+
+test "loading generic .env" {
+ var z = try Zdotenv.init(std.heap.page_allocator);
+ try z.load();
+}
+
+// --library c
+test "parse env 1" {
+ // use better allocators than this when not testing
+ const allocator = std.heap.page_allocator;
+ var z = try Zdotenv.init(allocator);
+ try z.load();
+
+ var parser = try zdotenv.Parser.init(
+ allocator,
+ z.file.?,
+ );
+ defer parser.deinit();
+
+ var env_map_global = try std.process.getEnvMap(allocator);
+ const password = env_map_global.get("PASSWORD_ENV") orelse "bad";
+
+ assert(std.mem.eql(u8, password, "I AM ALIVE!!"));
+}
+
+// --library c
+test "parse env 2" {
+ // use better allocators than this when not testing
+ const allocator = std.heap.page_allocator;
+
+ var z = try Zdotenv.init(allocator);
+ try z.loadFromFile("/home/butterz/Documents/gitclones/zdotenv/test-env.env");
+ var parser = try zdotenv.Parser.init(allocator, z.file.?);
+ defer parser.deinit();
+
+ var env_map_global = try std.process.getEnvMap(allocator);
+ const password = env_map_global.get("PASSWORD") orelse "bad";
+ assert(std.mem.eql(u8, password, "asdasd123123AS@#$"));
+}
diff --git a/src/lib.zig b/src/lib.zig
new file mode 100644
index 0000000..aabe29f
--- /dev/null
+++ b/src/lib.zig
@@ -0,0 +1,7 @@
+// Lib.zig is the package interface where all modules are collected for export
+
+pub const parser = @import("parser.zig");
+pub const Parser = parser.Parser;
+
+pub const zdotenv = @import("dotenv.zig");
+pub const Zdotenv = zdotenv.Zdotenv;
diff --git a/src/main.zig b/src/main.zig
new file mode 100644
index 0000000..4f04209
--- /dev/null
+++ b/src/main.zig
@@ -0,0 +1,4 @@
+const std = @import("std");
+
+/// The binary main is used for testing the package to showcase the API
+pub fn main() !void {}
diff --git a/src/parser.zig b/src/parser.zig
new file mode 100644
index 0000000..00a216e
--- /dev/null
+++ b/src/parser.zig
@@ -0,0 +1,43 @@
+const std = @import("std");
+
+/// Parser is a simpel .env parser to extract the Key & Value from the given input (env) file.
+pub const Parser = struct {
+ env_values: std.StringHashMap([]const u8), // Store all Key Values in string hashmap for quick iteration and storage in local child process env
+ allocator: std.mem.Allocator,
+ file: std.fs.File,
+ const Self = @This();
+
+ pub fn init(allocator: std.mem.Allocator, file: std.fs.File) !Self {
+ return Self{ .allocator = allocator, .file = file, .env_values = std.StringHashMap([]const u8).init(allocator) };
+ }
+
+ pub fn deinit(self: *Self) void {
+ var values = self.env_values;
+ values.clearAndFree();
+ }
+ // parse is a simple parsing function. Its simple on purpose, this process should not be lofty or complex. No AST's or complex symbol resolution. Just take the Key and Value from the K=V from an .env and avoid comments (#)
+ pub fn parse(self: *Self) !std.StringHashMap([]const u8) {
+ var buf: [1024 * 2 * 2]u8 = undefined;
+ while (try self.file.reader().readUntilDelimiterOrEof(&buf, '\n')) |line| {
+ // Skip comments (i.e. #)
+ if (std.mem.startsWith(u8, line, "#")) continue;
+ if (std.mem.eql(u8, line, "")) continue;
+
+ var split_iter = std.mem.split(u8, line, "=");
+
+ var key = split_iter.next() orelse "";
+ var value = split_iter.next() orelse "";
+
+ key = std.mem.trim(u8, key, "\"");
+ value = std.mem.trim(u8, value, "\"");
+
+ // One must dupe to avoid pointer issues in map
+ const d_key = try self.allocator.dupe(u8, key);
+ const d_val = try self.allocator.dupe(u8, value);
+
+ try self.env_values.put(d_key, d_val);
+ }
+
+ return self.env_values;
+ }
+};
diff --git a/test-env.env b/test-env.env
new file mode 100644
index 0000000..7d498d3
--- /dev/null
+++ b/test-env.env
@@ -0,0 +1,4 @@
+PASSWORD="asdasd123123AS@#$"
+THING="it?"
+A_LONG_WORD_SEPERATED_BY_STUFF_HERE_BUT_NOT_COMMENTED="hi i am a sentence and such things"
+bool_values_and_stuff="true"