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"