xcaeser/zli
๐ Zig command-line interfaces made easy. A blazing fast CLI framework. Build ergonomic, high-performance command-line tools with zig.
A blazing-fast CLI framework for Zig.
Build modular, ergonomic, and high-performance CLIs with ease.
Batteries included. ZLI reference docs
--flag
, --flag=value
, shorthand -abc
)bool
, int
, string
required
, optional
, variadic
zig fetch --save=zli https://github.com/xcaeser/zli/archive/v4.1.1.tar.gz
Add to your build.zig
:
const zli_dep = b.dependency("zli", .{ .target = target, .optimize = optimize });
exe.root_module.addImport("zli", zli_dep.module("zli"));
your-app/
โโโ build.zig
โโโ src/
โ โโโ main.zig
โ โโโ cli/
โ โโโ root.zig // zli entrypoint
โ โโโ run.zig // subcommand 1
โ โโโ version.zig // subcommand 1
... // subcommand of subcommands, go nuts
root.zig
is the entry point// src/main.zig
const std = @import("std");
const fs = std.fs;
const cli = @import("cli/root.zig");
pub fn main() !void {
const allocator = std.heap.smp_allocator;
const file = fs.File.stdout();
var writer = file.writerStreaming(&.{}).interface;
const root = try cli.build(&writer, allocator);
defer root.deinit();
try root.execute(.{}); // Or pass data with: try root.execute(.{ .data = &my_data });
try writer.flush(); // Don't forget to flush!
}
// src/cli/root.zig
const std = @import("std");
const Writer = std.Io.Writer;
const zli = @import("zli");
const run = @import("run.zig");
const version = @import("version.zig");
pub fn build(writer: *Writer, allocator: std.mem.Allocator) !*zli.Command {
const root = try zli.Command.init(writer, allocator, .{
.name = "blitz",
.description = "Your dev toolkit CLI",
}, showHelp);
try root.addCommands(&.{
try run.register(allocator),
try version.register(allocator),
});
return root;
}
fn showHelp(ctx: zli.CommandContext) !void {
try ctx.command.printHelp();
}
// src/cli/run.zig
const std = @import("std");
const Writer = std.Io.Writer;
const zli = @import("zli");
const now_flag = zli.Flag{
.name = "now",
.shortcut = "n",
.description = "Run immediately",
.type = .Bool,
.default_value = .{ .Bool = false },
};
pub fn register(writer: *Writer,allocator: std.mem.Allocator) !*zli.Command {
const cmd = try zli.Command.init(writer, allocator, .{
.name = "run",
.description = "Run your workflow",
}, run);
try cmd.addFlag(now_flag);
try cmd.addPositionalArg(.{
.name = "script",
.description = "Script to execute",
.required = true,
});
try cmd.addPositionalArg(.{
.name = "env",
.description = "Environment name",
.required = false,
});
return cmd;
}
fn run(ctx: zli.CommandContext) !void {
const now = ctx.flag("now", bool); // type-safe flag access
const script = ctx.getArg("script") orelse {
try ctx.writer.print("Missing script arg\n", .{});
return;
};
const env = ctx.getArg("env") orelse "default";
std.debug.print("Running {s} in {s} (now = {})\n", .{ script, env, now });
// You can also get other commands by name:
// if (ctx.root.findCommand("create")) |create_cmd| {
// try create_cmd.printUsageLine();
// }
// if you passed data to your root command, you can access it here:
// const object = ctx.getContextData(type_of_your_data); // can be struct, []const u8, etc., object is a pointer.
};
// src/cli/version.zig
const std = @import("std");
const Writer = std.Io.Writer;
const zli = @import("zli");
pub fn register(writer: *Writer, allocator: std.mem.Allocator) !*zli.Command {
return zli.Command.init(writer, allocator, .{
.name = "version",
.shortcut = "v",
.description = "Show CLI version",
}, show);
}
fn show(ctx: zli.CommandContext) !void {
std.debug.print("{?}\n", .{ctx.root.options.version});
}
Available funtions:
spinner.start
: to add a new line. sets the spinner to runningspinner.updateStyle
: to update the spinner stylespinner.updateMessage
: to update text of a running spinnerspinner.succeed
, fail
, info
, preserve
: mandatory to complete a line you started. each spinner.start
needs a spinner.succeed
, fail
etc.. spinner after this action is done for that specific linespinner.print
instead of your own writer.print
to not have non-displayed messages as spinner works on its own threadconst std = @import("std");
const zli = @import("zli");
pub fn run(ctx: zli.CommandContext) !void {
var spinner = ctx.spinner;
spinner.updateStyle(.{ .frames = Spinner.SpinnerStyles.earth, .refresh_rate_ms = 150 }); // many styles available
// Step 1
try spinner.start("Step 1", .{}); // New line
std.Thread.sleep(2000 * std.time.ns_per_ms);
try spinner.succeed("Step 1 success", .{}); // each start must be closed with succeed, fail, info, preserve
spinner.updateStyle(.{ .frames = Spinner.SpinnerStyles.weather, .refresh_rate_ms = 150 }); // many styles available
// Step 2
try spinner.start("Step 2", .{}); // New line
std.Thread.sleep(3000 * std.time.ns_per_ms);
spinner.updateStyle(.{ .frames = Spinner.SpinnerStyles.dots, .refresh_rate_ms = 150 }); // many styles available
try spinner.updateMessage("Step 2: Calculating things...", .{}); // update the text of step 2
const i = work(); // do some work
try spinner.info("Step 2 info: {d}", .{i});
// Step 3
try spinner.start("Step 3", .{});
std.Thread.sleep(2000 * std.time.ns_per_ms);
try spinner.fail("Step 3 fail", .{});
try spinner.print("Finish\n", .{}); // instead of using ctx.writer or another writer to avoid concurrency issues
}
fn work() u128 {
var i: u128 = 1;
for (0..100000000) |t| {
i = (t + i);
}
return i;
}
Commands & subcommands
Command aliases
Flags & shorthands
Type-safe flag values
Positional args (required, optional, variadic)
Named access: ctx.getArg("name")
Context data
Help/version auto handling
Deprecation notices
Pretty-aligned help for flags & args
Clean usage output like Cobra
Spinners and loading state (very powerful)
Persistent flags
See docs.md for full usage, examples, and internals.
MIT. See LICENSE. Contributions welcome.