zig-cli

Usage

The 31 examples below are taken from zig-cli's README.

// Define options as a struct
const MyOptions = struct {
    name: []const u8,
    port: u16 = 8080,
};

// Type-safe action
fn run(ctx: *cli.Context(MyOptions)) !void {
    const name = ctx.get(.name);  // Compile-time validated!
    const port = ctx.get(.port);
}
const zig_cli = b.dependency("zig-cli", .{
    .target = target,
    .optimize = optimize,
});

exe.root_module.addImport("zig-cli", zig_cli.module("zig-cli"));
const std = @import("std");
const cli = @import("zig-cli");

// 1. Define options as a struct - that's it!
const GreetOptions = struct {
    name: []const u8 = "World",  // With default value
    enthusiastic: bool = false,   // Boolean flag
};

// 2. Type-safe action function
fn greet(ctx: *cli.Context(GreetOptions)) !void {
    const io = std.Options.debug_io;
    var buf: [4096]u8 = undefined;
    var file_writer = std.Io.File.stdout().writerStreaming(io, &buf);
    const stdout = &file_writer.interface;

    // Compile-time validated field access - no strings!
    const name = ctx.get(.name);
    const punct: []const u8 = if (ctx.get(.enthusiastic)) "!" else ".";

    try stdout.print("Hello, {s}{s}\n", .{ name, punct });
    try stdout.flush();
}

pub fn main(init: std.process.Init) !void {
    const allocator = init.gpa;

    // 3. Create command - options auto-generated!
    var cmd = try cli.Command(GreetOptions).init(allocator, "greet", "Greet someone");
    defer cmd.deinit();

    _ = cmd.setAction(greet);

    // 4. Parse args and execute
    var args_list = std.ArrayList([]const u8){};
    defer args_list.deinit(allocator);

    var args_iter = std.process.Args.Iterator.init(init.minimal.args);
    _ = args_iter.skip(); // skip program name
    while (args_iter.next()) |arg| {
        try args_list.append(allocator, arg);
    }

    var parser = cli.Parser.init(allocator);
    try parser.parse(cmd.getCommand(), args_list.items);
}
const std = @import("std");
const prompt = @import("zig-cli").prompt;

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

    // Text prompt
    var text_prompt = prompt.TextPrompt.init(allocator, "What is your name?");
    defer text_prompt.deinit();
    const name = try text_prompt.prompt();
    defer allocator.free(name);

    // Confirm prompt
    var confirm_prompt = prompt.ConfirmPrompt.init(allocator, "Continue?");
    defer confirm_prompt.deinit();
    const confirmed = try confirm_prompt.prompt();

    _ = confirmed;

    // Select prompt
    const choices = [_]prompt.SelectPrompt.Choice{
        .{ .label = "Option 1", .value = "opt1" },
        .{ .label = "Option 2", .value = "opt2" },
    };
    var select_prompt = prompt.SelectPrompt.init(allocator, "Choose:", &choices);
    defer select_prompt.deinit();
    const selected = try select_prompt.prompt();
    defer allocator.free(selected);
}
const GreetOptions = struct {
    name: []const u8,              // Required string
    age: ?u16 = null,              // Optional integer
    times: u8 = 1,                 // With default value
    verbose: bool = false,         // Boolean flag
    format: enum { text, json } = .text, // Enum support
};

fn greetAction(ctx: *cli.Context(GreetOptions)) !void {
    // Compile-time validated field access - no string lookups!
    const name = ctx.get(.name);      // Returns []const u8
    const age = ctx.get(.age);        // Returns ?u16
    const times = ctx.get(.times);    // Returns u8

    // Or parse entire struct at once
    const opts = try ctx.parse();

    _ = age;
    _ = times;
    std.debug.print("Hello, {s}!\n", .{opts.name});
}

pub fn main(init: std.process.Init) !void {
    const allocator = init.gpa;

    // Auto-generates CLI options from struct fields!
    var cmd = try cli.Command(GreetOptions).init(allocator, "greet", "Greet a user");
    defer cmd.deinit();

    _ = cmd.setAction(greetAction);

    var args_list = std.ArrayList([]const u8){};
    defer args_list.deinit(allocator);

    var args_iter = std.process.Args.Iterator.init(init.minimal.args);
    _ = args_iter.skip();
    while (args_iter.next()) |arg| {
        try args_list.append(allocator, arg);
    }

    var parser = cli.Parser.init(allocator);
    try parser.parse(cmd.getCommand(), args_list.items);
}
// Create a base command
const cmd = try cli.BaseCommand.init(allocator, "myapp", "Description");

// Add options manually
const option = cli.Option.init("name", "long-name", "Description", .string)
    .withShort('n')              // Short flag (-n)
    .withRequired(true)          // Make it required
    .withDefault("value");       // Set default value

_ = try cmd.addOption(option);
const arg = cli.Argument.init("name", "Description", .string)
    .withRequired(true)          // Required argument
    .withVariadic(false);        // Accept multiple values

_ = try cmd.addArgument(arg);
const subcmd = try cli.BaseCommand.init(allocator, "subcmd", "Subcommand description");

// Add aliases for the command
_ = try subcmd.addAlias("sub");
_ = try subcmd.addAlias("s");

const opt = cli.Option.init("opt", "option", "Option description", .string);
_ = try subcmd.addOption(opt);

_ = subcmd.setAction(myAction);
_ = try app.addCommand(subcmd);
var chain = cli.Middleware.MiddlewareChain.init(allocator);
defer chain.deinit();

// Add built-in middleware
try chain.use(cli.Middleware.Middleware.init("logging", cli.Middleware.loggingMiddleware));
try chain.use(cli.Middleware.Middleware.init("timing", cli.Middleware.timingMiddleware));
try chain.use(cli.Middleware.Middleware.init("validation", cli.Middleware.validationMiddleware));

// Custom middleware
fn authMiddleware(ctx: _cli.Middleware.MiddlewareContext) !bool {
    const is_authenticated = checkAuth();
    if (!is_authenticated) {
        try ctx.set("error", "Unauthorized");
        return false; // Stop chain
    }
    try ctx.set("user", "john@example.com");
    return true; // Continue
}

// Add with priority (lower runs first)
try chain.use(cli.Middleware.Middleware.init("auth", authMiddleware).withOrder(-10));

// Execute middleware chain before command
var middleware_ctx = cli.Middleware.MiddlewareContext.init(allocator, parse_context, command);
defer middleware_ctx.deinit();

if (try chain.execute(&middleware_ctx)) {
    // All middleware passed, execute command
    try command.executeAction(parse_context);
}
fn myAction(ctx: _cli.BaseCommand.ParseContext) !void {
    // Get option value
    const value = ctx.getOption("name") orelse "default";

    // Check if option was provided
    if (ctx.hasOption("verbose")) {
        // Do something
    }

    // Get positional argument
    const arg = ctx.getArgument(0) orelse return error.MissingArgument;

    // Get argument count
    const count = ctx.getArgumentCount();

    _ = value;
    _ = arg;
    _ = count;
}
// Runtime API (string-based, via BaseCommand)
const value = ctx.getOption("name");  // Returns ?[]const u8
if (value) |v| {
    const age_str = ctx.getOption("age") orelse "0";
    const age = try std.fmt.parseInt(u16, age_str, 10);
    _ = v;
    _ = age;
}

// Typed API (compile-time validated, via cli.Command(T))
const name = ctx.get(.name);  // Returns []const u8 directly
const age = ctx.get(.age);    // Returns u16, already parsed
//              ^^^ Compile-time validated enum field!
const AppConfig = struct {
    database: struct {
        host: []const u8,
        port: u16,
        max_connections: u32 = 100,
    },
    log_level: enum { debug, info, warn, @"error" } = .info,
    debug: bool = false,
};

// Load with full type checking
var config = try cli.config.load(AppConfig, allocator, "config.toml");
defer config.deinit();

// Direct field access - no optionals, no string parsing!
std.debug.print("DB: {s}:{d}\n", .{
    config.value.database.host,
    config.value.database.port,
});
std.debug.print("Log Level: {s}\n", .{@tagName(config.value.log_level)});

// Auto-discovery also works
var discovered = try cli.config.discover(AppConfig, allocator, "myapp");
defer discovered.deinit();
var text = prompt.TextPrompt.init(allocator, "Enter value:");
defer text.deinit();

_ = text.withPlaceholder("placeholder text");
_ = text.withDefault("default value");
_ = text.withValidation(myValidator);

const value = try text.prompt();
defer allocator.free(value);
fn myValidator(value: []const u8) ?[]const u8 {
    if (value.len < 3) {
        return "Value must be at least 3 characters";
    }
    return null;  // Valid
}
var confirm = prompt.ConfirmPrompt.init(allocator, "Continue?");
defer confirm.deinit();

_ = confirm.withDefault(true);

const result = try confirm.prompt();  // Returns bool
const choices = [_]prompt.SelectPrompt.Choice{
    .{ .label = "TypeScript", .value = "ts", .description = "JavaScript with types" },
    .{ .label = "Zig", .value = "zig", .description = "Systems programming" },
};

var select = prompt.SelectPrompt.init(allocator, "Choose a language:", &choices);
defer select.deinit();

const selected = try select.prompt();
defer allocator.free(selected);
const choices = [_]prompt.MultiSelectPrompt.Choice{
    .{ .label = "Option 1", .value = "opt1" },
    .{ .label = "Option 2", .value = "opt2" },
};

var multi = try prompt.MultiSelectPrompt.init(allocator, "Select options:", &choices);
defer multi.deinit();

const selected = try multi.prompt();  // Returns [][]const u8
defer {
    for (selected) |item| allocator.free(item);
    allocator.free(selected);
}
var password = prompt.PasswordPrompt.init(allocator, "Enter password:");
defer password.deinit();

_ = password.withMaskChar('_');
_ = password.withValidation(validatePassword);

const pwd = try password.prompt();
defer allocator.free(pwd);
var spinner = prompt.SpinnerPrompt.init(allocator, "Loading data...");
try spinner.start();

// Do some work
_ = std.c.nanosleep(&.{ .sec = 2, .nsec = 0 }, null);

try spinner.stop("Data loaded successfully!");
// Intro/Outro for CLI flows
try prompt.intro(allocator, "My CLI Application");
// ... your application logic ...
try prompt.outro(allocator, "All done! Thanks for using our CLI.");

// Notes and logs
try prompt.note(allocator, "Important", "This is additional information");
try prompt.log(allocator, .info, "Starting process...");
try prompt.log(allocator, .success, "Process completed!");
try prompt.log(allocator, .warning, "This is a warning");
try prompt.log(allocator, .error_level, "An error occurred");

// Cancel message
try prompt.cancel(allocator, "Operation was canceled");
// Simple box
try prompt.box(allocator, "Title", "This is the content");

// Custom box with styling
var box = prompt.Box.init(allocator);
box = box.withStyle(.rounded);  // .single, .double, .rounded, .ascii
box = box.withPadding(2);
try box.render("My Box",
    \\Line 1 of content
    \\Line 2 of content
    \\Line 3 of content
);
var num_prompt = prompt.NumberPrompt.init(allocator, "Enter port:", .integer);
defer num_prompt.deinit();

_ = num_prompt.withRange(1, 65535);  // Set min/max
_ = num_prompt.withDefault(8080);

const port = try num_prompt.prompt();  // Returns f64
const port_int = @as(u16, @intFromFloat(port));
var path_prompt = prompt.PathPrompt.init(allocator, "Select file:", .file);
defer path_prompt.deinit();

_ = path_prompt.withMustExist(true);  // Must exist
_ = path_prompt.withDefault("./config.toml");

const path = try path_prompt.prompt();
defer allocator.free(path);

// Press Tab to autocomplete based on filesystem
const prompts = [_]prompt.GroupPrompt.PromptDef{
    .{ .text = .{ .key = "name", .message = "Your name?" } },
    .{ .number = .{ .key = "age", .message = "Your age?", .number_type = .integer } },
    .{ .confirm = .{ .key = "agree", .message = "Do you agree?" } },
    .{ .select = .{
        .key = "lang",
        .message = "Choose language:",
        .choices = &[_]prompt.SelectPrompt.Choice{
            .{ .label = "Zig", .value = "zig" },
            .{ .label = "TypeScript", .value = "ts" },
        },
    }},
};

var group = prompt.GroupPrompt.init(allocator, &prompts);
defer group.deinit();

try group.run();

// Access results by key
const name = group.getText("name");
const age = group.getNumber("age");
const agreed = group.getBool("agree");
const lang = group.getText("lang");
var progress = prompt.ProgressBar.init(allocator, 100, "Processing files");
defer progress.deinit();

try progress.start();

for (0..100) |i| {
    // Do some work
    _ = std.c.nanosleep(&.{ .sec = 0, .nsec = 50 _ std.time.ns_per_ms }, null);
    try progress.update(i + 1);
}

try progress.finish();
const columns = [_]prompt.Table.Column{
    .{ .header = "Name", .alignment = .left },
    .{ .header = "Age", .alignment = .right },
    .{ .header = "Status", .alignment = .center },
};

var table = prompt.Table.init(allocator, &columns);
defer table.deinit();

table = table.withStyle(.rounded);  // .simple, .rounded, .double, .minimal

try table.addRow(&[_][]const u8{ "Alice", "30", "Active" });
try table.addRow(&[_][]const u8{ "Bob", "25", "Inactive" });
try table.addRow(&[_][]const u8{ "Charlie", "35", "Active" });

try table.render();
// Create styled text with chainable API
const styled = try prompt.style(allocator, "Error occurred")
    .red()
    .bold()
    .underline()
    .render();
defer allocator.free(styled);

try prompt.Terminal.init().write(styled);

// Available colors: black, red, green, yellow, blue, magenta, cyan, white
// Available styles: bold(), dim(), italic(), underline()
// Available backgrounds: bgRed(), bgGreen(), bgBlue(), etc.
// 1. Define your config schema as a struct
const AppConfig = struct {
    database: struct {
        host: []const u8,
        port: u16,
    },
    log_level: enum { debug, info, warn, @"error" } = .info,
    debug: bool = false,
};

// 2. Load with full type checking
var config = try cli.config.load(AppConfig, allocator, "config.toml");
defer config.deinit();

// 3. Direct field access - type-safe!
std.debug.print("DB: {s}:{d}\n", .{
    config.value.database.host,
    config.value.database.port,
});

// Load from string
var config2 = try cli.config.loadFromString(AppConfig, allocator, toml_content, .toml);
defer config2.deinit();

// Auto-discover config file
var config3 = try cli.config.discover(AppConfig, allocator, "myapp");
defer config3.deinit();
// Searches for: myapp.toml, myapp.json5, myapp.jsonc
// In: ., ./.config, ~/.config/myapp
var raw_config = cli.config.Config.init(allocator);
defer raw_config.deinit();

try raw_config.loadFromFile("config.toml", .auto);

// Get typed values
if (raw_config.getString("name")) |name| {
    std.debug.print("Name: {s}\n", .{name});
}

if (raw_config.getInt("port")) |port| {
    std.debug.print("Port: {d}\n", .{port});
}

if (raw_config.getBool("debug")) |debug| {
    std.debug.print("Debug: {}\n", .{debug});
}
const ansi = @import("zig-cli").prompt.Ansi;

const colored = try ansi.colorize(allocator, "text", .green);
defer allocator.free(colored);

// Convenience functions
const bold = try ansi.bold(allocator, "text");
const red = try ansi.red(allocator, "error");
const green = try ansi.green(allocator, "success");
const symbols = ansi.Symbols.forTerminal(supports_unicode);

std.debug.print("{s} Success!\n", .{symbols.checkmark});
std.debug.print("{s} Error!\n", .{symbols.cross});
std.debug.print("{s} Loading...\n", .{symbols.spinner[0]});