### ๐Ÿ“Ÿ zli - a blazing-fast CLI framework for Zig. Build modular, ergonomic, and high-performance CLIs with ease. Batteries included. [ZLI reference docs](https://xcaeser.github.io/zli) [![Tests](https://github.com/xcaeser/zli/actions/workflows/main.yml/badge.svg)](https://github.com/xcaeser/zli/actions/workflows/main.yml) [![Zig Version](https://img.shields.io/badge/Zig_Version-0.16.0-orange.svg?logo=zig)](README.md) [![License: MIT](https://img.shields.io/badge/License-MIT-lightgrey.svg?logo=cachet)](LICENSE) [![Built by xcaeser](https://img.shields.io/badge/Built%20by-@xcaeser-blue)](https://github.com/xcaeser) [![Version](https://img.shields.io/badge/ZLI-v5.1.2-green)](https://github.com/xcaeser/zli/releases) ## ๐Ÿš€ Features - Modular commands & subcommands - Fast flag parsing (`--flag`, `--flag=value`, shorthand `-abc`) - Type-safe support for `bool`, `int`, `string` - Named positional arguments with `required`, `optional`, `variadic` - Auto help/version/deprecation handling - Pretty help output with aligned flags & args - Spinners for a more interactive experience - Usage hints, context-aware ## ๐Ÿ“ฆ Installation ```sh zig fetch --save=zli https://github.com/xcaeser/zli/archive/v5.1.2.tar.gz ``` Add to your `build.zig`: ```zig const zli_dep = b.dependency("zli", .{ .target = target, .optimize = optimize }); exe.root_module.addImport("zli", zli_dep.module("zli")); ``` ## ๐ŸŒฑ Single-file Quick Start Start with one `src/main.zig`: initialize the root command, add a flag, add a subcommand, then run it. ```zig const std = @import("std"); const Io = std.Io; const zli = @import("zli"); pub fn main(init: std.process.Init) !void { const io = init.io; var wbuf: [1024]u8 = undefined; var stdout_writer = Io.File.Writer.init(.stdout(), io, &wbuf); const stdout = &stdout_writer.interface; defer stdout.flush() catch {}; var rbuf: [1024]u8 = undefined; var stdin_reader = Io.File.Reader.init(.stdin(), io, &rbuf); const stdin = &stdin_reader.interface; const init_options = zli.InitOptions{ .allocator = init.gpa, .io = io, .writer = stdout, .reader = stdin, }; const root = try zli.Command.init(init_options, .{ .name = "hello", .description = "A tiny zli app", .version = .{ .major = 0, .minor = 1, .patch = 0, .pre = null, .build = null }, }, showHelp); try root.addFlag(.{ .name = "verbose", .shortcut = "v", .description = "Print more details", .type = .Bool, .default_value = .{ .Bool = false }, .persistent = true, }); const greet = try zli.Command.init(init_options, .{ .name = "greet", .description = "Greet someone", }, greetSomeone); try greet.addPositionalArg(.{ .name = "name", .description = "Who to greet", .required = true, }); try root.addCommand(greet); var args_iter = init.minimal.args.iterate(); root.runAndExit(&args_iter, .{}); } fn showHelp(ctx: zli.CommandContext) !void { try ctx.command.printHelp(); } fn greetSomeone(ctx: zli.CommandContext) !void { const verbose = ctx.flag("verbose", bool); const name = ctx.getArg("name") orelse "friend"; if (verbose) { try ctx.writer.print("Preparing greeting for {s}\n", .{name}); } try ctx.writer.print("Hello, {s}!\n", .{name}); } ``` Run it: ```sh zig build run -- greet Ada zig build run -- --verbose greet Ada zig build run -- greet Ada --help ``` Use `runAndExit` for normal CLI binaries: it prints errors/help and exits with the right process code. Use `execute` when you want library-friendly behavior for tests, embedding, or custom error handling; it returns errors instead of exiting the process. ## ๐Ÿ—‚ Folder Structure (but you can do what you want) ``` 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 ``` - Each command is in its own file - You explicitly register subcommands - `root.zig` is the entry point ## ๐Ÿงช Example ### Your program ```zig // src/main.zig const std = @import("std"); const Io = std.Io; const cli = @import("cli/root.zig"); pub fn main(init: std.process.Init) !void { const io = init.io; var wbuf: [1024]u8 = undefined; var stdout_writer = Io.File.Writer.init(.stdout(), io, &wbuf); const stdout = &stdout_writer.interface; defer stdout.flush() catch {}; var rbuf: [1024]u8 = undefined; var stdin_reader = Io.File.Reader.init(.stdin(), io, &rbuf); const stdin = &stdin_reader.interface; const root = try cli.build(.{ .allocator = init.gpa, .io = io, .writer = stdout, .reader = stdin, }); var argsIter = init.minimal.args.iterate(); root.runAndExit(&argsIter, .{}); // Or pass data with: root.runAndExit(&argsIter, .{ .data = &my_data }); } ``` ### Root command - entrypoint ```zig // src/cli/root.zig const std = @import("std"); const zli = @import("zli"); const run = @import("run.zig"); const version = @import("version.zig"); pub fn build(init_options: zli.InitOptions) !*zli.Command { const root = try zli.Command.init(init_options, .{ .name = "blitz", .description = "Your dev toolkit CLI", .version = .{ .major = 0, .minor = 0, .patch = 1, .pre = null, .build = null }, }, showHelp); try root.addCommands(&.{ try run.register(init_options), try version.register(init_options), }); return root; } fn showHelp(ctx: zli.CommandContext) !void { try ctx.command.printHelp(); } ``` ### Run subcommand ```zig // src/cli/run.zig const std = @import("std"); 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(init_options: zli.InitOptions) !*zli.Command { const cmd = try zli.Command.init(init_options, .{ .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. }; ``` ### Version subcommand ```zig // src/cli/version.zig const std = @import("std"); const zli = @import("zli"); pub fn register(init_options: zli.InitOptions) !*zli.Command { return zli.Command.init(init_options, .{ .name = "version", .shortcut = "v", .description = "Show CLI version", }, show); } fn show(ctx: zli.CommandContext) !void { try ctx.root.printVersion(); } ``` ### Spinners example Available functions: - `spinner.start`: to add a new line. sets the spinner to running - `spinner.updateStyle`: to update the spinner style - `spinner.updateMessage`: to update text of a running spinner - `spinner.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 line - Recommendation: use `spinner.print` instead of your own `writer.print` to not have non-displayed messages as spinner works on its own thread ```zig const std = @import("std"); const zli = @import("zli"); pub fn run(ctx: zli.CommandContext) !void { const spinner = ctx.spinner; spinner.updateStyle(.{ .frames = zli.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 = zli.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 = zli.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; } ``` ## โœ… Features Checklist - [x] Commands & subcommands - [x] Command aliases - [x] Flags & shorthands - [x] Type-safe flag values - [x] Positional args (required, optional, variadic) - [x] Named access: `ctx.getArg("name")` - [x] Context data - [x] Help/version auto handling - [x] Deprecation notices - [x] Pretty-aligned help for flags & args - [x] Clean usage output like Cobra - [x] Spinners and loading state (very powerful) - [x] Persistent flags ## ๐Ÿ“š Documentation See [docs](https://xcaeser.github.io/zli/) for full usage, examples, and internals. ## ๐Ÿ“ License MIT. See [LICENSE](LICENSE). Contributions welcome.