zig-config: A Lightweight .env/.ini Library for Zig

Hello Everyone!

I’ve been working on a lightweight config library for Zig called zig-config. It is to made to handle configuration files in .env and .ini formats. It has features like variable substitution, section handling, merging, and serialization.

I am building this to learn and get into zig since I never really got into languages like rust or C but love the way zig types. So please give any feedback you got!

Key Features:

  • .env and .ini Parsing: Supports comments, quoted values, empty lines, and section headers.

  • Variable Substitution: Handles patterns like ${VAR}, ${VAR:-fallback}, ${VAR:+val}, and escaped variables like \$HOME.

  • Typed Accessors: Has getInt(), getFloat(), getBool() for type-safe retrieval.

  • Section Handling: Go into specific sections using getSection("section_name").

  • Merging Configs: Merge configs with strategies like overwrite, skip_existing, or error_on_conflict.

  • Serialization: Write configs back to .env or .ini files.

  • Error Handling: Comprehensive error types like InvalidPlaceholder UnknownVariable, etc.

Usage Example:

Loading a .ini file:


; settings.ini

[database]

host=localhost

user=root

port=5432


const cfg = try Config.loadIniFile("settings.ini", allocator);

defer cfg.deinit();

const host = cfg.get("database.host") orelse "localhost";

const db = try cfg.getSection("database", allocator);

defer db.deinit();

const user = db.get("user") orelse "root";

Variable substitution:


HOST=localhost

PORT=8080

URL=http://${HOST}:${PORT}

FALLBACK=${NOT_SET:-default}


const url = try cfg.get("URL"); // http://localhost:8080

const fallback = try cfg.get("FALLBACK"); // "default"

Roadmap:

  • Integration with std.process.getEnvMap() for process env loading. → just learned about this.

  • Support for nested substitutions like ${A:-${B:-fallback}}.

  • Enhanced error handling for circular references.

  • Generic accessor: get(T: type, key, allocator) ?T.

  • Fuzz testing for .env and .ini parsing.

  • Maybe: Add support for additional config formats (e.g. JSON, TOML?).

I am new to Zig and would greatly appreciate any feedback, suggestions, or contributions. Feel free to check out the repo here: GitHub - Niek-HM/zig-config: Simple zig library that handle's .env / .ini files

Looking forward to your thoughts!

7 Likes

I quite like the API (imo one of the hardest tasks when writing a library, besides good naming) and I will keep this library in mind for a project I want to do at some point™.

Since TOML is these days used quite a lot on Linux (mainly in the systemd suite), this would be a nice addition and your Merging features immediately made me think of it.

So it would be a nice (and useful) addition for writing systemd-generators.

But as with anything Open Source, if you want to do this is up to you.

I guess you mean with that that the Config structs can create an or load from an EnvMap?

I have not yet read into this so it might not be right for me, but I saw someone comment that it might be useful in projects using .env’s… Will prob look into it this weekend to see if it is worth using.

As for TOML, I want to add them, but first I want to finish up the .env and .ini, to have the main features down and then expand.

Regarding additional formats: if you use an off-the-shelf YAML parser, you get JSON support for free too (YAML is a superset of JSON weirdly enough).

Lol, it does look really similar. Good to know for when I start working on it!

I updated to a v0.2 which has toml support (outside of timestamps). Hope it will be usefull for you!

1 Like

This library is pretty cool. I like how much effort you’ve put into providing a clean interface.

I’ve been reading the source code a bit, and noticed that you used errdefer in some places to clean up allocations on errors, which is a good habit to get into. However, there are still a lot of places where allocation failures lead to memory leaks.

There’s a really nice utility in std.testing that allows you to easily throw together a test that checks if leaks happen for any allocation failure point:

fn testAllocFailuresFn(allocator: std.mem.Allocator) !void {
    const text =
        \\A1=
        \\A2=value
        \\R1=${A1:-fallback}
        \\R2=${A2:-fallback}
        \\R3=${MISSING:-fallback}
        \\R4=${A1-fallback}
        \\R5=${A2-fallback}
        \\R6=${MISSING-fallback}
    ;

    var cfg = try Config.parseEnv(text, allocator);
    cfg.deinit();
}

test "proper cleanup on allocation failure" {
    try std.testing.checkAllAllocationFailures(std.testing.allocator, testAllocFailuresFn, .{});
}

I actually just learned about checkAllAllocationFailures right after making my own implementation of something similar, so I may as well share that too:

test "proper cleanup on allocation failure1" {
    const allocator = testing.allocator;
    const text =
        \\A1=
        \\A2=value
        \\R1=${A1:-fallback}
        \\R2=${A2:-fallback}
        \\R3=${MISSING:-fallback}
        \\R4=${A1-fallback}
        \\R5=${A2-fallback}
        \\R6=${MISSING-fallback}
    ;

    const total_allocations = blk: {
        var non_failer: std.testing.FailingAllocator = .init(allocator, .{});

        var cfg = try Config.parseEnv(text, non_failer.allocator());
        defer cfg.deinit();

        break :blk non_failer.allocations;
    };

    var any_leaked = false;

    var arena: std.heap.ArenaAllocator = .init(allocator);
    defer arena.deinit();

    for (0..total_allocations) |fail_index| {
        _ = arena.reset(.retain_capacity);

        var failer: std.testing.FailingAllocator = .init(arena.allocator(), .{
            .fail_index = fail_index,
        });

        _ = Config.parseEnv(text, failer.allocator()) catch {
            const leaked_count = failer.allocations -| failer.deallocations;
            if (leaked_count > 0) {
                any_leaked = true;

                std.debug.print("fail_index: {}, leaked_count: {}\n", .{ fail_index, leaked_count });

                if (@errorReturnTrace()) |stack_trace| {
                    const debug_info = try std.debug.getSelfDebugInfo();
                    const tty_config = std.io.tty.detectConfig(std.io.getStdErr());

                    try std.debug.writeStackTrace(stack_trace.*, std.io.getStdErr().writer(), debug_info, tty_config);
                }
            }

            continue;
        };
    }

    try std.testing.expect(!any_leaked);
}

The difference between my implementation and checkAllAllocationFailures is that I don’t actually print the traces of the leaked allocations themselves, just the error trace for the failed allocation.


A final thing to note is that it’s very easy to introduce double-frees when adding more errdefer cleanup code. I assume you are already on top of this since none of your errdefer code fails in this way, but it’s worth pointing out the common mistake:

var container: Container = try .init(allocator);
errdefer container.deinit();

var item1: Item = try .init(allocator);
errdefer item1.deinit();

try container.takeOwnershipOf(item1);

var item2: Item = try .init(allocator);
errdefer item2.deinit();

// If this line errors, `item1` will be freed twice:
// once when `item1.deinit()` is executed,
// and once when `container.deinit()` is executed.
try container.takeOwnershipOf(item2);

This mistake would be fixed like so:

var container: Container = try .init(allocator);
errdefer container.deinit();

{
    var item1: Item = try .init(allocator);
    errdefer item1.deinit();
    
    try container.takeOwnershipOf(item1);
}

{
    var item2: Item = try .init(allocator);
    errdefer item2.deinit();
    
    try container.takeOwnershipOf(item2);
}
1 Like

Ah, you are totally right, it is something I used when adding toml and cleaning some stuff since it seemed to fix a leak but it just pushed it to another place… I’ll try to fix it tomorrow and push it with some other clean-up’s I did.

1 Like

It took a bit long, but because of your comment I also found some other problems which made it way more complex. So, I basically reworked most of it. There are still some problems I will have to fix later, but I thought it is good enough for the normal user and it improves so much I should release it. The rest of the improvements are on the way as well!