A tiny zero-heap-allocation CLI library for Zig.
  • Zig 94.4%
  • Nix 5.6%
Find a file
2025-12-15 18:05:22 +01:00
src Add print.Flags 2025-12-15 00:02:35 +01:00
.envrc Setup flake.nix 2025-12-14 19:24:53 +01:00
.gitignore Setup flake.nix 2025-12-14 19:24:53 +01:00
build.zig Proof of concept 2025-12-14 23:34:54 +01:00
build.zig.zon Update build.zig.zon 2025-12-15 00:22:08 +01:00
flake.lock Setup flake.nix 2025-12-14 19:24:53 +01:00
flake.nix Setup flake.nix 2025-12-14 19:24:53 +01:00
LICENSE Initial commit 2025-12-14 19:17:46 +01:00
README.md Update README 2025-12-15 18:05:22 +01:00

zli

zli is a composable, type-safe command-line interface library for Zig. It leverages comptime to define commands and flags, ensuring minimal runtime overhead and strict compile-time checks.

Installation

Add zli to your build.zig.zon:

zig fetch --save git+https://github.com/lukasl-dev/zli

And to your build.zig.

const zli = b.dependency("zli", .{});

Usage

Here is a clean example demonstrating how to define commands and pass a custom context.

const std = @import("std");

const AppState = struct {
    debug: bool = false,
};

const zli = @import("zli").customize(AppState);

const commands: []const zli.Command = &.{
    .{
        .name = "greet",
        .description = "Prints a greeting",
        .run = &runGreet,
    },
};

fn runGreet(ctx: zli.Context) !void {
    try ctx.stdout.print("Hello from zli!\n", .{});
    if (ctx.custom.debug) {
        try ctx.stdout.print("(Debug mode enabled)\n", .{});
    }
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    var args = try std.process.argsWithAllocator(gpa.allocator());
    defer args.deinit();

    const exec = args.next() orelse return;

    var stdout_buf: [4096]u8 = undefined;
    var stdout = std.fs.File.stdout().writer(&stdout_buf);

    var stderr_buf: [4096]u8 = undefined;
    var stderr = std.fs.File.stderr().writer(&stderr_buf);

    var runner: zli.Commands(commands) = .init(
        &stdout.interface,
        &stderr.interface,
        exec,
    );

    const cmd_name = args.next() orelse {
        // no command provided: print help
        const manual: zli.print.Manual(commands) = .default;
        try manual.printContextless(&stdout.interface, exec);

        return;
    };

    const app_state: AppState = .{ .debug = true };
    runner.run(cmd_name, &args, app_state) catch |err| switch (err) {
        error.UnknownCommand => {
            try stderr.print("unknown command: {s}\n", .{cmd_name});
        },
        else => return err,
    };

    // don't forget to flush :)
    try stdout.interface.flush();
    try stderr.interface.flush();
}