|
1 | 1 | # zig-cli |
2 | 2 |
|
3 | | -A modern, feature-rich CLI library for Zig, inspired by popular frameworks like clapp. Build beautiful command-line applications and interactive prompts with ease. |
| 3 | +A **type-safe, compile-time validated** CLI library for Zig. Define your CLI with structs, get full type safety and zero runtime overhead. |
| 4 | + |
| 5 | +**No string-based lookups. No runtime parsing. Just pure, type-safe Zig.** |
| 6 | + |
| 7 | +```zig |
| 8 | +// Define options as a struct |
| 9 | +const MyOptions = struct { |
| 10 | + name: []const u8, |
| 11 | + port: u16 = 8080, |
| 12 | +}; |
| 13 | +
|
| 14 | +// Type-safe action |
| 15 | +fn run(ctx: *cli.Context(MyOptions)) !void { |
| 16 | + const name = ctx.get(.name); // Compile-time validated! |
| 17 | + const port = ctx.get(.port); |
| 18 | +} |
| 19 | +``` |
| 20 | + |
| 21 | +Inspired by modern CLI frameworks, built for Zig's strengths. |
4 | 22 |
|
5 | 23 | ## Features |
6 | 24 |
|
7 | | -### CLI Framework |
8 | | -- **Fluent API**: Chainable builder pattern for intuitive CLI construction |
| 25 | +### CLI Framework (Type-Safe) |
| 26 | +- **Compile-Time Validation**: All field access validated at compile time |
| 27 | +- **Struct-Based Options**: Define CLI options as structs - auto-generate everything |
| 28 | +- **Zero Runtime Overhead**: All type checking happens at compile time |
| 29 | +- **IDE Autocomplete**: Full IntelliSense/LSP support for field names |
9 | 30 | - **Command Routing**: Support for nested subcommands with aliases |
10 | | -- **Argument Parsing**: Robust parsing with validation pipeline |
11 | | -- **Type Safety**: Strong typing for options (string, int, float, bool) |
12 | | -- **Auto-generated Help**: Beautiful help text generation |
13 | | -- **Validation**: Built-in validation with custom validators |
14 | | -- **Command Aliases**: Support for command shortcuts and alternative names |
15 | | -- **Middleware System**: Pre/post command hooks with built-in middleware |
| 31 | +- **Auto-Generated Help**: Beautiful help text from struct definitions |
| 32 | +- **Type Safety**: Enums, optionals, nested structs all supported |
| 33 | +- **Middleware System**: Type-safe pre/post command hooks |
16 | 34 |
|
17 | 35 | ### Interactive Prompts |
18 | 36 | - **State Machine**: Clean 5-state state machine (initial → active ↔ error → submit/cancel) |
@@ -71,42 +89,50 @@ exe.root_module.addImport("zig-cli", zig_cli.module("zig-cli")); |
71 | 89 | const std = @import("std"); |
72 | 90 | const cli = @import("zig-cli"); |
73 | 91 |
|
74 | | -fn greetAction(ctx: *cli.Command.ParseContext) !void { |
75 | | - const name = ctx.getOption("name") orelse "World"; |
| 92 | +// 1. Define options as a struct - that's it! |
| 93 | +const GreetOptions = struct { |
| 94 | + name: []const u8 = "World", // With default value |
| 95 | + enthusiastic: bool = false, // Boolean flag |
| 96 | +}; |
| 97 | +
|
| 98 | +// 2. Type-safe action function |
| 99 | +fn greet(ctx: *cli.Context(GreetOptions)) !void { |
76 | 100 | const stdout = std.io.getStdOut().writer(); |
77 | | - try stdout.print("Hello, {s}!\n", .{name}); |
| 101 | +
|
| 102 | + // Compile-time validated field access - no strings! |
| 103 | + const name = ctx.get(.name); |
| 104 | + const punct: []const u8 = if (ctx.get(.enthusiastic)) "!" else "."; |
| 105 | +
|
| 106 | + try stdout.print("Hello, {s}{s}\n", .{name, punct}); |
78 | 107 | } |
79 | 108 |
|
80 | 109 | pub fn main() !void { |
81 | 110 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; |
82 | 111 | defer _ = gpa.deinit(); |
83 | 112 | const allocator = gpa.allocator(); |
84 | 113 |
|
85 | | - // Create CLI application |
86 | | - var app = try cli.CLI.init( |
87 | | - allocator, |
88 | | - "myapp", |
89 | | - "1.0.0", |
90 | | - "My awesome CLI application" |
91 | | - ); |
92 | | - defer app.deinit(); |
93 | | -
|
94 | | - // Add options |
95 | | - const name_option = cli.Option.init("name", "name", "Your name", .string) |
96 | | - .withShort('n') |
97 | | - .withDefault("World"); |
98 | | - _ = try app.option(name_option); |
| 114 | + // 3. Create command - options auto-generated! |
| 115 | + var cmd = try cli.command(GreetOptions).init(allocator, "greet", "Greet someone"); |
| 116 | + defer cmd.deinit(); |
99 | 117 |
|
100 | | - // Set action |
101 | | - _ = app.action(greetAction); |
| 118 | + _ = cmd.setAction(greet); |
102 | 119 |
|
103 | | - // Parse arguments |
| 120 | + // 4. Parse and execute |
104 | 121 | const args = try std.process.argsAlloc(allocator); |
105 | 122 | defer std.process.argsFree(allocator, args); |
106 | | - try app.parse(args); |
| 123 | + try cli.Parser.init(allocator).parse(cmd.getCommand(), args[1..]); |
107 | 124 | } |
108 | 125 | ``` |
109 | 126 |
|
| 127 | +That's it! Run with: `myapp greet --name Alice --enthusiastic` |
| 128 | + |
| 129 | +**Benefits:** |
| 130 | +- ✅ Options auto-generated from struct fields |
| 131 | +- ✅ Compile-time validation - typos caught by compiler |
| 132 | +- ✅ Full IDE autocomplete support |
| 133 | +- ✅ No string-based lookups |
| 134 | +- ✅ Zero runtime overhead |
| 135 | + |
110 | 136 | ### Interactive Prompts |
111 | 137 |
|
112 | 138 | ```zig |
@@ -260,6 +286,156 @@ fn myAction(ctx: *cli.Command.ParseContext) !void { |
260 | 286 | } |
261 | 287 | ``` |
262 | 288 |
|
| 289 | +### Type-Safe API (Compile-Time Validated) |
| 290 | + |
| 291 | +zig-cli provides a fully typed API layer that leverages Zig's comptime features for maximum type safety and zero runtime overhead. |
| 292 | + |
| 293 | +#### Type-Safe Commands |
| 294 | + |
| 295 | +Define your command options as a struct and get compile-time validation: |
| 296 | + |
| 297 | +```zig |
| 298 | +const GreetOptions = struct { |
| 299 | + name: []const u8, // Required string |
| 300 | + age: ?u16 = null, // Optional integer |
| 301 | + times: u8 = 1, // With default value |
| 302 | + verbose: bool = false, // Boolean flag |
| 303 | + format: enum { text, json } = .text, // Enum support |
| 304 | +}; |
| 305 | +
|
| 306 | +fn greetAction(ctx: *cli.TypedContext(GreetOptions)) !void { |
| 307 | + // Compile-time validated field access - no string lookups! |
| 308 | + const name = ctx.get(.name); // Returns []const u8 |
| 309 | + const age = ctx.get(.age); // Returns ?u16 |
| 310 | + const times = ctx.get(.times); // Returns u8 |
| 311 | +
|
| 312 | + // Or parse entire struct at once |
| 313 | + const opts = try ctx.parse(); |
| 314 | +
|
| 315 | + std.debug.print("Hello, {s}!\n", .{opts.name}); |
| 316 | +} |
| 317 | +
|
| 318 | +pub fn main() !void { |
| 319 | + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; |
| 320 | + defer _ = gpa.deinit(); |
| 321 | + const allocator = gpa.allocator(); |
| 322 | +
|
| 323 | + // Auto-generates CLI options from struct fields! |
| 324 | + var cmd = try cli.TypedCommand(GreetOptions).init( |
| 325 | + allocator, |
| 326 | + "greet", |
| 327 | + "Greet a user", |
| 328 | + ); |
| 329 | + defer cmd.deinit(); |
| 330 | +
|
| 331 | + _ = cmd.setAction(greetAction); |
| 332 | +
|
| 333 | + const args = try std.process.argsAlloc(allocator); |
| 334 | + defer std.process.argsFree(allocator, args); |
| 335 | +
|
| 336 | + // Get underlying command for parsing |
| 337 | + try cli.Parser.init(allocator).parse(cmd.getCommand(), args[1..]); |
| 338 | +} |
| 339 | +``` |
| 340 | + |
| 341 | +Benefits: |
| 342 | +- ✅ **Compile-time validation** - field names validated at compile time |
| 343 | +- ✅ **IDE autocomplete** - full IntelliSense support |
| 344 | +- ✅ **Type safety** - no runtime string parsing or optionals |
| 345 | +- ✅ **Auto-generation** - options automatically created from struct |
| 346 | +- ✅ **Zero overhead** - comptime code generates efficient runtime |
| 347 | + |
| 348 | +#### Type-Safe Config |
| 349 | + |
| 350 | +Load config files with compile-time schema validation: |
| 351 | + |
| 352 | +```zig |
| 353 | +const AppConfig = struct { |
| 354 | + database: struct { |
| 355 | + host: []const u8, |
| 356 | + port: u16, |
| 357 | + max_connections: u32 = 100, |
| 358 | + }, |
| 359 | + log_level: enum { debug, info, warn, @"error" } = .info, |
| 360 | + debug: bool = false, |
| 361 | +}; |
| 362 | +
|
| 363 | +// Load with full type checking |
| 364 | +var config = try cli.config.loadTyped(AppConfig, allocator, "config.toml"); |
| 365 | +defer config.deinit(); |
| 366 | +
|
| 367 | +// Direct field access - no optionals, no string parsing! |
| 368 | +std.debug.print("DB: {s}:{d}\n", .{ |
| 369 | + config.value.database.host, |
| 370 | + config.value.database.port, |
| 371 | +}); |
| 372 | +std.debug.print("Log Level: {s}\n", .{@tagName(config.value.log_level)}); |
| 373 | +
|
| 374 | +// Auto-discovery also works |
| 375 | +var discovered = try cli.config.discoverTyped(AppConfig, allocator, "myapp"); |
| 376 | +defer discovered.deinit(); |
| 377 | +``` |
| 378 | + |
| 379 | +Supported types: |
| 380 | +- Primitives: `bool`, `i8`-`i64`, `u8`-`u64`, `f32`, `f64` |
| 381 | +- Strings: `[]const u8` |
| 382 | +- Enums: Any Zig enum |
| 383 | +- Optionals: `?T` for optional fields |
| 384 | +- Nested structs: Arbitrary depth |
| 385 | +- Arrays: Fixed-size arrays |
| 386 | + |
| 387 | +#### Type-Safe Middleware |
| 388 | + |
| 389 | +Create middleware with typed context instead of string HashMap: |
| 390 | + |
| 391 | +```zig |
| 392 | +const AuthData = struct { |
| 393 | + user_id: []const u8 = "", |
| 394 | + username: []const u8 = "", |
| 395 | + role: enum { admin, user, guest } = .guest, |
| 396 | + authenticated: bool = false, |
| 397 | +}; |
| 398 | +
|
| 399 | +fn authMiddleware(ctx: *cli.TypedMiddleware(AuthData)) !bool { |
| 400 | + // Type-safe field access with compile-time validation |
| 401 | + ctx.set(.user_id, "12345"); |
| 402 | + ctx.set(.username, "john_doe"); |
| 403 | + ctx.set(.role, .admin); // Enum - compile-time checked! |
| 404 | + ctx.set(.authenticated, true); |
| 405 | +
|
| 406 | + // Access with type safety |
| 407 | + if (ctx.get(.role) == .admin) { |
| 408 | + std.debug.print("Admin access granted\n", .{}); |
| 409 | + } |
| 410 | +
|
| 411 | + return true; |
| 412 | +} |
| 413 | +
|
| 414 | +// Use in middleware chain |
| 415 | +var chain = cli.TypedMiddlewareChain(AuthData).init(allocator); |
| 416 | +defer chain.deinit(); |
| 417 | +
|
| 418 | +try chain.useTyped(authMiddleware, "auth"); |
| 419 | +``` |
| 420 | + |
| 421 | +#### Runtime vs Typed API Comparison |
| 422 | + |
| 423 | +```zig |
| 424 | +// Runtime API (string-based) |
| 425 | +const value = ctx.getOption("name"); // Returns ?[]const u8 |
| 426 | +if (value) |v| { |
| 427 | + const age_str = ctx.getOption("age") orelse "0"; |
| 428 | + const age = try std.fmt.parseInt(u16, age_str, 10); |
| 429 | +} |
| 430 | +
|
| 431 | +// Typed API (compile-time validated) |
| 432 | +const name = ctx.get(.name); // Returns []const u8 directly |
| 433 | +const age = ctx.get(.age); // Returns u16, already parsed |
| 434 | +// ^^^^^ Compile-time validated enum field! |
| 435 | +``` |
| 436 | + |
| 437 | +See `examples/typed.zig` for a complete working example. |
| 438 | + |
263 | 439 | ### Prompts |
264 | 440 |
|
265 | 441 | #### Text Prompt |
@@ -521,24 +697,39 @@ try prompt.Terminal.init().write(styled); |
521 | 697 |
|
522 | 698 | ### Configuration Files |
523 | 699 |
|
524 | | -zig-cli supports loading configuration from TOML, JSONC (JSON with Comments), and JSON5 files. |
| 700 | +zig-cli supports type-safe configuration loading from TOML, JSONC (JSON with Comments), and JSON5 files. |
525 | 701 |
|
526 | 702 | #### Loading Config |
527 | 703 |
|
528 | 704 | ```zig |
529 | | -// Load from file (auto-detects format) |
530 | | -var config = try cli.config.load(allocator, "config.toml"); |
| 705 | +// 1. Define your config schema as a struct |
| 706 | +const AppConfig = struct { |
| 707 | + database: struct { |
| 708 | + host: []const u8, |
| 709 | + port: u16, |
| 710 | + }, |
| 711 | + log_level: enum { debug, info, warn, @"error" } = .info, |
| 712 | + debug: bool = false, |
| 713 | +}; |
| 714 | +
|
| 715 | +// 2. Load with full type checking |
| 716 | +var config = try cli.config.load(AppConfig, allocator, "config.toml"); |
531 | 717 | defer config.deinit(); |
532 | 718 |
|
533 | | -// Or load from string |
534 | | -var config2 = cli.config.Config.init(allocator); |
| 719 | +// 3. Direct field access - type-safe! |
| 720 | +std.debug.print("DB: {s}:{d}\n", .{ |
| 721 | + config.value.database.host, |
| 722 | + config.value.database.port, |
| 723 | +}); |
| 724 | +
|
| 725 | +// Load from string |
| 726 | +var config2 = try cli.config.loadFromString(AppConfig, allocator, toml_content, .toml); |
535 | 727 | defer config2.deinit(); |
536 | | -try config2.loadFromString(content, .toml); // or .jsonc, .json5 |
537 | 728 |
|
538 | 729 | // Auto-discover config file |
539 | | -var config3 = try cli.config.discover(allocator, "myapp"); |
| 730 | +var config3 = try cli.config.discover(AppConfig, allocator, "myapp"); |
540 | 731 | defer config3.deinit(); |
541 | | -// Searches for: myapp.toml, myapp.json5, myapp.jsonc, myapp.json |
| 732 | +// Searches for: myapp.toml, myapp.json5, myapp.jsonc |
542 | 733 | // In: ., ./.config, ~/.config/myapp |
543 | 734 | ``` |
544 | 735 |
|
@@ -639,6 +830,7 @@ Check out the `examples/` directory for complete examples: |
639 | 830 | - `advanced.zig` - Complex CLI with multiple commands and arguments |
640 | 831 | - `showcase.zig` - Comprehensive feature demonstration including all new prompts |
641 | 832 | - `config.zig` - Configuration file examples (TOML, JSONC, JSON5) |
| 833 | +- **`typed.zig`** - Type-safe API examples (compile-time validated) (NEW!) |
642 | 834 |
|
643 | 835 | Example config files are in `examples/configs/`: |
644 | 836 | - `example.toml` - TOML format example |
@@ -740,6 +932,10 @@ Contributions are welcome! Please feel free to submit a Pull Request. |
740 | 932 | - [x] Number prompt with range validation |
741 | 933 | - [x] Path prompt with autocomplete |
742 | 934 | - [x] Middleware system for commands |
| 935 | +- [x] **Type-safe API** with compile-time validation (NEW!) |
| 936 | + - [x] TypedCommand with auto-generated options from structs |
| 937 | + - [x] TypedConfig with schema validation |
| 938 | + - [x] TypedMiddleware with compile-time field checking |
743 | 939 |
|
744 | 940 | ### Future Enhancements |
745 | 941 | - [ ] Tree rendering for hierarchical data |
|
0 commit comments