Skip to content

Commit 212ca0f

Browse files
committed
chore: wip
1 parent 2dd81c4 commit 212ca0f

20 files changed

Lines changed: 1557 additions & 214 deletions

README.md

Lines changed: 234 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,36 @@
11
# zig-cli
22

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.
422

523
## Features
624

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
930
- **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
1634

1735
### Interactive Prompts
1836
- **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"));
7189
const std = @import("std");
7290
const cli = @import("zig-cli");
7391
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 {
76100
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});
78107
}
79108
80109
pub fn main() !void {
81110
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
82111
defer _ = gpa.deinit();
83112
const allocator = gpa.allocator();
84113
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();
99117
100-
// Set action
101-
_ = app.action(greetAction);
118+
_ = cmd.setAction(greet);
102119
103-
// Parse arguments
120+
// 4. Parse and execute
104121
const args = try std.process.argsAlloc(allocator);
105122
defer std.process.argsFree(allocator, args);
106-
try app.parse(args);
123+
try cli.Parser.init(allocator).parse(cmd.getCommand(), args[1..]);
107124
}
108125
```
109126

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+
110136
### Interactive Prompts
111137

112138
```zig
@@ -260,6 +286,156 @@ fn myAction(ctx: *cli.Command.ParseContext) !void {
260286
}
261287
```
262288

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+
263439
### Prompts
264440

265441
#### Text Prompt
@@ -521,24 +697,39 @@ try prompt.Terminal.init().write(styled);
521697

522698
### Configuration Files
523699

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.
525701

526702
#### Loading Config
527703

528704
```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");
531717
defer config.deinit();
532718
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);
535727
defer config2.deinit();
536-
try config2.loadFromString(content, .toml); // or .jsonc, .json5
537728
538729
// Auto-discover config file
539-
var config3 = try cli.config.discover(allocator, "myapp");
730+
var config3 = try cli.config.discover(AppConfig, allocator, "myapp");
540731
defer config3.deinit();
541-
// Searches for: myapp.toml, myapp.json5, myapp.jsonc, myapp.json
732+
// Searches for: myapp.toml, myapp.json5, myapp.jsonc
542733
// In: ., ./.config, ~/.config/myapp
543734
```
544735

@@ -639,6 +830,7 @@ Check out the `examples/` directory for complete examples:
639830
- `advanced.zig` - Complex CLI with multiple commands and arguments
640831
- `showcase.zig` - Comprehensive feature demonstration including all new prompts
641832
- `config.zig` - Configuration file examples (TOML, JSONC, JSON5)
833+
- **`typed.zig`** - Type-safe API examples (compile-time validated) (NEW!)
642834

643835
Example config files are in `examples/configs/`:
644836
- `example.toml` - TOML format example
@@ -740,6 +932,10 @@ Contributions are welcome! Please feel free to submit a Pull Request.
740932
- [x] Number prompt with range validation
741933
- [x] Path prompt with autocomplete
742934
- [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
743939

744940
### Future Enhancements
745941
- [ ] Tree rendering for hierarchical data

examples/simple.zig

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
const std = @import("std");
2+
const cli = @import("zig-cli");
3+
4+
// Define your CLI options as a struct - that's it!
5+
const GreetOptions = struct {
6+
name: []const u8, // Required
7+
age: ?u16 = null, // Optional
8+
times: u8 = 1, // With default
9+
enthusiastic: bool = false, // Boolean flag
10+
};
11+
12+
// Type-safe action - no string lookups!
13+
fn greet(ctx: *cli.Context(GreetOptions)) !void {
14+
const stdout = std.io.getStdOut().writer();
15+
16+
// Compile-time validated field access
17+
const name = ctx.get(.name);
18+
const age = ctx.get(.age);
19+
const times = ctx.get(.times);
20+
const enthusiastic = ctx.get(.enthusiastic);
21+
22+
const punct: []const u8 = if (enthusiastic) "!" else ".";
23+
24+
var i: u8 = 0;
25+
while (i < times) : (i += 1) {
26+
try stdout.print("Hello, {s}", .{name});
27+
if (age) |a| {
28+
try stdout.print(" (age {d})", .{a});
29+
}
30+
try stdout.print("{s}\n", .{punct});
31+
}
32+
}
33+
34+
pub fn main() !void {
35+
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
36+
defer _ = gpa.deinit();
37+
const allocator = gpa.allocator();
38+
39+
// Create command - options auto-generated from struct!
40+
var cmd = try cli.command(GreetOptions).init(allocator, "greet", "Greet someone");
41+
defer cmd.deinit();
42+
43+
_ = cmd.setAction(greet);
44+
45+
// Parse and execute
46+
const args = try std.process.argsAlloc(allocator);
47+
defer std.process.argsFree(allocator, args);
48+
49+
try cli.Parser.init(allocator).parse(cmd.getCommand(), args[1..]);
50+
}

0 commit comments

Comments
 (0)