├── .gitignore ├── assets └── zdotenv.png ├── test-env.env ├── .env ├── src ├── lib.zig ├── main.zig ├── parser.zig └── dotenv.zig ├── .github └── workflows │ └── build.yml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .zig-cache/ 2 | zig-out/ 3 | .zigmod 4 | deps.zig 5 | -------------------------------------------------------------------------------- /assets/zdotenv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitlyTwiser/zdotenv/HEAD/assets/zdotenv.png -------------------------------------------------------------------------------- /test-env.env: -------------------------------------------------------------------------------- 1 | PASSWORD="asdasd123123AS@#$" 2 | THING="it?" 3 | A_LONG_WORD_SEPERATED_BY_STUFF_HERE_BUT_NOT_COMMENTED="hi i am a sentence and such things" 4 | bool_values_and_stuff="true" 5 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # This is just used for testing of course 2 | PASSWORD_ENV="I AM ALIVE!!" 3 | THING_ENV="" 4 | # A_LONG_WORD_SEPERATED_BY_STUFF_HERE="" 5 | stuff_and_things_env="true" 6 | maybe_env="123" 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/lib.zig: -------------------------------------------------------------------------------- 1 | // Lib.zig is the package interface where all modules are collected for export 2 | 3 | const parser = @import("parser.zig"); 4 | pub const Parser = parser.Parser; 5 | 6 | const zdotenv = @import("dotenv.zig"); 7 | pub const Zdotenv = zdotenv.Zdotenv; 8 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | push: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build-linux: 13 | name: Build & Test 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: Install Zig 19 | run: "sudo snap install zig --classic --beta" 20 | 21 | - name: Build & Test 22 | run: zig build test -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const zdotenv = @import("lib.zig").Zdotenv; 3 | const assert = std.debug.assert; 4 | 5 | /// The binary main is used for testing the package to showcase the API 6 | pub fn main() !void { 7 | var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 8 | defer arena.deinit(); 9 | const allocator = arena.allocator(); 10 | 11 | var dotenv = try zdotenv.init(allocator); 12 | try dotenv.load(); 13 | 14 | const env_map = try std.process.getEnvMap(allocator); 15 | const pass = env_map.get("PASSWORD_ENV") orelse "foobar"; 16 | 17 | std.debug.print("{s}", .{pass}); 18 | 19 | assert(std.mem.eql(u8, pass, "I AM ALIVE!!")); 20 | } 21 | -------------------------------------------------------------------------------- /src/parser.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | /// Parser is a simpel .env parser to extract the Key & Value from the given input (env) file. 4 | pub const Parser = struct { 5 | env_values: std.StringHashMap([]const u8), // Store all Key Values in string hashmap for quick iteration and storage in local child process env 6 | allocator: std.mem.Allocator, 7 | file: std.fs.File, 8 | const Self = @This(); 9 | 10 | pub fn init(allocator: std.mem.Allocator, file: std.fs.File) !Self { 11 | return Self{ .allocator = allocator, .file = file, .env_values = std.StringHashMap([]const u8).init(allocator) }; 12 | } 13 | 14 | pub fn deinit(self: *Self) void { 15 | var values = self.env_values; 16 | values.clearAndFree(); 17 | } 18 | // 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 (#) 19 | pub fn parse(self: *Self) !std.StringHashMap([]const u8) { 20 | var buf: [1024 * 2 * 2]u8 = undefined; 21 | while (try self.file.reader().readUntilDelimiterOrEof(&buf, '\n')) |line| { 22 | // Skip comments (i.e. #) 23 | if (std.mem.startsWith(u8, line, "#")) continue; 24 | if (std.mem.eql(u8, line, "")) continue; 25 | 26 | var split_iter = std.mem.splitScalar(u8, line, '='); 27 | 28 | var key = split_iter.next() orelse ""; 29 | var value = split_iter.next() orelse ""; 30 | 31 | key = std.mem.trim(u8, key, "\""); 32 | value = std.mem.trim(u8, value, "\""); 33 | 34 | // One must dupe to avoid pointer issues in map 35 | const d_key = try self.allocator.dupe(u8, key); 36 | const d_val = try self.allocator.dupe(u8, value); 37 | 38 | try self.env_values.put(d_key, d_val); 39 | } 40 | 41 | return self.env_values; 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 | 6 | Zdotenv is a simple .env parser and a port of godotenv and ruby dotenv, but with a smidge more simplicity. 7 | 8 | ### Usage: 9 | Add zdotenv to your zig project: 10 | ``` 11 | zig fetch --save https://github.com/BitlyTwiser/zdotenv/archive/refs/tags/v0.1.1.tar.gz 12 | ``` 13 | 14 | Add to build file: 15 | ``` 16 | const zdotenv = b.dependency("zdotenv", .{}); 17 | exe.root_module.addImport("zdotenv", zdotenv.module("zdotenv")); 18 | ``` 19 | 20 | Zdotenv has 2 pathways: 21 | 22 | 1. Absolute path to .env 23 | - Expects an absolute path to the .env (unix systems expect a preceding / in the path) 24 | ``` 25 | const z = try Zdotenv.init(std.heap.page_allocator); 26 | // Must be an absolute path! 27 | try z.loadFromFile("/home//Documents/gitclones/zdotenv/test-env.env"); 28 | ``` 29 | 30 | 2. relaltive path: 31 | - Expects the .env to be placed alongside the calling binary 32 | ``` 33 | const z = try Zdotenv.init(std.heap.page_allocator); 34 | try z.load(); 35 | ``` 36 | 37 | Example from Main: 38 | ``` 39 | const std = @import("std"); 40 | const zdotenv = @import("lib.zig"); 41 | const assert = std.debug.assert; 42 | 43 | /// The binary main is used for testing the package to showcase the API 44 | pub fn main() !void { 45 | var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 46 | defer arena.deinit(); 47 | const allocator = arena.allocator(); 48 | 49 | var dotenv = try zdotenv.Zdotenv.init(allocator); 50 | try dotenv.load(); 51 | 52 | const env_map = try std.process.getEnvMap(allocator); 53 | const pass = env_map.get("PASSWORD") orelse "foobar"; 54 | 55 | assert(std.mem.eql(u8, pass, "I AM ALIVE!!")); 56 | } 57 | 58 | ``` 59 | 60 | Importing this into your own library, you will use `@import("zdotenv")`. Otherwise, this would be the same :) 61 | 62 | ## C usage: 63 | Zig (at the time of this writing) does not have a solid way of directly adjusting the env variables. Doing things like: 64 | ``` 65 | var env_map = std.process.getEnvMap(std.heap.page_allocator); 66 | env_map.put("t", "val"); 67 | ``` 68 | 69 | 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. 70 | 71 | Therefore, we do make a C call to store the env variables that are parsed. So linking libC and running tests with ```--library c``` is needed 72 | 73 | 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 :) 74 | -------------------------------------------------------------------------------- /src/dotenv.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const zdotenv = @import("lib.zig"); 3 | const assert = std.debug.assert; 4 | 5 | const file_location_type = union(enum) { 6 | relative, 7 | absolute, 8 | }; 9 | 10 | const FileError = error{ FileNotFound, FileNotAbsolute, GenericError }; 11 | 12 | // Zig fails to have a native way to do this, so we call the setenv C library 13 | extern fn setenv(name: [*:0]const u8, value: [*:0]const u8, overwrite: i32) c_int; 14 | 15 | /// Zdontenv is the primary interface for loading env values 16 | pub const Zdotenv = struct { 17 | allocator: std.mem.Allocator, 18 | file: ?std.fs.File, 19 | 20 | const env_path_relative = ".env"; 21 | const Self = @This(); 22 | 23 | pub fn init(allocator: std.mem.Allocator) !Self { 24 | return Self{ 25 | .allocator = allocator, 26 | .file = null, 27 | }; 28 | } 29 | 30 | pub fn deinit(self: Self) void { 31 | if (self.file) |file| { 32 | file.close(); 33 | } 34 | } 35 | 36 | /// Load from a specific file on disk. Must be absolute path to a location on disk 37 | pub fn loadFromFile(self: *Self, filename: []const u8) !void { 38 | const file = self.readFile(filename, .absolute) catch |e| { 39 | switch (e) { 40 | error.FileNotFound => { 41 | std.debug.print("file {s} does not exist. Please ensure the file exists and try again\n", .{filename}); 42 | }, 43 | error.FileNotAbsolute => { 44 | std.debug.print("given filepath {s} is not absolute. Filepath must start with / and be an absolute path on Postix systems\n", .{filename}); 45 | }, 46 | else => { 47 | std.debug.print("error opening env file. Please check the file exists and try again\n", .{}); 48 | }, 49 | } 50 | 51 | return; 52 | }; 53 | 54 | // Set file 55 | self.file = file; 56 | 57 | // This will load the data into the environment of the calling program 58 | try self.parseAndLoadEnv(); 59 | } 60 | 61 | // 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) 62 | pub fn load(self: *Self) !void { 63 | const file = self.readFile(env_path_relative, .relative) catch |e| { 64 | switch (e) { 65 | error.FileNotFound => { 66 | std.debug.print(".env file does not exist in current directory. Please ensure the file exists and try again\n", .{}); 67 | }, 68 | else => { 69 | std.debug.print("error opening .env file. Please check the file exists and try again\n", .{}); 70 | }, 71 | } 72 | 73 | return; 74 | }; 75 | 76 | //Set file 77 | self.file = file; 78 | 79 | // This will load the data into the environment of the calling program 80 | try self.parseAndLoadEnv(); 81 | } 82 | 83 | fn parseAndLoadEnv(self: *Self) !void { 84 | var parser = try zdotenv.Parser.init(self.allocator, self.file.?); 85 | defer parser.deinit(); 86 | 87 | var env_map = try parser.parse(); 88 | 89 | var iter = env_map.iterator(); 90 | 91 | while (iter.next()) |entry| { 92 | // Dupe strings with terminating zero for C 93 | const key_z = try self.allocator.dupeZ(u8, entry.key_ptr.*); 94 | const value_z = try self.allocator.dupeZ(u8, entry.value_ptr.*); 95 | 96 | if (setenv(key_z, value_z, 1) != 0) { 97 | std.debug.print("Failed to set env var\n", .{}); 98 | return; 99 | } 100 | } 101 | } 102 | 103 | // Simple wrapper for opening a file passing the memory allocation to the caller. Caller MUST dealloc memory! 104 | fn readFile(self: *Self, filename: []const u8, typ: file_location_type) FileError!std.fs.File { 105 | _ = self; 106 | 107 | switch (typ) { 108 | .relative => { 109 | return std.fs.cwd().openFile(filename, .{ .mode = .read_only }) catch |e| { 110 | switch (e) { 111 | error.FileNotFound => { 112 | return FileError.FileNotFound; 113 | }, 114 | else => { 115 | return FileError.GenericError; 116 | }, 117 | } 118 | return; 119 | }; 120 | }, 121 | .absolute => { 122 | if (!std.fs.path.isAbsolute(filename)) return error.FileNotAbsolute; 123 | return std.fs.openFileAbsolute(filename, .{ .mode = .read_only }) catch |e| { 124 | switch (e) { 125 | error.FileNotFound => { 126 | return FileError.FileNotFound; 127 | }, 128 | else => { 129 | return FileError.GenericError; 130 | }, 131 | } 132 | 133 | return; 134 | }; 135 | }, 136 | } 137 | } 138 | }; 139 | 140 | test "loading env from absolute file location" { 141 | var z = try Zdotenv.init(std.heap.page_allocator); 142 | // Must be an absolute path! 143 | try z.loadFromFile("/home/butterz/Documents/gitclones/zdotenv/test-env.env"); 144 | } 145 | 146 | test "loading generic .env" { 147 | var z = try Zdotenv.init(std.heap.page_allocator); 148 | try z.load(); 149 | } 150 | 151 | // --library c 152 | test "parse env 1" { 153 | // use better allocators than this when not testing 154 | const allocator = std.heap.page_allocator; 155 | var z = try Zdotenv.init(allocator); 156 | try z.load(); 157 | 158 | var parser = try zdotenv.Parser.init( 159 | allocator, 160 | z.file.?, 161 | ); 162 | defer parser.deinit(); 163 | 164 | var env_map_global = try std.process.getEnvMap(allocator); 165 | const password = env_map_global.get("PASSWORD_ENV") orelse "bad"; 166 | 167 | assert(std.mem.eql(u8, password, "I AM ALIVE!!")); 168 | } 169 | 170 | // --library c 171 | test "parse env 2" { 172 | // use better allocators than this when not testing 173 | const allocator = std.heap.page_allocator; 174 | 175 | var z = try Zdotenv.init(allocator); 176 | try z.loadFromFile("/home/butterz/Documents/gitclones/zdotenv/test-env.env"); 177 | var parser = try zdotenv.Parser.init(allocator, z.file.?); 178 | defer parser.deinit(); 179 | 180 | var env_map_global = try std.process.getEnvMap(allocator); 181 | const password = env_map_global.get("PASSWORD") orelse "bad"; 182 | assert(std.mem.eql(u8, password, "asdasd123123AS@#$")); 183 | } 184 | --------------------------------------------------------------------------------