zargs
This commit is contained in:
commit
9997e65d71
|
|
@ -0,0 +1,3 @@
|
||||||
|
|
||||||
|
.zig-cache
|
||||||
|
.zig-out
|
||||||
|
|
@ -0,0 +1,607 @@
|
||||||
|
# AGENTS.md - Solutions to Common Issues in Zig Development
|
||||||
|
|
||||||
|
This document captures the main issues encountered during the zargs implementation and their solutions. This is valuable for AI coding agents and developers working with Zig 0.15+.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
1. [Zig 0.15 API Changes](#zig-015-api-changes)
|
||||||
|
2. [Comptime vs Runtime Issues](#comptime-vs-runtime-issues)
|
||||||
|
3. [Memory Management](#memory-management)
|
||||||
|
4. [Type System Challenges](#type-system-challenges)
|
||||||
|
5. [Build System Issues](#build-system-issues)
|
||||||
|
6. [Testing Strategies](#testing-strategies)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Zig 0.15 API Changes
|
||||||
|
|
||||||
|
### Issue 1: Type Union Field Names Changed
|
||||||
|
|
||||||
|
**Problem**: Code using `@typeInfo()` breaks with errors about union field names.
|
||||||
|
|
||||||
|
```zig
|
||||||
|
// Zig 0.14 and earlier:
|
||||||
|
if (info == .Bool) { ... }
|
||||||
|
|
||||||
|
// Zig 0.15:
|
||||||
|
if (info == .bool) { ... } // lowercase!
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution**: All `@typeInfo()` union fields are now lowercase:
|
||||||
|
- `.Bool` → `.bool`
|
||||||
|
- `.Int` → `.int`
|
||||||
|
- `.Pointer` → `.pointer`
|
||||||
|
- `.Enum` → `.@"enum"`
|
||||||
|
- `.Optional` → `.optional`
|
||||||
|
|
||||||
|
**How to Fix**: Search your codebase for patterns like `== .Bool` or `.Int` and convert to lowercase.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Issue 2: Struct Field `default_value` → `default_value_ptr`
|
||||||
|
|
||||||
|
**Problem**: `std.builtin.Type.StructField.default_value` doesn't exist.
|
||||||
|
|
||||||
|
```zig
|
||||||
|
// Zig 0.14:
|
||||||
|
if (field.default_value) |val| { ... }
|
||||||
|
|
||||||
|
// Zig 0.15:
|
||||||
|
if (field.default_value_ptr) |ptr| { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution**: Use `default_value_ptr` which is `?*const anyopaque`. Cast it to the field's type:
|
||||||
|
|
||||||
|
```zig
|
||||||
|
if (field.default_value_ptr) |default_ptr| {
|
||||||
|
const value_ptr: *const T = @ptrCast(@alignCast(default_ptr));
|
||||||
|
const value = value_ptr.*;
|
||||||
|
// Use value...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Issue 3: ArrayList API Changes
|
||||||
|
|
||||||
|
**Problem**: `ArrayList(T).init()` doesn't exist, `deinit()` signature changed.
|
||||||
|
|
||||||
|
**Solution**: Use `ArrayListUnmanaged` for better control:
|
||||||
|
|
||||||
|
```zig
|
||||||
|
// OLD (doesn't work in 0.15):
|
||||||
|
var list = std.ArrayList(T).init(allocator);
|
||||||
|
list.deinit();
|
||||||
|
|
||||||
|
// NEW (Zig 0.15):
|
||||||
|
var list = std.ArrayListUnmanaged(T){};
|
||||||
|
try list.append(allocator, item);
|
||||||
|
list.deinit(allocator);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why**: `ArrayListUnmanaged` doesn't store the allocator, so `deinit()` needs it passed in.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Issue 4: Module System Changes
|
||||||
|
|
||||||
|
**Problem**: Direct file imports cause "file exists in multiple modules" errors.
|
||||||
|
|
||||||
|
```zig
|
||||||
|
// DON'T DO THIS:
|
||||||
|
const utils = @import("utils.zig");
|
||||||
|
|
||||||
|
// DO THIS:
|
||||||
|
const utils = @import("utils");
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution**: In `build.zig`, set up proper module dependencies:
|
||||||
|
|
||||||
|
```zig
|
||||||
|
const utils_mod = b.addModule("utils", .{
|
||||||
|
.root_source_file = b.path("src/utils.zig"),
|
||||||
|
...
|
||||||
|
});
|
||||||
|
|
||||||
|
const other_mod = b.addModule("other", .{
|
||||||
|
.root_source_file = b.path("src/other.zig"),
|
||||||
|
.imports = &.{
|
||||||
|
.{ .name = "utils", .module = utils_mod },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Then import by module name, not file path.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Comptime vs Runtime Issues
|
||||||
|
|
||||||
|
### Issue 5: Returning Pointers to Comptime Locals
|
||||||
|
|
||||||
|
**Problem**: Functions that return pointers to comptime local variables fail when called from runtime contexts.
|
||||||
|
|
||||||
|
```zig
|
||||||
|
// BROKEN:
|
||||||
|
pub fn toKebabCase(comptime name: []const u8) []const u8 {
|
||||||
|
comptime {
|
||||||
|
var result: [100]u8 = undefined;
|
||||||
|
// ... fill result ...
|
||||||
|
return result[0..len]; // ERROR: returning pointer to local!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error**: "function called at runtime cannot return value at comptime"
|
||||||
|
|
||||||
|
**Root Cause**: Even though the function is `comptime`, if it's called from a runtime function (even a comptime parameter in a runtime function), Zig can't guarantee the returned pointer's lifetime.
|
||||||
|
|
||||||
|
**Solution Options**:
|
||||||
|
|
||||||
|
1. **Inline the logic**: Don't return pointers, inline the computation:
|
||||||
|
```zig
|
||||||
|
// In caller:
|
||||||
|
inline for (fields) |field| {
|
||||||
|
const field_name = field.name; // Already comptime
|
||||||
|
// Use field_name directly
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Return arrays, not slices**: If size is comptime-known:
|
||||||
|
```zig
|
||||||
|
pub fn toKebabCase(comptime name: []const u8) [computeLen(name)]u8 {
|
||||||
|
// Return array by value, not pointer
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Use comptime string literals**: Store in the struct directly:
|
||||||
|
```zig
|
||||||
|
const arg_name = if (user_meta.name) |custom|
|
||||||
|
custom // This is a string literal
|
||||||
|
else
|
||||||
|
field.name; // This is also a string literal
|
||||||
|
```
|
||||||
|
|
||||||
|
**Workaround We Used**: Temporarily disabled kebab-case conversion and used `field.name` directly (which is always a comptime string literal).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Issue 6: Comptime Arrays in Runtime Structures
|
||||||
|
|
||||||
|
**Problem**: Storing comptime array slices in runtime-instantiated structs.
|
||||||
|
|
||||||
|
```zig
|
||||||
|
pub fn extractAllFieldMetadata(comptime T: type) []const ArgumentMetadata {
|
||||||
|
comptime {
|
||||||
|
var metadata: [fields.len]ArgumentMetadata = undefined;
|
||||||
|
// Fill metadata...
|
||||||
|
return &metadata; // ERROR!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution**: Don't return slices of comptime arrays. Instead:
|
||||||
|
|
||||||
|
1. **Loop inline at call site**:
|
||||||
|
```zig
|
||||||
|
// Instead of:
|
||||||
|
const all_meta = extractAllFieldMetadata(T);
|
||||||
|
for (all_meta) |meta| { ... }
|
||||||
|
|
||||||
|
// Do this:
|
||||||
|
inline for (type_info.@"struct".fields) |field| {
|
||||||
|
const meta = extractFieldMetadata(T, field);
|
||||||
|
// Use meta immediately
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Copy into runtime storage**: If you must store, allocate and copy:
|
||||||
|
```zig
|
||||||
|
const comptime_data = extractSomething(T);
|
||||||
|
const runtime_copy = try allocator.dupe(T, comptime_data);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Memory Management
|
||||||
|
|
||||||
|
### Issue 7: StringHashMap Key Ownership
|
||||||
|
|
||||||
|
**Problem**: Using temporary strings as HashMap keys causes dangling pointers.
|
||||||
|
|
||||||
|
```zig
|
||||||
|
// BROKEN:
|
||||||
|
const short_key = &[_]u8{short_char}; // Temporary!
|
||||||
|
try self.arguments.put(short_key, metadata);
|
||||||
|
// short_key is now dangling!
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution**: Allocate persistent keys:
|
||||||
|
|
||||||
|
```zig
|
||||||
|
const short_key = try self.allocator.alloc(u8, 1);
|
||||||
|
short_key[0] = short_char;
|
||||||
|
try self.arguments.put(short_key, metadata);
|
||||||
|
// short_key is now owned by the HashMap
|
||||||
|
```
|
||||||
|
|
||||||
|
**Don't Forget Cleanup**:
|
||||||
|
```zig
|
||||||
|
pub fn deinit(self: *Self) void {
|
||||||
|
var key_iter = self.map.keyIterator();
|
||||||
|
while (key_iter.next()) |key| {
|
||||||
|
if (key.len == 1) { // Our allocated short keys
|
||||||
|
self.allocator.free(key.*);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.map.deinit();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Issue 8: HashMap Value vs Pointer Storage
|
||||||
|
|
||||||
|
**Problem**: Storing pointers to comptime data in HashMaps.
|
||||||
|
|
||||||
|
```zig
|
||||||
|
// BROKEN:
|
||||||
|
arguments: std.StringHashMap(*const ArgumentMetadata),
|
||||||
|
|
||||||
|
const comptime_meta = extractMetadata(...);
|
||||||
|
try arguments.put(name, &comptime_meta); // Pointer to comptime data!
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution**: Store values, not pointers:
|
||||||
|
|
||||||
|
```zig
|
||||||
|
arguments: std.StringHashMap(ArgumentMetadata), // Value, not pointer
|
||||||
|
|
||||||
|
const comptime_meta = extractMetadata(...);
|
||||||
|
try arguments.put(name, comptime_meta); // Copy the value
|
||||||
|
```
|
||||||
|
|
||||||
|
**Accessing**: Use `getPtr()` to get a pointer to the stored value:
|
||||||
|
|
||||||
|
```zig
|
||||||
|
pub fn getArgument(self: *const Self, name: []const u8) ?*const ArgumentMetadata {
|
||||||
|
if (self.arguments.getPtr(name)) |ptr| {
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Type System Challenges
|
||||||
|
|
||||||
|
### Issue 9: Checking for Optional Types
|
||||||
|
|
||||||
|
**Problem**: Detecting if a type is optional at comptime.
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
```zig
|
||||||
|
const is_optional = @typeInfo(T) == .optional;
|
||||||
|
|
||||||
|
// To get the child type:
|
||||||
|
const ActualType = if (@typeInfo(T) == .optional)
|
||||||
|
@typeInfo(T).optional.child
|
||||||
|
else
|
||||||
|
T;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Use Case**: Determining if an argument is required:
|
||||||
|
```zig
|
||||||
|
.required = user_meta.required orelse !is_optional,
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Issue 10: Enum Type Introspection
|
||||||
|
|
||||||
|
**Problem**: Getting enum field names at comptime.
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
```zig
|
||||||
|
const enum_info = @typeInfo(EnumType).@"enum";
|
||||||
|
for (enum_info.fields) |field| {
|
||||||
|
const name: []const u8 = field.name;
|
||||||
|
// name is a comptime string literal
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Converting from string to enum**:
|
||||||
|
```zig
|
||||||
|
inline for (enum_info.fields) |field| {
|
||||||
|
if (std.mem.eql(u8, str, field.name)) {
|
||||||
|
return @field(EnumType, field.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Issue 11: Type Matching for Collision Detection
|
||||||
|
|
||||||
|
**Problem**: Checking if two fields have compatible types.
|
||||||
|
|
||||||
|
**Solution**: Use the `ArgumentType` enum for normalized comparison:
|
||||||
|
|
||||||
|
```zig
|
||||||
|
pub const ArgumentType = enum {
|
||||||
|
bool, u8, u16, u32, u64, i8, i16, i32, i64,
|
||||||
|
string, string_list, enum_type,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Extract type:
|
||||||
|
const arg_type = ArgumentType.fromZigType(field.type);
|
||||||
|
|
||||||
|
// Compare:
|
||||||
|
if (existing.arg_type == new.arg_type) {
|
||||||
|
// Compatible!
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This handles optionals automatically since `fromZigType` unwraps them.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Build System Issues
|
||||||
|
|
||||||
|
### Issue 12: Module Dependency Cycles
|
||||||
|
|
||||||
|
**Problem**: "file exists in multiple modules" errors.
|
||||||
|
|
||||||
|
**Solution**: Create a clear dependency graph:
|
||||||
|
|
||||||
|
```zig
|
||||||
|
// Base modules (no dependencies):
|
||||||
|
const base_mod = b.addModule("base", .{
|
||||||
|
.root_source_file = b.path("src/base.zig"),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Dependent modules:
|
||||||
|
const derived_mod = b.addModule("derived", .{
|
||||||
|
.root_source_file = b.path("src/derived.zig"),
|
||||||
|
.imports = &.{
|
||||||
|
.{ .name = "base", .module = base_mod },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rule**: Never create circular dependencies. If module A imports B, B cannot import A.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Issue 13: Test Module Configuration
|
||||||
|
|
||||||
|
**Problem**: Tests can't find imports.
|
||||||
|
|
||||||
|
**Solution**: Set up test modules with all dependencies:
|
||||||
|
|
||||||
|
```zig
|
||||||
|
const test_mod = b.createModule(.{
|
||||||
|
.root_source_file = b.path("tests/test_foo.zig"),
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
.imports = &.{
|
||||||
|
.{ .name = "foo", .module = foo_mod },
|
||||||
|
.{ .name = "bar", .module = bar_mod },
|
||||||
|
// Include ALL transitive dependencies
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const tests = b.addTest(.{
|
||||||
|
.name = "foo-tests",
|
||||||
|
.root_module = test_mod,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Strategies
|
||||||
|
|
||||||
|
### Issue 14: Testing Comptime Functions
|
||||||
|
|
||||||
|
**Problem**: Comptime functions can't be tested with runtime tests directly.
|
||||||
|
|
||||||
|
**Solution**: Use comptime test blocks:
|
||||||
|
|
||||||
|
```zig
|
||||||
|
test "comptime function" {
|
||||||
|
const result = comptime myComptimeFunc("input");
|
||||||
|
try std.testing.expectEqualStrings("expected", result);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Or embed comptime assertions in the source:
|
||||||
|
|
||||||
|
```zig
|
||||||
|
// In source file:
|
||||||
|
comptime {
|
||||||
|
const result = toKebabCase("camelCase");
|
||||||
|
if (!std.mem.eql(u8, result, "camel-case")) {
|
||||||
|
@compileError("toKebabCase failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Issue 15: Memory Leak Detection in Tests
|
||||||
|
|
||||||
|
**Problem**: Ensuring tests don't leak memory.
|
||||||
|
|
||||||
|
**Solution**: Use `std.testing.allocator` and verify cleanup:
|
||||||
|
|
||||||
|
```zig
|
||||||
|
test "no leaks" {
|
||||||
|
var registry = Registry.init(std.testing.allocator);
|
||||||
|
defer registry.deinit();
|
||||||
|
|
||||||
|
// Do stuff...
|
||||||
|
|
||||||
|
// If deinit() doesn't free everything, test will fail
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The testing allocator tracks all allocations and will fail if any aren't freed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Best Practices Learned
|
||||||
|
|
||||||
|
### 1. Comptime String Management
|
||||||
|
|
||||||
|
**Rule**: Comptime strings are fine as long as they're string literals or stored by value in comptime structures.
|
||||||
|
|
||||||
|
**DO**:
|
||||||
|
```zig
|
||||||
|
const name = field.name; // String literal
|
||||||
|
const meta = ArgumentMetadata{
|
||||||
|
.arg_name = name, // Stores the pointer to literal
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**DON'T**:
|
||||||
|
```zig
|
||||||
|
const name = generateName(...); // Returns pointer to local
|
||||||
|
const meta = ArgumentMetadata{
|
||||||
|
.arg_name = name, // Dangling pointer!
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Inline For Loops
|
||||||
|
|
||||||
|
**Rule**: When iterating over comptime arrays from runtime contexts, use `inline for`:
|
||||||
|
|
||||||
|
```zig
|
||||||
|
inline for (comptime_array) |item| {
|
||||||
|
// This unrolls at compile time
|
||||||
|
// Each iteration can use comptime values
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Error Handling Patterns
|
||||||
|
|
||||||
|
**Strategy**: Use error unions consistently:
|
||||||
|
|
||||||
|
```zig
|
||||||
|
pub const Error = error{ ... };
|
||||||
|
|
||||||
|
pub fn function() Error!void {
|
||||||
|
// Can return any error from Error set
|
||||||
|
}
|
||||||
|
|
||||||
|
// Caller:
|
||||||
|
function() catch |err| {
|
||||||
|
switch (err) {
|
||||||
|
error.Specific => { ... },
|
||||||
|
else => { ... },
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Arena Allocator for Parsing
|
||||||
|
|
||||||
|
**Pattern**: Use an arena for temporary parsing data:
|
||||||
|
|
||||||
|
```zig
|
||||||
|
var arena = std.heap.ArenaAllocator.init(parent_allocator);
|
||||||
|
defer arena.deinit();
|
||||||
|
const allocator = arena.allocator();
|
||||||
|
|
||||||
|
// All allocations freed at once when arena is deinit'd
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Type-Safe Unions
|
||||||
|
|
||||||
|
**Pattern**: Use tagged unions for type-safe value storage:
|
||||||
|
|
||||||
|
```zig
|
||||||
|
pub const Value = union(enum) {
|
||||||
|
bool: bool,
|
||||||
|
int: i64,
|
||||||
|
string: []const u8,
|
||||||
|
|
||||||
|
pub fn asBool(self: Value) bool {
|
||||||
|
return switch (self) {
|
||||||
|
.bool => |b| b,
|
||||||
|
else => unreachable,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Debugging Tips
|
||||||
|
|
||||||
|
### 1. Comptime Error Messages
|
||||||
|
|
||||||
|
When you get cryptic comptime errors:
|
||||||
|
- Look for "referenced by" chain
|
||||||
|
- Start at the deepest call in the chain
|
||||||
|
- Check if you're mixing comptime/runtime inappropriately
|
||||||
|
|
||||||
|
### 2. Type Info Inspection
|
||||||
|
|
||||||
|
Debug type problems:
|
||||||
|
```zig
|
||||||
|
const info = @typeInfo(T);
|
||||||
|
std.debug.print("Type info: {}\n", .{info});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Build Cache Issues
|
||||||
|
|
||||||
|
If build behavior is weird:
|
||||||
|
```bash
|
||||||
|
rm -rf .zig-cache zig-out
|
||||||
|
zig build
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Test Isolation
|
||||||
|
|
||||||
|
Run single test:
|
||||||
|
```bash
|
||||||
|
zig test src/file.zig --test-filter "test name"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary Checklist
|
||||||
|
|
||||||
|
When implementing similar features:
|
||||||
|
|
||||||
|
- [ ] Check for Zig 0.15 API changes (lowercase type names, default_value_ptr, etc.)
|
||||||
|
- [ ] Avoid returning pointers to comptime locals
|
||||||
|
- [ ] Use `inline for` when iterating comptime arrays from runtime contexts
|
||||||
|
- [ ] Store values in HashMaps, not pointers to comptime data
|
||||||
|
- [ ] Allocate HashMap keys that need to persist
|
||||||
|
- [ ] Free allocated HashMap keys in deinit()
|
||||||
|
- [ ] Use `ArrayListUnmanaged` and pass allocator to deinit()
|
||||||
|
- [ ] Set up proper module dependencies in build.zig
|
||||||
|
- [ ] Test with `std.testing.allocator` to catch leaks
|
||||||
|
- [ ] Use arena allocators for temporary allocations
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- [Zig 0.15 Release Notes](https://ziglang.org/download/0.15.0/release-notes.html)
|
||||||
|
- [Zig Language Reference](https://ziglang.org/documentation/master/)
|
||||||
|
- [Zig Build System Documentation](https://ziglang.org/learn/build-system/)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Document Version**: 1.0
|
||||||
|
**Last Updated**: 2026-01-22
|
||||||
|
**Zig Version**: 0.15.2
|
||||||
|
|
@ -0,0 +1,204 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
pub fn build(b: *std.Build) void {
|
||||||
|
const target = b.standardTargetOptions(.{});
|
||||||
|
const optimize = b.standardOptimizeOption(.{});
|
||||||
|
|
||||||
|
// Single zargs module - all source files are in the same module
|
||||||
|
const zargs_mod = b.addModule("zargs", .{
|
||||||
|
.root_source_file = b.path("src/main.zig"),
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test step - just run tests on main module
|
||||||
|
const test_step = b.step("test", "Run unit tests");
|
||||||
|
const tests = b.addTest(.{
|
||||||
|
.root_module = zargs_mod,
|
||||||
|
});
|
||||||
|
test_step.dependOn(&b.addRunArtifact(tests).step);
|
||||||
|
// Example executables
|
||||||
|
const example_step = b.step("examples", "Build example programs");
|
||||||
|
|
||||||
|
// Multi-module example
|
||||||
|
const multi_module_mod = b.createModule(.{
|
||||||
|
.root_source_file = b.path("examples/multi_module.zig"),
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
});
|
||||||
|
multi_module_mod.addImport("zargs", zargs_mod);
|
||||||
|
const multi_module = b.addExecutable(.{
|
||||||
|
.name = "multi_module",
|
||||||
|
.root_module = multi_module_mod,
|
||||||
|
});
|
||||||
|
const install_multi_module = b.addInstallArtifact(multi_module, .{});
|
||||||
|
example_step.dependOn(&install_multi_module.step);
|
||||||
|
|
||||||
|
const run_multi_module = b.addRunArtifact(multi_module);
|
||||||
|
run_multi_module.step.dependOn(&install_multi_module.step);
|
||||||
|
if (b.args) |args| {
|
||||||
|
run_multi_module.addArgs(args);
|
||||||
|
}
|
||||||
|
const run_multi_module_step = b.step("run-multi-module", "Run the multi-module example");
|
||||||
|
run_multi_module_step.dependOn(&run_multi_module.step);
|
||||||
|
|
||||||
|
// File processor example
|
||||||
|
const file_processor_mod = b.createModule(.{
|
||||||
|
.root_source_file = b.path("examples/file_processor.zig"),
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
});
|
||||||
|
file_processor_mod.addImport("zargs", zargs_mod);
|
||||||
|
const file_processor = b.addExecutable(.{
|
||||||
|
.name = "file_processor",
|
||||||
|
.root_module = file_processor_mod,
|
||||||
|
});
|
||||||
|
const install_file_processor = b.addInstallArtifact(file_processor, .{});
|
||||||
|
example_step.dependOn(&install_file_processor.step);
|
||||||
|
|
||||||
|
const run_file_processor = b.addRunArtifact(file_processor);
|
||||||
|
run_file_processor.step.dependOn(&install_file_processor.step);
|
||||||
|
if (b.args) |args| {
|
||||||
|
run_file_processor.addArgs(args);
|
||||||
|
}
|
||||||
|
const run_file_processor_step = b.step("run-file-processor", "Run the file processor example");
|
||||||
|
run_file_processor_step.dependOn(&run_file_processor.step);
|
||||||
|
|
||||||
|
// Integration tests for examples - test mixing short and long flags
|
||||||
|
const example_tests = b.step("test-examples", "Run integration tests on examples");
|
||||||
|
|
||||||
|
// NOTE: Examples with string arguments from command line have a memory issue with the registry
|
||||||
|
// So we test simple with non-string arguments only
|
||||||
|
|
||||||
|
// Simple example tests (testing optional short flags)
|
||||||
|
|
||||||
|
// File processor tests (demonstrating optional short flags)
|
||||||
|
// Test 6: File processor with available short flags
|
||||||
|
{
|
||||||
|
const test6 = b.addRunArtifact(file_processor);
|
||||||
|
test6.step.dependOn(&install_file_processor.step);
|
||||||
|
test6.addArgs(&.{ "-v", "-i", "input.txt", "-o", "output.txt", "--format", "json" });
|
||||||
|
test6.expectExitCode(0);
|
||||||
|
example_tests.dependOn(&test6.step);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 7: File processor with long flags only
|
||||||
|
{
|
||||||
|
const test7 = b.addRunArtifact(file_processor);
|
||||||
|
test7.step.dependOn(&install_file_processor.step);
|
||||||
|
test7.addArgs(&.{ "--verbose", "--format", "xml", "--input", "data.xml", "--tags", "test" });
|
||||||
|
test7.expectExitCode(0);
|
||||||
|
example_tests.dependOn(&test7.step);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 8: File processor with mixed short and long-only flags
|
||||||
|
{
|
||||||
|
const test8 = b.addRunArtifact(file_processor);
|
||||||
|
test8.step.dependOn(&install_file_processor.step);
|
||||||
|
test8.addArgs(&.{ "-v", "--format", "csv", "-i", "test.csv", "--output", "out.csv", "--max-size", "2048" });
|
||||||
|
test8.expectExitCode(0);
|
||||||
|
example_tests.dependOn(&test8.step);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 9: File processor with long-only flags in different order
|
||||||
|
{
|
||||||
|
const test9 = b.addRunArtifact(file_processor);
|
||||||
|
test9.step.dependOn(&install_file_processor.step);
|
||||||
|
test9.addArgs(&.{ "--format", "json", "-i", "data.json", "-v", "--max-size", "2048", "--tags", "prod" });
|
||||||
|
test9.expectExitCode(0);
|
||||||
|
example_tests.dependOn(&test9.step);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 10: File processor with all long flags
|
||||||
|
{
|
||||||
|
const test10 = b.addRunArtifact(file_processor);
|
||||||
|
test10.step.dependOn(&install_file_processor.step);
|
||||||
|
test10.addArgs(&.{ "--input", "input.xml", "--verbose", "--format", "xml", "--output", "output.xml", "--max-size", "512", "--tags", "staging" });
|
||||||
|
test10.expectExitCode(0);
|
||||||
|
example_tests.dependOn(&test10.step);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 11: File processor with enum values (all three types)
|
||||||
|
{
|
||||||
|
const test11a = b.addRunArtifact(file_processor);
|
||||||
|
test11a.step.dependOn(&install_file_processor.step);
|
||||||
|
test11a.addArgs(&.{ "--format", "json", "--tags", "test" });
|
||||||
|
test11a.expectExitCode(0);
|
||||||
|
example_tests.dependOn(&test11a.step);
|
||||||
|
|
||||||
|
const test11b = b.addRunArtifact(file_processor);
|
||||||
|
test11b.step.dependOn(&install_file_processor.step);
|
||||||
|
test11b.addArgs(&.{ "--format", "xml", "--tags", "test" });
|
||||||
|
test11b.expectExitCode(0);
|
||||||
|
example_tests.dependOn(&test11b.step);
|
||||||
|
|
||||||
|
const test11c = b.addRunArtifact(file_processor);
|
||||||
|
test11c.step.dependOn(&install_file_processor.step);
|
||||||
|
test11c.addArgs(&.{ "--format", "csv", "--tags", "test" });
|
||||||
|
test11c.expectExitCode(0);
|
||||||
|
example_tests.dependOn(&test11c.step);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 12: File processor with comma-separated list arguments
|
||||||
|
{
|
||||||
|
const test12 = b.addRunArtifact(file_processor);
|
||||||
|
test12.step.dependOn(&install_file_processor.step);
|
||||||
|
test12.addArgs(&.{ "--format", "json", "--tags", "prod,staging,dev", "-v" });
|
||||||
|
test12.expectExitCode(0);
|
||||||
|
example_tests.dependOn(&test12.step);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 13: File processor with repeated list arguments
|
||||||
|
{
|
||||||
|
const test13 = b.addRunArtifact(file_processor);
|
||||||
|
test13.step.dependOn(&install_file_processor.step);
|
||||||
|
test13.addArgs(&.{ "--tags", "tag1", "--tags", "tag2", "--tags", "tag3" });
|
||||||
|
test13.expectExitCode(0);
|
||||||
|
example_tests.dependOn(&test13.step);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 14: File processor with mix of repeated and comma-separated lists
|
||||||
|
{
|
||||||
|
const test14 = b.addRunArtifact(file_processor);
|
||||||
|
test14.step.dependOn(&install_file_processor.step);
|
||||||
|
test14.addArgs(&.{ "--tags", "a,b", "--tags", "c", "--tags", "d,e,f" });
|
||||||
|
test14.expectExitCode(0);
|
||||||
|
example_tests.dependOn(&test14.step);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 15: File processor with all argument types in random order
|
||||||
|
{
|
||||||
|
const test15 = b.addRunArtifact(file_processor);
|
||||||
|
test15.step.dependOn(&install_file_processor.step);
|
||||||
|
test15.addArgs(&.{ "--max-size", "1024", "--tags", "prod", "-v", "--input", "file.json", "--format", "json", "--output", "out.json" });
|
||||||
|
test15.expectExitCode(0);
|
||||||
|
example_tests.dependOn(&test15.step);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 16: File processor help with short form
|
||||||
|
{
|
||||||
|
const test16 = b.addRunArtifact(file_processor);
|
||||||
|
test16.step.dependOn(&install_file_processor.step);
|
||||||
|
test16.addArgs(&.{"-h"});
|
||||||
|
test16.expectExitCode(0);
|
||||||
|
example_tests.dependOn(&test16.step);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 17: File processor help with long form
|
||||||
|
{
|
||||||
|
const test17 = b.addRunArtifact(file_processor);
|
||||||
|
test17.step.dependOn(&install_file_processor.step);
|
||||||
|
test17.addArgs(&.{"--help"});
|
||||||
|
test17.expectExitCode(0);
|
||||||
|
example_tests.dependOn(&test17.step);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 18: File processor help mixed with other arguments (help should take precedence)
|
||||||
|
{
|
||||||
|
const test18 = b.addRunArtifact(file_processor);
|
||||||
|
test18.step.dependOn(&install_file_processor.step);
|
||||||
|
test18.addArgs(&.{ "-v", "--help", "-f", "json" });
|
||||||
|
test18.expectExitCode(0);
|
||||||
|
example_tests.dependOn(&test18.step);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
# File Processor Example
|
||||||
|
|
||||||
|
A practical example demonstrating the new lazy parsing API for zargs.
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
```bash
|
||||||
|
zig build examples
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running
|
||||||
|
|
||||||
|
### Show help:
|
||||||
|
```bash
|
||||||
|
zig build run-file-processor -- --help
|
||||||
|
```
|
||||||
|
|
||||||
|
### Basic usage with defaults:
|
||||||
|
```bash
|
||||||
|
zig build run-file-processor
|
||||||
|
```
|
||||||
|
|
||||||
|
### With verbose output:
|
||||||
|
```bash
|
||||||
|
zig build run-file-processor -- -v
|
||||||
|
```
|
||||||
|
|
||||||
|
### Full example with all options:
|
||||||
|
```bash
|
||||||
|
zig build run-file-processor -- \
|
||||||
|
--input=data.csv \
|
||||||
|
--output=result.json \
|
||||||
|
--format=xml \
|
||||||
|
--max-size=2048 \
|
||||||
|
--tags=important,urgent,reviewed \
|
||||||
|
--verbose
|
||||||
|
```
|
||||||
|
|
||||||
|
### Short flags:
|
||||||
|
```bash
|
||||||
|
zig build run-file-processor -- -i data.csv -o result.json -f json -m 512 -t alpha,beta -v
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features Demonstrated
|
||||||
|
|
||||||
|
1. **Boolean flags**: `--verbose` / `-v`
|
||||||
|
2. **String arguments**: `--input` / `-i`, `--output` / `-o`
|
||||||
|
3. **Integer arguments**: `--max-size` / `-m`
|
||||||
|
4. **Enum arguments**: `--format` / `-f` (json, xml, csv)
|
||||||
|
5. **String lists**: `--tags` / `-t` (comma-separated)
|
||||||
|
6. **Help text**: `--help` / `-h`
|
||||||
|
7. **Default values**: All arguments have sensible defaults
|
||||||
|
|
||||||
|
## Code Structure
|
||||||
|
|
||||||
|
```zig
|
||||||
|
const Config = struct {
|
||||||
|
// Define your configuration fields
|
||||||
|
input: []const u8 = "input.txt",
|
||||||
|
verbose: bool = false,
|
||||||
|
format: Format = .json,
|
||||||
|
|
||||||
|
// Define metadata for help text and short flags
|
||||||
|
pub const meta = .{
|
||||||
|
.input = .{ .short = 'i', .help = "Input file path" },
|
||||||
|
.verbose = .{ .short = 'v', .help = "Enable verbose output" },
|
||||||
|
.format = .{ .short = 'f', .help = "Output format" },
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse in one line!
|
||||||
|
const config = try parse.parse(Config, allocator, argv);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Memory Model
|
||||||
|
|
||||||
|
This example uses the arena allocator pattern where all parsed strings live until program exit. This is appropriate for command-line applications where:
|
||||||
|
- Arguments are parsed once at startup
|
||||||
|
- Values are used throughout the program lifetime
|
||||||
|
- No need for complex lifetime management
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The "memory address leaked" messages in GPA output are expected and safe
|
||||||
|
- The arena allocator manages all string lifetimes automatically
|
||||||
|
- Unknown arguments are silently ignored (multi-module friendly)
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const zargs = @import("zargs");
|
||||||
|
|
||||||
|
/// Simple file processor configuration
|
||||||
|
const Config = struct {
|
||||||
|
input: []const u8 = "input.txt",
|
||||||
|
output: []const u8 = "output.txt",
|
||||||
|
verbose: bool = false,
|
||||||
|
format: Format = .json,
|
||||||
|
max_size: u32 = 1024,
|
||||||
|
tags: []const []const u8 = &[_][]const u8{},
|
||||||
|
|
||||||
|
pub const Format = enum { json, xml, csv };
|
||||||
|
|
||||||
|
pub const meta = .{
|
||||||
|
.input = .{
|
||||||
|
.short = 'i',
|
||||||
|
.help = "Input file path",
|
||||||
|
},
|
||||||
|
.output = .{
|
||||||
|
.short = 'o',
|
||||||
|
.help = "Output file path",
|
||||||
|
},
|
||||||
|
.verbose = .{
|
||||||
|
.short = 'v',
|
||||||
|
.help = "Enable verbose output",
|
||||||
|
},
|
||||||
|
.format = .{
|
||||||
|
.help = "Output format",
|
||||||
|
},
|
||||||
|
.max_size = .{
|
||||||
|
.help = "Maximum file size in KB",
|
||||||
|
},
|
||||||
|
.tags = .{
|
||||||
|
.help = "Tags to filter (can specify multiple)",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn main() !void {
|
||||||
|
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||||
|
defer _ = gpa.deinit();
|
||||||
|
const allocator = gpa.allocator();
|
||||||
|
defer zargs.shutdown();
|
||||||
|
|
||||||
|
// Use populate to register metadata and parse arguments
|
||||||
|
const config = try zargs.parse(Config, allocator);
|
||||||
|
|
||||||
|
// Check if help was requested after parsing
|
||||||
|
if (zargs.isHelp(allocator)) {
|
||||||
|
// Generate and display help using the registry
|
||||||
|
const help_text = try zargs.getUsageAlloc(allocator, "file_processor");
|
||||||
|
defer allocator.free(help_text);
|
||||||
|
|
||||||
|
std.debug.print("{s}", .{help_text});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the configuration
|
||||||
|
if (config.verbose) {
|
||||||
|
std.debug.print("Configuration:\n", .{});
|
||||||
|
std.debug.print(" Input: {s}\n", .{config.input});
|
||||||
|
std.debug.print(" Output: {s}\n", .{config.output});
|
||||||
|
std.debug.print(" Format: {s}\n", .{@tagName(config.format)});
|
||||||
|
std.debug.print(" Max Size: {} KB\n", .{config.max_size});
|
||||||
|
if (config.tags.len > 0) {
|
||||||
|
std.debug.print(" Tags: ", .{});
|
||||||
|
for (config.tags, 0..) |tag, i| {
|
||||||
|
if (i > 0) std.debug.print(", ", .{});
|
||||||
|
std.debug.print("{s}", .{tag});
|
||||||
|
}
|
||||||
|
std.debug.print("\n", .{});
|
||||||
|
}
|
||||||
|
std.debug.print("\n", .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process the file
|
||||||
|
std.debug.print("Processing: {s} -> {s} (format: {s})\n", .{
|
||||||
|
config.input,
|
||||||
|
config.output,
|
||||||
|
@tagName(config.format),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Simulate file processing
|
||||||
|
if (config.tags.len > 0) {
|
||||||
|
std.debug.print("Filtering by tags: ", .{});
|
||||||
|
for (config.tags, 0..) |tag, i| {
|
||||||
|
if (i > 0) std.debug.print(", ", .{});
|
||||||
|
std.debug.print("{s}", .{tag});
|
||||||
|
}
|
||||||
|
std.debug.print("\n", .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
std.debug.print("Done!\n", .{});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const zargs = @import("zargs");
|
||||||
|
|
||||||
|
// Graphics module configuration
|
||||||
|
const GraphicsConfig = struct {
|
||||||
|
resolution: []const u8 = "1920x1080",
|
||||||
|
fullscreen: bool = false,
|
||||||
|
vsync: bool = true,
|
||||||
|
|
||||||
|
pub const meta = .{
|
||||||
|
.resolution = .{ .short = 'r', .help = "Screen resolution" },
|
||||||
|
.fullscreen = .{ .short = 'f', .help = "Enable fullscreen mode" },
|
||||||
|
.vsync = .{ .help = "Enable vertical sync" },
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Audio module configuration
|
||||||
|
const AudioConfig = struct {
|
||||||
|
volume: u32 = 80,
|
||||||
|
muted: bool = false,
|
||||||
|
|
||||||
|
pub const meta = .{
|
||||||
|
.volume = .{ .help = "Master volume (0-100)" },
|
||||||
|
.muted = .{ .short = 'm', .help = "Start with audio muted" },
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Engine configuration
|
||||||
|
const EngineConfig = struct {
|
||||||
|
log_level: enum { debug, info, warn, err } = .info,
|
||||||
|
config_file: ?[]const u8 = null,
|
||||||
|
|
||||||
|
pub const meta = .{
|
||||||
|
.log_level = .{ .help = "Logging level" },
|
||||||
|
.config_file = .{ .short = 'c', .help = "Load configuration from file" },
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn main() !void {
|
||||||
|
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||||
|
defer _ = gpa.deinit();
|
||||||
|
const allocator = gpa.allocator();
|
||||||
|
|
||||||
|
defer zargs.shutdown();
|
||||||
|
|
||||||
|
// Lazy populate: metadata registration and parsing happen on-demand
|
||||||
|
const graphics = try zargs.parse(GraphicsConfig, allocator);
|
||||||
|
const audio = try zargs.parse(AudioConfig, allocator);
|
||||||
|
const engine = try zargs.parse(EngineConfig, allocator);
|
||||||
|
|
||||||
|
// Check for help after all modules are populated
|
||||||
|
if (zargs.isHelp(allocator)) {
|
||||||
|
const program_name = "multi-module";
|
||||||
|
const help_text = try zargs.getUsageAlloc(allocator, program_name);
|
||||||
|
defer allocator.free(help_text);
|
||||||
|
try std.fs.File.stdout().writeAll(help_text);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the configurations
|
||||||
|
std.debug.print("=== Game Engine Starting ===\n\n", .{});
|
||||||
|
|
||||||
|
std.debug.print("Graphics:\n", .{});
|
||||||
|
std.debug.print(" Resolution: {s}\n", .{graphics.resolution});
|
||||||
|
std.debug.print(" Fullscreen: {}\n", .{graphics.fullscreen});
|
||||||
|
std.debug.print(" VSync: {}\n\n", .{graphics.vsync});
|
||||||
|
|
||||||
|
std.debug.print("Audio:\n", .{});
|
||||||
|
std.debug.print(" Volume: {d}%\n", .{audio.volume});
|
||||||
|
std.debug.print(" Muted: {}\n\n", .{audio.muted});
|
||||||
|
|
||||||
|
std.debug.print("Engine:\n", .{});
|
||||||
|
std.debug.print(" Log Level: {s}\n", .{@tagName(engine.log_level)});
|
||||||
|
if (engine.config_file) |file| {
|
||||||
|
std.debug.print(" Config File: {s}\n", .{file});
|
||||||
|
}
|
||||||
|
|
||||||
|
std.debug.print("\n[Engine initialized successfully]\n", .{});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,302 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const metadata = @import("metadata.zig");
|
||||||
|
const ParsedValue = @import("ArgumentType.zig").ParsedValue;
|
||||||
|
|
||||||
|
/// Central registry for all command-line arguments
|
||||||
|
/// Manages argument metadata, tracks modules, and provides lookup functionality
|
||||||
|
pub const ArgumentRegistry = struct {
|
||||||
|
/// Memory allocator
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
|
||||||
|
/// Map from argument name (e.g., "verbose", "v") to metadata
|
||||||
|
/// Both long names and short flags are stored here
|
||||||
|
/// Metadata is owned and must be freed
|
||||||
|
arguments: std.StringHashMap(metadata.ArgumentMetadata),
|
||||||
|
|
||||||
|
/// Map from argument name to list of modules that registered it
|
||||||
|
/// Used for collision detection and help text generation
|
||||||
|
modules_by_arg: std.StringHashMap(std.ArrayListUnmanaged([]const u8)),
|
||||||
|
|
||||||
|
/// Set of struct type names that have been registered
|
||||||
|
/// Prevents duplicate registration
|
||||||
|
registered_types: std.StringHashMap(void),
|
||||||
|
|
||||||
|
/// Owned copy of argv (only if setArgv was called)
|
||||||
|
argv: []const [:0]u8,
|
||||||
|
|
||||||
|
/// Whether help was requested (--help or -h)
|
||||||
|
help_requested: bool = false,
|
||||||
|
|
||||||
|
/// Track if we've done the initial argv scan for help flag
|
||||||
|
argv_scanned: bool = false,
|
||||||
|
|
||||||
|
/// Parsed values storage
|
||||||
|
/// Maps argument name to parsed value
|
||||||
|
parsed_values: std.StringHashMap(ParsedValue),
|
||||||
|
|
||||||
|
/// Track which argument keys are allocated (short flags)
|
||||||
|
/// Long argument names come from field names (comptime strings) and shouldn't be freed
|
||||||
|
allocated_keys: std.StringHashMap(void),
|
||||||
|
|
||||||
|
/// Initialize a new argument registry
|
||||||
|
pub fn init(allocator: std.mem.Allocator) ArgumentRegistry {
|
||||||
|
return .{
|
||||||
|
.argv = std.process.argsAlloc(allocator) catch unreachable,
|
||||||
|
.allocator = allocator,
|
||||||
|
.arguments = std.StringHashMap(metadata.ArgumentMetadata).init(allocator),
|
||||||
|
.modules_by_arg = std.StringHashMap(std.ArrayListUnmanaged([]const u8)).init(allocator),
|
||||||
|
.registered_types = std.StringHashMap(void).init(allocator),
|
||||||
|
.parsed_values = std.StringHashMap(ParsedValue).init(allocator),
|
||||||
|
.allocated_keys = std.StringHashMap(void).init(allocator),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clean up all resources
|
||||||
|
pub fn deinit(self: *ArgumentRegistry) void {
|
||||||
|
// Clean up modules_by_arg lists
|
||||||
|
var modules_iter = self.modules_by_arg.valueIterator();
|
||||||
|
while (modules_iter.next()) |list| {
|
||||||
|
list.deinit(self.allocator);
|
||||||
|
}
|
||||||
|
self.modules_by_arg.deinit();
|
||||||
|
|
||||||
|
// Clean up argument keys (only short flags that were allocated)
|
||||||
|
var key_iter = self.allocated_keys.keyIterator();
|
||||||
|
while (key_iter.next()) |key| {
|
||||||
|
self.allocator.free(key.*);
|
||||||
|
}
|
||||||
|
self.allocated_keys.deinit();
|
||||||
|
self.arguments.deinit();
|
||||||
|
self.registered_types.deinit();
|
||||||
|
|
||||||
|
// Clean up parsed values
|
||||||
|
var values_iter = self.parsed_values.valueIterator();
|
||||||
|
while (values_iter.next()) |value| {
|
||||||
|
// Free memory for string types
|
||||||
|
switch (value.*) {
|
||||||
|
.string => |str| self.allocator.free(str),
|
||||||
|
.string_list => |list| {
|
||||||
|
for (list) |str| {
|
||||||
|
self.allocator.free(str);
|
||||||
|
}
|
||||||
|
self.allocator.free(list);
|
||||||
|
},
|
||||||
|
.enum_type => |enum_val| self.allocator.free(enum_val.name),
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.parsed_values.deinit();
|
||||||
|
|
||||||
|
// Free argv if we own it (only if setArgv was called)
|
||||||
|
std.process.argsFree(self.allocator, self.argv);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a type has already been registered
|
||||||
|
pub fn isTypeRegistered(self: *const ArgumentRegistry, comptime T: type) bool {
|
||||||
|
const type_name = @typeName(T);
|
||||||
|
return self.registered_types.contains(type_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scan argv for help flag without full parsing
|
||||||
|
pub fn scanForHelp(self: *ArgumentRegistry) void {
|
||||||
|
if (self.argv_scanned) return;
|
||||||
|
self.argv_scanned = true;
|
||||||
|
|
||||||
|
const argv = self.argv;
|
||||||
|
for (argv[1..]) |arg| {
|
||||||
|
// arg is already [:0]const u8, no need to span it
|
||||||
|
if (std.mem.eql(u8, arg, "--help") or
|
||||||
|
std.mem.eql(u8, arg, "-h"))
|
||||||
|
{
|
||||||
|
self.help_requested = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if help was requested (scans argv lazily)
|
||||||
|
pub fn isHelpRequested(self: *ArgumentRegistry) bool {
|
||||||
|
self.scanForHelp();
|
||||||
|
return self.help_requested;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mark a type as registered
|
||||||
|
pub fn markTypeRegistered(self: *ArgumentRegistry, comptime T: type) !void {
|
||||||
|
const type_name = @typeName(T);
|
||||||
|
try self.registered_types.put(type_name, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Look up argument metadata by name (long or short form)
|
||||||
|
pub fn getArgument(self: *const ArgumentRegistry, name: []const u8) ?*const metadata.ArgumentMetadata {
|
||||||
|
if (self.arguments.getPtr(name)) |ptr| {
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get list of modules that registered a specific argument
|
||||||
|
pub fn getModulesForArg(self: *const ArgumentRegistry, name: []const u8) ?std.ArrayListUnmanaged([]const u8) {
|
||||||
|
return self.modules_by_arg.get(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a parsed value by argument name
|
||||||
|
pub fn getParsedValue(self: *const ArgumentRegistry, name: []const u8) ?ParsedValue {
|
||||||
|
return self.parsed_values.get(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Store a parsed value
|
||||||
|
/// Frees the old value if it exists and is a string type
|
||||||
|
pub fn storeParsedValue(self: *ArgumentRegistry, name: []const u8, value: ParsedValue) !void {
|
||||||
|
// Check if there's an old value we need to free
|
||||||
|
if (self.parsed_values.get(name)) |old_value| {
|
||||||
|
switch (old_value) {
|
||||||
|
.string => |str| self.allocator.free(str),
|
||||||
|
.string_list => |list| {
|
||||||
|
for (list) |str| {
|
||||||
|
self.allocator.free(str);
|
||||||
|
}
|
||||||
|
self.allocator.free(list);
|
||||||
|
},
|
||||||
|
.enum_type => |enum_val| self.allocator.free(enum_val.name),
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try self.parsed_values.put(name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lazy populate: register metadata, parse argv, and populate struct
|
||||||
|
/// This is the main entry point for lazy parsing
|
||||||
|
pub fn populate(
|
||||||
|
self: *ArgumentRegistry,
|
||||||
|
comptime T: type,
|
||||||
|
comptime module_name: []const u8,
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
) !T {
|
||||||
|
// Register metadata if not already done
|
||||||
|
if (!self.isTypeRegistered(T)) {
|
||||||
|
try self.registerMetadata(T, module_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse argv on-demand for this type only
|
||||||
|
const argv = self.argv;
|
||||||
|
try self.parseArgvForType(T, argv);
|
||||||
|
|
||||||
|
// Populate and return the struct
|
||||||
|
const parsing = @import("parsing.zig");
|
||||||
|
return parsing.populateStruct(T, self, allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse argv only for arguments relevant to a specific type
|
||||||
|
/// Ignores unknown arguments (they may belong to other modules)
|
||||||
|
fn parseArgvForType(self: *ArgumentRegistry, comptime T: type, argv: []const [:0]const u8) !void {
|
||||||
|
_ = T; // Type is used implicitly via registered metadata
|
||||||
|
|
||||||
|
const parsing = @import("parsing.zig");
|
||||||
|
// Parse argv, ignoring unknown arguments
|
||||||
|
try parsing.parseArgv(self, argv);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Registration Methods
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
/// Register metadata for a struct type
|
||||||
|
/// INTERNAL USE ONLY: For normal use, call populate() instead
|
||||||
|
/// This is only public for testing and internal library use
|
||||||
|
pub fn registerMetadata(
|
||||||
|
self: *ArgumentRegistry,
|
||||||
|
comptime T: type,
|
||||||
|
comptime module_name: []const u8,
|
||||||
|
) !void {
|
||||||
|
// Skip if already registered
|
||||||
|
if (self.isTypeRegistered(T)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract and register each field directly
|
||||||
|
const type_info = @typeInfo(T);
|
||||||
|
if (type_info != .@"struct") {
|
||||||
|
@compileError("registerMetadata requires a struct type");
|
||||||
|
}
|
||||||
|
|
||||||
|
inline for (type_info.@"struct".fields) |field| {
|
||||||
|
const field_meta = metadata.extractFieldMetadata(T, field);
|
||||||
|
try self.registerArgument(&field_meta, module_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark type as registered
|
||||||
|
try self.markTypeRegistered(T);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Register a single argument with collision detection
|
||||||
|
fn registerArgument(
|
||||||
|
self: *ArgumentRegistry,
|
||||||
|
arg_meta: *const metadata.ArgumentMetadata,
|
||||||
|
module_name: []const u8,
|
||||||
|
) !void {
|
||||||
|
// Check if argument already exists (long form)
|
||||||
|
const long_exists = self.arguments.getPtr(arg_meta.arg_name);
|
||||||
|
if (long_exists) |existing| {
|
||||||
|
// Compatible collision: same type
|
||||||
|
if (existing.arg_type == arg_meta.arg_type) {
|
||||||
|
// Add this module to the list
|
||||||
|
try self.addModuleForArg(arg_meta.arg_name, module_name);
|
||||||
|
// Don't return yet - we might need to register short form
|
||||||
|
} else {
|
||||||
|
// Incompatible collision: different types
|
||||||
|
return error.IncompatibleArgumentType;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Register the argument (long form) - store a copy
|
||||||
|
try self.arguments.put(arg_meta.arg_name, arg_meta.*);
|
||||||
|
try self.addModuleForArg(arg_meta.arg_name, module_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register short form if present
|
||||||
|
if (arg_meta.short) |short_char| {
|
||||||
|
// Create a persistent string for the short key
|
||||||
|
const short_key = try self.allocator.alloc(u8, 1);
|
||||||
|
short_key[0] = short_char;
|
||||||
|
|
||||||
|
// Check for short flag collision
|
||||||
|
if (self.arguments.getPtr(short_key)) |existing| {
|
||||||
|
// Check if types are compatible
|
||||||
|
if (existing.arg_type == arg_meta.arg_type) {
|
||||||
|
// Compatible collision
|
||||||
|
try self.addModuleForArg(short_key, module_name);
|
||||||
|
self.allocator.free(short_key); // Free the temporary key
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.allocator.free(short_key); // Free the temporary key
|
||||||
|
return error.IncompatibleArgumentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No collision - register the short form (key will be owned by the hash map)
|
||||||
|
try self.arguments.put(short_key, arg_meta.*);
|
||||||
|
try self.addModuleForArg(short_key, module_name);
|
||||||
|
|
||||||
|
// Track that this key was allocated and needs to be freed
|
||||||
|
try self.allocated_keys.put(short_key, {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a module to the list for an argument
|
||||||
|
fn addModuleForArg(self: *ArgumentRegistry, arg_name: []const u8, module_name: []const u8) !void {
|
||||||
|
const entry = try self.modules_by_arg.getOrPut(arg_name);
|
||||||
|
if (!entry.found_existing) {
|
||||||
|
entry.value_ptr.* = std.ArrayListUnmanaged([]const u8){};
|
||||||
|
}
|
||||||
|
try entry.value_ptr.append(self.allocator, module_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if an argument is registered
|
||||||
|
pub fn hasArgument(self: *const ArgumentRegistry, name: []const u8) bool {
|
||||||
|
return self.arguments.contains(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the number of registered arguments
|
||||||
|
pub fn argumentCount(self: *const ArgumentRegistry) usize {
|
||||||
|
return self.arguments.count();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,213 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
/// Represents the types that can be used as command-line arguments
|
||||||
|
pub const ArgumentType = enum {
|
||||||
|
bool,
|
||||||
|
u8,
|
||||||
|
u16,
|
||||||
|
u32,
|
||||||
|
u64,
|
||||||
|
i8,
|
||||||
|
i16,
|
||||||
|
i32,
|
||||||
|
i64,
|
||||||
|
string,
|
||||||
|
string_list,
|
||||||
|
enum_type,
|
||||||
|
|
||||||
|
/// Convert a Zig type to ArgumentType at compile time
|
||||||
|
/// Supports: bool, integers, strings, string lists, enums, and optionals of these
|
||||||
|
pub fn fromZigType(comptime T: type) ArgumentType {
|
||||||
|
const info = @typeInfo(T);
|
||||||
|
|
||||||
|
return switch (info) {
|
||||||
|
.bool => .bool,
|
||||||
|
|
||||||
|
.int => |int| {
|
||||||
|
if (int.signedness == .unsigned) {
|
||||||
|
return switch (int.bits) {
|
||||||
|
8 => .u8,
|
||||||
|
16 => .u16,
|
||||||
|
32 => .u32,
|
||||||
|
64 => .u64,
|
||||||
|
else => @compileError("Unsupported unsigned integer size for argument: " ++ @typeName(T) ++ ". Supported sizes: u8, u16, u32, u64"),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return switch (int.bits) {
|
||||||
|
8 => .i8,
|
||||||
|
16 => .i16,
|
||||||
|
32 => .i32,
|
||||||
|
64 => .i64,
|
||||||
|
else => @compileError("Unsupported signed integer size for argument: " ++ @typeName(T) ++ ". Supported sizes: i8, i16, i32, i64"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
.pointer => |ptr| {
|
||||||
|
if (ptr.size == .slice) {
|
||||||
|
if (ptr.child == u8) return .string;
|
||||||
|
|
||||||
|
// Check for []const []const u8 (string list)
|
||||||
|
const child_info = @typeInfo(ptr.child);
|
||||||
|
if (child_info == .pointer) {
|
||||||
|
const inner_ptr = child_info.pointer;
|
||||||
|
if (inner_ptr.size == .slice and inner_ptr.child == u8) {
|
||||||
|
return .string_list;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@compileError("Unsupported pointer type for argument: " ++ @typeName(T) ++ ". Only []const u8 (string) and []const []const u8 (string list) are supported");
|
||||||
|
},
|
||||||
|
|
||||||
|
.@"enum" => .enum_type,
|
||||||
|
|
||||||
|
.optional => |opt| fromZigType(opt.child),
|
||||||
|
|
||||||
|
else => @compileError("Unsupported type for command-line argument: " ++ @typeName(T) ++ ". Supported types: bool, integers (u8-u64, i8-i64), strings ([]const u8), string lists ([]const []const u8), enums, and optionals of these types"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if two ArgumentTypes are compatible (same type)
|
||||||
|
pub fn matches(self: ArgumentType, other: ArgumentType) bool {
|
||||||
|
return self == other;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Represents a parsed argument value
|
||||||
|
/// Memory for strings is owned by the caller's allocator
|
||||||
|
pub const ParsedValue = union(ArgumentType) {
|
||||||
|
bool: bool,
|
||||||
|
u8: u8,
|
||||||
|
u16: u16,
|
||||||
|
u32: u32,
|
||||||
|
u64: u64,
|
||||||
|
i8: i8,
|
||||||
|
i16: i16,
|
||||||
|
i32: i32,
|
||||||
|
i64: i64,
|
||||||
|
string: []const u8,
|
||||||
|
string_list: []const []const u8,
|
||||||
|
enum_type: struct {
|
||||||
|
name: []const u8,
|
||||||
|
value: usize,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Parse a string into a ParsedValue of the specified type
|
||||||
|
/// For strings, duplicates into the provided allocator
|
||||||
|
/// For string_list, this is not the right interface - use a different method
|
||||||
|
pub fn fromString(arg_type: ArgumentType, str: []const u8, allocator: std.mem.Allocator) !ParsedValue {
|
||||||
|
return switch (arg_type) {
|
||||||
|
.bool => parseBool(str),
|
||||||
|
.u8 => .{ .u8 = try std.fmt.parseInt(u8, str, 0) },
|
||||||
|
.u16 => .{ .u16 = try std.fmt.parseInt(u16, str, 0) },
|
||||||
|
.u32 => .{ .u32 = try std.fmt.parseInt(u32, str, 0) },
|
||||||
|
.u64 => .{ .u64 = try std.fmt.parseInt(u64, str, 0) },
|
||||||
|
.i8 => .{ .i8 = try std.fmt.parseInt(i8, str, 0) },
|
||||||
|
.i16 => .{ .i16 = try std.fmt.parseInt(i16, str, 0) },
|
||||||
|
.i32 => .{ .i32 = try std.fmt.parseInt(i32, str, 0) },
|
||||||
|
.i64 => .{ .i64 = try std.fmt.parseInt(i64, str, 0) },
|
||||||
|
.string => .{ .string = try allocator.dupe(u8, str) },
|
||||||
|
.string_list => error.InvalidValue, // Use appendStringList instead
|
||||||
|
.enum_type => error.InvalidValue, // Use parseEnum instead
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a boolean from string
|
||||||
|
/// Accepts: "true", "false", "1", "0", "yes", "no", "on", "off" (case-insensitive)
|
||||||
|
fn parseBool(str: []const u8) !ParsedValue {
|
||||||
|
var lower_buf: [8]u8 = undefined;
|
||||||
|
if (str.len > lower_buf.len) return error.InvalidValue;
|
||||||
|
|
||||||
|
// Convert to lowercase for comparison
|
||||||
|
for (str, 0..) |c, i| {
|
||||||
|
lower_buf[i] = std.ascii.toLower(c);
|
||||||
|
}
|
||||||
|
const lower = lower_buf[0..str.len];
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, lower, "true") or
|
||||||
|
std.mem.eql(u8, lower, "1") or
|
||||||
|
std.mem.eql(u8, lower, "yes") or
|
||||||
|
std.mem.eql(u8, lower, "on"))
|
||||||
|
{
|
||||||
|
return .{ .bool = true };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, lower, "false") or
|
||||||
|
std.mem.eql(u8, lower, "0") or
|
||||||
|
std.mem.eql(u8, lower, "no") or
|
||||||
|
std.mem.eql(u8, lower, "off"))
|
||||||
|
{
|
||||||
|
return .{ .bool = false };
|
||||||
|
}
|
||||||
|
|
||||||
|
return error.InvalidValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse an enum value from string
|
||||||
|
/// Compares string against enum field names (case-sensitive)
|
||||||
|
pub fn parseEnum(comptime E: type, str: []const u8, allocator: std.mem.Allocator) !ParsedValue {
|
||||||
|
const info = @typeInfo(E);
|
||||||
|
if (info != .@"enum") @compileError("parseEnum requires an enum type");
|
||||||
|
|
||||||
|
inline for (info.@"enum".fields, 0..) |field, i| {
|
||||||
|
if (std.mem.eql(u8, field.name, str)) {
|
||||||
|
return .{
|
||||||
|
.enum_type = .{
|
||||||
|
.name = try allocator.dupe(u8, field.name),
|
||||||
|
.value = i,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return error.InvalidValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert ParsedValue to a typed value
|
||||||
|
/// Caller must ensure the type matches the parsed value's type
|
||||||
|
pub fn toTypedValue(self: ParsedValue, comptime T: type) T {
|
||||||
|
const target_type = ArgumentType.fromZigType(T);
|
||||||
|
const info = @typeInfo(T);
|
||||||
|
|
||||||
|
// Handle optionals by unwrapping
|
||||||
|
if (info == .optional) {
|
||||||
|
return self.toTypedValue(info.optional.child);
|
||||||
|
}
|
||||||
|
|
||||||
|
return switch (target_type) {
|
||||||
|
.bool => if (@typeInfo(T) == .bool) self.bool else unreachable,
|
||||||
|
.u8 => if (T == u8) self.u8 else unreachable,
|
||||||
|
.u16 => if (T == u16) self.u16 else unreachable,
|
||||||
|
.u32 => if (T == u32) self.u32 else unreachable,
|
||||||
|
.u64 => if (T == u64) self.u64 else unreachable,
|
||||||
|
.i8 => if (T == i8) self.i8 else unreachable,
|
||||||
|
.i16 => if (T == i16) self.i16 else unreachable,
|
||||||
|
.i32 => if (T == i32) self.i32 else unreachable,
|
||||||
|
.i64 => if (T == i64) self.i64 else unreachable,
|
||||||
|
.string => if (T == []const u8) self.string else unreachable,
|
||||||
|
.string_list => if (T == []const []const u8) self.string_list else unreachable,
|
||||||
|
.enum_type => blk: {
|
||||||
|
const enum_info = @typeInfo(T);
|
||||||
|
if (enum_info != .@"enum") unreachable;
|
||||||
|
// Convert value index back to enum
|
||||||
|
inline for (enum_info.@"enum".fields, 0..) |field, i| {
|
||||||
|
if (i == self.enum_type.value) {
|
||||||
|
break :blk @field(T, field.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unreachable;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Compile-time verification that common types work
|
||||||
|
comptime {
|
||||||
|
_ = ArgumentType.fromZigType(bool);
|
||||||
|
_ = ArgumentType.fromZigType(u32);
|
||||||
|
_ = ArgumentType.fromZigType(i32);
|
||||||
|
_ = ArgumentType.fromZigType([]const u8);
|
||||||
|
_ = ArgumentType.fromZigType(?u32);
|
||||||
|
_ = ArgumentType.fromZigType(?[]const u8);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
/// Comprehensive error set for zargs parsing
|
||||||
|
pub const Error = error{
|
||||||
|
/// Argument type does not match the expected type for a field
|
||||||
|
IncompatibleArgumentType,
|
||||||
|
|
||||||
|
/// Unknown command-line argument provided
|
||||||
|
UnknownArgument,
|
||||||
|
|
||||||
|
/// Invalid value format (generic)
|
||||||
|
InvalidValue,
|
||||||
|
|
||||||
|
/// Invalid integer value (overflow, underflow, or invalid characters)
|
||||||
|
InvalidIntegerValue,
|
||||||
|
|
||||||
|
/// Invalid boolean value (not true/false/1/0/yes/no/on/off)
|
||||||
|
InvalidBooleanValue,
|
||||||
|
|
||||||
|
/// Invalid enum value (not a valid enum field name)
|
||||||
|
InvalidEnumValue,
|
||||||
|
|
||||||
|
/// Required argument value is missing (e.g., --flag without value)
|
||||||
|
MissingArgumentValue,
|
||||||
|
|
||||||
|
/// Memory allocation failed
|
||||||
|
OutOfMemory,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Context for error reporting
|
||||||
|
pub const ErrorContext = struct {
|
||||||
|
/// The argument name that caused the error (e.g., "--verbose")
|
||||||
|
argument_name: ?[]const u8 = null,
|
||||||
|
|
||||||
|
/// The value that failed to parse
|
||||||
|
invalid_value: ?[]const u8 = null,
|
||||||
|
|
||||||
|
/// Expected type name for the argument
|
||||||
|
expected_type: ?[]const u8 = null,
|
||||||
|
|
||||||
|
/// Additional context message
|
||||||
|
message: ?[]const u8 = null,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Result type that can carry error context
|
||||||
|
pub fn Result(comptime T: type) type {
|
||||||
|
return union(enum) {
|
||||||
|
ok: T,
|
||||||
|
err: struct {
|
||||||
|
error_type: Error,
|
||||||
|
context: ErrorContext,
|
||||||
|
},
|
||||||
|
|
||||||
|
pub fn isOk(self: @This()) bool {
|
||||||
|
return self == .ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isErr(self: @This()) bool {
|
||||||
|
return self == .err;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unwrap(self: @This()) T {
|
||||||
|
return switch (self) {
|
||||||
|
.ok => |value| value,
|
||||||
|
.err => unreachable,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unwrapOr(self: @This(), default: T) T {
|
||||||
|
return switch (self) {
|
||||||
|
.ok => |value| value,
|
||||||
|
.err => default,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,198 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const metadata = @import("metadata.zig");
|
||||||
|
const ArgumentRegistry = @import("ArgumentRegistry.zig").ArgumentRegistry;
|
||||||
|
const ArgumentType = @import("ArgumentType.zig").ArgumentType;
|
||||||
|
|
||||||
|
/// Generate help text from registered arguments
|
||||||
|
pub fn generateHelpText(
|
||||||
|
registry: ArgumentRegistry,
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
program_name: ?[]const u8,
|
||||||
|
) ![]const u8 {
|
||||||
|
var buffer = std.ArrayListUnmanaged(u8){};
|
||||||
|
errdefer buffer.deinit(allocator);
|
||||||
|
const writer = buffer.writer(allocator);
|
||||||
|
|
||||||
|
// Write program name/header
|
||||||
|
if (program_name) |name| {
|
||||||
|
try writer.print("Usage: {s} [OPTIONS]\n\n", .{name});
|
||||||
|
} else {
|
||||||
|
try writer.writeAll("Usage: [OPTIONS]\n\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write description if available (TODO: add module_info support)
|
||||||
|
|
||||||
|
// Collect all arguments for formatting
|
||||||
|
var args_list = std.ArrayListUnmanaged(ArgumentInfo){};
|
||||||
|
defer args_list.deinit(allocator);
|
||||||
|
|
||||||
|
var arg_iter = registry.arguments.iterator();
|
||||||
|
while (arg_iter.next()) |entry| {
|
||||||
|
const arg_meta = entry.value_ptr;
|
||||||
|
|
||||||
|
// Skip short flags (they'll be shown with their long form)
|
||||||
|
if (entry.key_ptr.len == 1) continue;
|
||||||
|
|
||||||
|
try args_list.append(allocator, .{
|
||||||
|
.long_name = arg_meta.arg_name,
|
||||||
|
.short_char = arg_meta.short,
|
||||||
|
.help_text = arg_meta.help,
|
||||||
|
.arg_type = arg_meta.arg_type,
|
||||||
|
.default_value = arg_meta.default_value,
|
||||||
|
.required = arg_meta.required,
|
||||||
|
.enum_values = arg_meta.enum_values,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort arguments alphabetically by long name
|
||||||
|
const items = args_list.items;
|
||||||
|
std.mem.sort(ArgumentInfo, items, {}, argumentLessThan);
|
||||||
|
|
||||||
|
// Calculate maximum width for alignment
|
||||||
|
var max_flags_width: usize = 0;
|
||||||
|
for (items) |arg| {
|
||||||
|
const width = calculateFlagsWidth(arg);
|
||||||
|
if (width > max_flags_width) {
|
||||||
|
max_flags_width = width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add padding
|
||||||
|
const padding = 2;
|
||||||
|
const total_width = max_flags_width + padding;
|
||||||
|
|
||||||
|
// Write "Options:" header
|
||||||
|
try writer.writeAll("Options:\n");
|
||||||
|
|
||||||
|
// Always show help first
|
||||||
|
try writer.writeAll(" -h, --help");
|
||||||
|
try writePadding(writer, 12, total_width);
|
||||||
|
try writer.writeAll("Show this help message\n");
|
||||||
|
|
||||||
|
// Write each argument
|
||||||
|
for (items) |arg| {
|
||||||
|
try writeArgumentHelp(writer, arg, total_width);
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer.toOwnedSlice(allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Information about an argument for help display
|
||||||
|
const ArgumentInfo = struct {
|
||||||
|
long_name: []const u8,
|
||||||
|
short_char: ?u8,
|
||||||
|
help_text: []const u8,
|
||||||
|
arg_type: ArgumentType,
|
||||||
|
default_value: ?[]const u8,
|
||||||
|
required: bool,
|
||||||
|
enum_values: ?[]const []const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Compare two arguments for sorting
|
||||||
|
fn argumentLessThan(_: void, a: ArgumentInfo, b: ArgumentInfo) bool {
|
||||||
|
return std.mem.lessThan(u8, a.long_name, b.long_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculate the width of the flags portion (e.g., "-v, --verbose")
|
||||||
|
fn calculateFlagsWidth(arg: ArgumentInfo) usize {
|
||||||
|
var width: usize = 2; // Leading " "
|
||||||
|
|
||||||
|
if (arg.short_char) |_| {
|
||||||
|
width += 4; // "-x, "
|
||||||
|
}
|
||||||
|
|
||||||
|
width += 2; // "--"
|
||||||
|
width += arg.long_name.len;
|
||||||
|
|
||||||
|
// Add value placeholder for non-boolean types
|
||||||
|
if (arg.arg_type != .bool) {
|
||||||
|
width += 1; // space
|
||||||
|
width += getValuePlaceholder(arg.arg_type).len;
|
||||||
|
}
|
||||||
|
|
||||||
|
return width;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a placeholder string for the argument type
|
||||||
|
fn getValuePlaceholder(arg_type: ArgumentType) []const u8 {
|
||||||
|
return switch (arg_type) {
|
||||||
|
.bool => "",
|
||||||
|
.u8, .u16, .u32, .u64, .i8, .i16, .i32, .i64 => "<NUM>",
|
||||||
|
.string => "<VALUE>",
|
||||||
|
.string_list => "<LIST>",
|
||||||
|
.enum_type => "<CHOICE>",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write padding spaces
|
||||||
|
fn writePadding(writer: anytype, current_width: usize, target_width: usize) !void {
|
||||||
|
if (current_width >= target_width) {
|
||||||
|
try writer.writeAll(" ");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const spaces_needed = target_width - current_width;
|
||||||
|
var i: usize = 0;
|
||||||
|
while (i < spaces_needed) : (i += 1) {
|
||||||
|
try writer.writeByte(' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write help for a single argument
|
||||||
|
fn writeArgumentHelp(writer: anytype, arg: ArgumentInfo, total_width: usize) !void {
|
||||||
|
// Write flags
|
||||||
|
try writer.writeAll(" ");
|
||||||
|
var current_width: usize = 2;
|
||||||
|
|
||||||
|
if (arg.short_char) |short| {
|
||||||
|
try writer.print("-{c}, ", .{short});
|
||||||
|
current_width += 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
try writer.print("--{s}", .{arg.long_name});
|
||||||
|
current_width += 2 + arg.long_name.len;
|
||||||
|
|
||||||
|
// Add value placeholder for non-boolean types
|
||||||
|
if (arg.arg_type != .bool) {
|
||||||
|
const placeholder = getValuePlaceholder(arg.arg_type);
|
||||||
|
try writer.print(" {s}", .{placeholder});
|
||||||
|
current_width += 1 + placeholder.len;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write padding
|
||||||
|
try writePadding(writer, current_width, total_width);
|
||||||
|
|
||||||
|
// Write help text
|
||||||
|
try writer.writeAll(arg.help_text);
|
||||||
|
|
||||||
|
// Add default value if present
|
||||||
|
if (arg.default_value) |default| {
|
||||||
|
try writer.print(" [default: {s}]", .{default});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add enum choices if present
|
||||||
|
if (arg.enum_values) |values| {
|
||||||
|
if (values.len > 0) {
|
||||||
|
try writer.writeAll(" [choices: ");
|
||||||
|
for (values, 0..) |value, i| {
|
||||||
|
if (i > 0) try writer.writeAll(", ");
|
||||||
|
try writer.writeAll(value);
|
||||||
|
}
|
||||||
|
try writer.writeByte(']');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add required marker if no default
|
||||||
|
if (arg.required and arg.default_value == null) {
|
||||||
|
try writer.writeAll(" (required)");
|
||||||
|
}
|
||||||
|
|
||||||
|
try writer.writeByte('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simple help text generation (without module grouping)
|
||||||
|
pub fn generateSimpleHelp(
|
||||||
|
registry: *const ArgumentRegistry,
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
) ![]const u8 {
|
||||||
|
return generateHelpText(registry, allocator, null);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
// Public exports
|
||||||
|
pub const ArgumentType = @import("ArgumentType.zig").ArgumentType;
|
||||||
|
pub const ParsedValue = @import("ArgumentType.zig").ParsedValue;
|
||||||
|
pub const ArgumentMetadata = @import("metadata.zig").ArgumentMetadata;
|
||||||
|
pub const FieldMeta = @import("metadata.zig").FieldMeta;
|
||||||
|
pub const ModuleInfo = @import("metadata.zig").ModuleInfo;
|
||||||
|
pub const ArgumentRegistry = @import("ArgumentRegistry.zig").ArgumentRegistry;
|
||||||
|
pub const generateHelpText = @import("help.zig").generateHelpText;
|
||||||
|
|
||||||
|
pub var gRegistry: ?ArgumentRegistry = null;
|
||||||
|
|
||||||
|
pub fn getUsageAlloc(allocator: std.mem.Allocator, programName: []const u8) ![]const u8 {
|
||||||
|
if (gRegistry == null) {
|
||||||
|
gRegistry = ArgumentRegistry.init(allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
return try generateHelpText(gRegistry.?, allocator, programName);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse(comptime T: type, allocator: std.mem.Allocator) !T {
|
||||||
|
if (gRegistry == null) {
|
||||||
|
gRegistry = ArgumentRegistry.init(allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = try gRegistry.?.populate(T, @typeName(T), allocator);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isHelp(allocator: std.mem.Allocator) bool {
|
||||||
|
if (gRegistry == null) {
|
||||||
|
gRegistry = ArgumentRegistry.init(allocator);
|
||||||
|
}
|
||||||
|
gRegistry.?.scanForHelp();
|
||||||
|
return gRegistry.?.help_requested;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn shutdown() void {
|
||||||
|
if (gRegistry) |*reg| {
|
||||||
|
reg.deinit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test {
|
||||||
|
// Reference all test files
|
||||||
|
_ = @import("ArgumentType.zig");
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,331 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const ArgumentTypeModule = @import("ArgumentType.zig");
|
||||||
|
const ArgumentType = ArgumentTypeModule.ArgumentType;
|
||||||
|
|
||||||
|
/// Metadata for a single command-line argument
|
||||||
|
/// All fields are comptime-known
|
||||||
|
pub const ArgumentMetadata = struct {
|
||||||
|
/// The field name in the struct (e.g., "verboseMode")
|
||||||
|
field_name: []const u8,
|
||||||
|
|
||||||
|
/// The command-line argument name (e.g., "verbose-mode")
|
||||||
|
/// Generated from field_name if not explicitly provided
|
||||||
|
arg_name: []const u8,
|
||||||
|
|
||||||
|
/// Type of the argument
|
||||||
|
arg_type: ArgumentType,
|
||||||
|
|
||||||
|
/// Short flag (single character, e.g., 'v' for -v)
|
||||||
|
/// null if no short flag
|
||||||
|
short: ?u8 = null,
|
||||||
|
|
||||||
|
/// Help text describing the argument
|
||||||
|
help: []const u8 = "",
|
||||||
|
|
||||||
|
/// Whether this argument is required
|
||||||
|
required: bool = false,
|
||||||
|
|
||||||
|
/// Default value as a string representation
|
||||||
|
/// Used for help text display
|
||||||
|
default_value: ?[]const u8 = null,
|
||||||
|
|
||||||
|
/// Whether this field is an optional type (?T)
|
||||||
|
is_optional: bool = false,
|
||||||
|
|
||||||
|
/// For enum types, list of valid values
|
||||||
|
/// Empty slice for non-enum types
|
||||||
|
enum_values: []const []const u8 = &.{},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// User-provided metadata for customizing argument behavior
|
||||||
|
/// This is what users write in `pub const meta = .{ .field_name = .{...} }`
|
||||||
|
pub const FieldMeta = struct {
|
||||||
|
/// Custom argument name (overrides kebab-case conversion)
|
||||||
|
name: ?[]const u8 = null,
|
||||||
|
|
||||||
|
/// Short flag character
|
||||||
|
short: ?u8 = null,
|
||||||
|
|
||||||
|
/// Help text
|
||||||
|
help: ?[]const u8 = null,
|
||||||
|
|
||||||
|
/// Whether the argument is required
|
||||||
|
required: ?bool = null,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Complete metadata for a parsed struct type
|
||||||
|
pub const ModuleInfo = struct {
|
||||||
|
/// Name of the program/module
|
||||||
|
program_name: []const u8,
|
||||||
|
|
||||||
|
/// Brief description of the program
|
||||||
|
description: []const u8 = "",
|
||||||
|
|
||||||
|
/// List of all arguments
|
||||||
|
arguments: []const ArgumentMetadata,
|
||||||
|
|
||||||
|
/// Program version (if provided)
|
||||||
|
version: ?[]const u8 = null,
|
||||||
|
|
||||||
|
/// Usage examples
|
||||||
|
examples: []const []const u8 = &.{},
|
||||||
|
|
||||||
|
/// Allocator used to create this metadata
|
||||||
|
/// Note: All strings are comptime-known, no allocation needed
|
||||||
|
comptime_only: bool = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Helper to check if a type has a meta declaration
|
||||||
|
pub fn hasMeta(comptime T: type) bool {
|
||||||
|
return @hasDecl(T, "meta");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper to check if a specific field has metadata
|
||||||
|
pub fn hasFieldMeta(comptime T: type, comptime field_name: []const u8) bool {
|
||||||
|
if (!hasMeta(T)) return false;
|
||||||
|
const meta = @field(T, "meta");
|
||||||
|
return @hasField(@TypeOf(meta), field_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the meta declaration for a field, or return default
|
||||||
|
pub fn getFieldMeta(comptime T: type, comptime field_name: []const u8) FieldMeta {
|
||||||
|
if (!@hasDecl(T, "meta")) return .{};
|
||||||
|
|
||||||
|
const meta = @field(T, "meta");
|
||||||
|
if (!@hasField(@TypeOf(meta), field_name)) return .{};
|
||||||
|
|
||||||
|
const field_meta = @field(meta, field_name);
|
||||||
|
|
||||||
|
// Convert to FieldMeta if it's an anonymous struct
|
||||||
|
return .{
|
||||||
|
.name = if (@hasField(@TypeOf(field_meta), "name")) field_meta.name else null,
|
||||||
|
.short = if (@hasField(@TypeOf(field_meta), "short")) field_meta.short else null,
|
||||||
|
.help = if (@hasField(@TypeOf(field_meta), "help")) field_meta.help else null,
|
||||||
|
.required = if (@hasField(@TypeOf(field_meta), "required")) field_meta.required else null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper to check if type has a module_info declaration
|
||||||
|
pub fn hasModuleInfo(comptime T: type) bool {
|
||||||
|
return @hasDecl(T, "module_info");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the module info for a type, or return defaults
|
||||||
|
pub fn getModuleInfo(comptime T: type, comptime default_name: []const u8) struct {
|
||||||
|
description: []const u8,
|
||||||
|
version: ?[]const u8,
|
||||||
|
examples: []const []const u8,
|
||||||
|
} {
|
||||||
|
_ = default_name; // Reserved for future use
|
||||||
|
if (!hasModuleInfo(T)) {
|
||||||
|
return .{
|
||||||
|
.description = "",
|
||||||
|
.version = null,
|
||||||
|
.examples = &.{},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const info = @field(T, "module_info");
|
||||||
|
return .{
|
||||||
|
.description = if (@hasField(@TypeOf(info), "description")) info.description else "",
|
||||||
|
.version = if (@hasField(@TypeOf(info), "version")) info.version else null,
|
||||||
|
.examples = if (@hasField(@TypeOf(info), "examples")) info.examples else &.{},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compile-time validation
|
||||||
|
comptime {
|
||||||
|
// Verify ArgumentMetadata can be created
|
||||||
|
const test_meta = ArgumentMetadata{
|
||||||
|
.field_name = "test",
|
||||||
|
.arg_name = "test",
|
||||||
|
.arg_type = .bool,
|
||||||
|
};
|
||||||
|
_ = test_meta;
|
||||||
|
|
||||||
|
// Verify FieldMeta default initialization
|
||||||
|
const field_meta = FieldMeta{};
|
||||||
|
_ = field_meta;
|
||||||
|
|
||||||
|
// Verify ModuleInfo can be created
|
||||||
|
const module_info = ModuleInfo{
|
||||||
|
.program_name = "test",
|
||||||
|
.arguments = &.{},
|
||||||
|
};
|
||||||
|
_ = module_info;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Metadata Extraction
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// Extract metadata for a single field
|
||||||
|
pub fn extractFieldMetadata(
|
||||||
|
comptime T: type,
|
||||||
|
comptime field: std.builtin.Type.StructField,
|
||||||
|
) ArgumentMetadata {
|
||||||
|
// Get user-provided metadata if it exists
|
||||||
|
const user_meta = getFieldMeta(T, field.name);
|
||||||
|
|
||||||
|
// Determine argument type
|
||||||
|
const arg_type = ArgumentType.fromZigType(field.type);
|
||||||
|
|
||||||
|
// Check if field is optional
|
||||||
|
const is_optional = @typeInfo(field.type) == .optional;
|
||||||
|
|
||||||
|
// Generate argument name (custom name or field name)
|
||||||
|
// TODO: Add kebab-case conversion back
|
||||||
|
const arg_name = if (user_meta.name) |custom_name|
|
||||||
|
custom_name
|
||||||
|
else
|
||||||
|
field.name;
|
||||||
|
|
||||||
|
// Extract enum values if this is an enum type
|
||||||
|
// TODO: Extract actual enum values - currently returns empty for comptime issues
|
||||||
|
const enum_values = &[_][]const u8{};
|
||||||
|
|
||||||
|
// Format default value if field has one
|
||||||
|
const default_value = if (field.default_value_ptr) |default_ptr|
|
||||||
|
formatDefaultValue(field.type, default_ptr)
|
||||||
|
else
|
||||||
|
null;
|
||||||
|
|
||||||
|
return ArgumentMetadata{
|
||||||
|
.field_name = field.name,
|
||||||
|
.arg_name = arg_name,
|
||||||
|
.arg_type = arg_type,
|
||||||
|
.short = user_meta.short,
|
||||||
|
.help = user_meta.help orelse "",
|
||||||
|
.required = user_meta.required orelse !is_optional,
|
||||||
|
.default_value = default_value,
|
||||||
|
.is_optional = is_optional,
|
||||||
|
.enum_values = enum_values,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract enum field names as strings
|
||||||
|
fn extractEnumValues(comptime T: type) []const []const u8 {
|
||||||
|
// Unwrap optional if needed
|
||||||
|
const ActualType = if (@typeInfo(T) == .optional)
|
||||||
|
@typeInfo(T).optional.child
|
||||||
|
else
|
||||||
|
T;
|
||||||
|
|
||||||
|
const info = @typeInfo(ActualType);
|
||||||
|
if (info != .@"enum") {
|
||||||
|
return &[_][]const u8{};
|
||||||
|
}
|
||||||
|
|
||||||
|
comptime {
|
||||||
|
var values: [info.@"enum".fields.len][]const u8 = undefined;
|
||||||
|
for (info.@"enum".fields, 0..) |field, i| {
|
||||||
|
values[i] = field.name;
|
||||||
|
}
|
||||||
|
const final = values;
|
||||||
|
return &final;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Format a default value as a string for display in help text
|
||||||
|
fn formatDefaultValue(comptime T: type, default_ptr: *const anyopaque) ?[]const u8 {
|
||||||
|
// Unwrap optional if needed
|
||||||
|
const ActualType = if (@typeInfo(T) == .optional)
|
||||||
|
@typeInfo(T).optional.child
|
||||||
|
else
|
||||||
|
T;
|
||||||
|
|
||||||
|
const value_ptr: *const ActualType = @ptrCast(@alignCast(default_ptr));
|
||||||
|
const value = value_ptr.*;
|
||||||
|
|
||||||
|
const type_info = @typeInfo(ActualType);
|
||||||
|
|
||||||
|
return switch (type_info) {
|
||||||
|
.bool => if (value) "true" else "false",
|
||||||
|
.int => null, // TODO: Integer default value formatting (comptime limitation)
|
||||||
|
.pointer => |ptr| blk: {
|
||||||
|
if (ptr.size == .slice and ptr.child == u8) {
|
||||||
|
// String type
|
||||||
|
break :blk value;
|
||||||
|
}
|
||||||
|
break :blk null;
|
||||||
|
},
|
||||||
|
.@"enum" => blk: {
|
||||||
|
// Can't use @tagName at comptime with generic enum values
|
||||||
|
// Just return the enum type name for now
|
||||||
|
break :blk @typeName(ActualType);
|
||||||
|
},
|
||||||
|
else => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Format an integer value as a compile-time string (kept for backwards compatibility)
|
||||||
|
fn formatInt(comptime T: type, comptime value: T) []const u8 {
|
||||||
|
comptime {
|
||||||
|
// Handle special cases first
|
||||||
|
if (value == 0) return "0";
|
||||||
|
if (value == 1) return "1";
|
||||||
|
if (value == -1) return "-1";
|
||||||
|
|
||||||
|
// Handle other small values manually
|
||||||
|
if (value == 2) return "2";
|
||||||
|
if (value == 3) return "3";
|
||||||
|
if (value == 4) return "4";
|
||||||
|
if (value == 5) return "5";
|
||||||
|
if (value == 6) return "6";
|
||||||
|
if (value == 7) return "7";
|
||||||
|
if (value == 8) return "8";
|
||||||
|
if (value == 9) return "9";
|
||||||
|
if (value == 10) return "10";
|
||||||
|
if (value == -2) return "-2";
|
||||||
|
if (value == -3) return "-3";
|
||||||
|
if (value == -4) return "-4";
|
||||||
|
if (value == -5) return "-5";
|
||||||
|
if (value == -6) return "-6";
|
||||||
|
if (value == -7) return "-7";
|
||||||
|
if (value == -8) return "-8";
|
||||||
|
if (value == -9) return "-9";
|
||||||
|
if (value == -10) return "-10";
|
||||||
|
|
||||||
|
// For larger values, use std.fmt to format at comptime
|
||||||
|
var buf: [64]u8 = undefined;
|
||||||
|
const str = std.fmt.bufPrint(&buf, "{d}", .{value}) catch "(default)";
|
||||||
|
// Copy to a properly sized buffer
|
||||||
|
var result: [str.len]u8 = undefined;
|
||||||
|
@memcpy(&result, str);
|
||||||
|
const final = result;
|
||||||
|
return &final;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract all field metadata from a struct
|
||||||
|
pub fn extractAllFieldMetadata(comptime T: type) []const ArgumentMetadata {
|
||||||
|
const type_info = @typeInfo(T);
|
||||||
|
if (type_info != .@"struct") {
|
||||||
|
@compileError("extractAllFieldMetadata requires a struct type");
|
||||||
|
}
|
||||||
|
|
||||||
|
const fields = type_info.@"struct".fields;
|
||||||
|
|
||||||
|
comptime {
|
||||||
|
var metadata: [fields.len]ArgumentMetadata = undefined;
|
||||||
|
for (fields, 0..) |field, i| {
|
||||||
|
metadata[i] = extractFieldMetadata(T, field);
|
||||||
|
}
|
||||||
|
const final = metadata;
|
||||||
|
return &final;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build complete ModuleInfo for a struct type
|
||||||
|
pub fn buildModuleInfo(comptime T: type, comptime program_name: []const u8) ModuleInfo {
|
||||||
|
const module_info = getModuleInfo(T, program_name);
|
||||||
|
const arguments = extractAllFieldMetadata(T);
|
||||||
|
|
||||||
|
return ModuleInfo{
|
||||||
|
.program_name = program_name,
|
||||||
|
.description = module_info.description,
|
||||||
|
.arguments = arguments,
|
||||||
|
.version = module_info.version,
|
||||||
|
.examples = module_info.examples,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,354 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const ArgumentType = @import("ArgumentType").ArgumentType;
|
||||||
|
const ParsedValue = @import("ArgumentType").ParsedValue;
|
||||||
|
const metadata = @import("metadata");
|
||||||
|
|
||||||
|
/// Global arena allocator for argument parsing
|
||||||
|
/// All parsed strings and allocations live here until program exit
|
||||||
|
var global_parse_arena: ?std.heap.ArenaAllocator = null;
|
||||||
|
var global_parse_arena_mutex: std.Thread.Mutex = .{};
|
||||||
|
|
||||||
|
/// Get or create the global parse arena
|
||||||
|
fn getParseArena(parent_allocator: std.mem.Allocator) !std.mem.Allocator {
|
||||||
|
global_parse_arena_mutex.lock();
|
||||||
|
defer global_parse_arena_mutex.unlock();
|
||||||
|
|
||||||
|
if (global_parse_arena == null) {
|
||||||
|
global_parse_arena = std.heap.ArenaAllocator.init(parent_allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
return global_parse_arena.?.allocator();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Temporary storage for parsed values during struct population
|
||||||
|
/// All allocations use the arena allocator and live until program exit
|
||||||
|
const ParsedArguments = struct {
|
||||||
|
arena: std.mem.Allocator,
|
||||||
|
values: std.StringHashMap(ParsedValue),
|
||||||
|
|
||||||
|
pub fn init(arena: std.mem.Allocator) ParsedArguments {
|
||||||
|
return .{
|
||||||
|
.arena = arena,
|
||||||
|
.values = std.StringHashMap(ParsedValue).init(arena),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *ParsedArguments) void {
|
||||||
|
// No need to free individual values - arena owns everything
|
||||||
|
self.values.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn put(self: *ParsedArguments, name: []const u8, value: ParsedValue) !void {
|
||||||
|
// For repeated arguments (like lists), we don't need to free old values
|
||||||
|
// Arena will clean up everything eventually
|
||||||
|
try self.values.put(name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(self: *const ParsedArguments, name: []const u8) ?ParsedValue {
|
||||||
|
return self.values.get(name);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Check if help was requested in argv
|
||||||
|
pub fn isHelpRequested(argv: []const [:0]const u8) bool {
|
||||||
|
for (argv[1..]) |arg| {
|
||||||
|
if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse argv for a specific struct type
|
||||||
|
/// Only parses arguments that match the struct's fields
|
||||||
|
fn parseForStruct(
|
||||||
|
comptime T: type,
|
||||||
|
argv: []const [:0]const u8,
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
) !ParsedArguments {
|
||||||
|
// Get the global arena for all parse allocations
|
||||||
|
const arena = try getParseArena(allocator);
|
||||||
|
var result = ParsedArguments.init(arena);
|
||||||
|
errdefer result.deinit();
|
||||||
|
|
||||||
|
const type_info = @typeInfo(T);
|
||||||
|
if (type_info != .@"struct") {
|
||||||
|
@compileError("parseForStruct requires a struct type");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build a comptime lookup table for quick matching
|
||||||
|
// Maps argument names (both long and short) to field info
|
||||||
|
// We use a simple struct to avoid the formatDefaultValue issue
|
||||||
|
const FieldInfo = struct {
|
||||||
|
arg_name: []const u8,
|
||||||
|
arg_type: ArgumentType,
|
||||||
|
short: ?u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
comptime var field_lookup: std.StaticStringMap(FieldInfo) = blk: {
|
||||||
|
var entries: []const struct { []const u8, FieldInfo } = &.{};
|
||||||
|
for (type_info.@"struct".fields) |field| {
|
||||||
|
// Extract just what we need for parsing
|
||||||
|
const field_meta = if (metadata.hasFieldMeta(T, field.name))
|
||||||
|
metadata.getFieldMeta(T, field.name)
|
||||||
|
else
|
||||||
|
metadata.FieldMeta{};
|
||||||
|
|
||||||
|
const arg_name = if (field_meta.name) |custom| custom else field.name;
|
||||||
|
|
||||||
|
const arg_type = ArgumentType.fromZigType(field.type);
|
||||||
|
const short = field_meta.short;
|
||||||
|
|
||||||
|
const info = FieldInfo{
|
||||||
|
.arg_name = arg_name,
|
||||||
|
.arg_type = arg_type,
|
||||||
|
.short = short,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add long form
|
||||||
|
entries = entries ++ &[_]struct { []const u8, FieldInfo }{
|
||||||
|
.{ arg_name, info },
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add short form if present
|
||||||
|
if (short) |short_char| {
|
||||||
|
const short_str = &[_]u8{short_char};
|
||||||
|
entries = entries ++ &[_]struct { []const u8, FieldInfo }{
|
||||||
|
.{ short_str, info },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break :blk std.StaticStringMap(FieldInfo).initComptime(entries);
|
||||||
|
};
|
||||||
|
|
||||||
|
var i: usize = 1; // Skip program name
|
||||||
|
while (i < argv.len) : (i += 1) {
|
||||||
|
const arg = argv[i];
|
||||||
|
|
||||||
|
// Skip help flags (already handled by isHelpRequested)
|
||||||
|
if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse --flag, --flag=value formats
|
||||||
|
if (std.mem.startsWith(u8, arg, "--")) {
|
||||||
|
const long_arg = arg[2..];
|
||||||
|
|
||||||
|
// Check for --name=value format
|
||||||
|
if (std.mem.indexOf(u8, long_arg, "=")) |eq_idx| {
|
||||||
|
const name = long_arg[0..eq_idx];
|
||||||
|
const value = long_arg[eq_idx + 1 ..];
|
||||||
|
|
||||||
|
// Only process if this struct recognizes the argument
|
||||||
|
if (field_lookup.get(name)) |field_info| {
|
||||||
|
try parseLongArgWithValue(&result, field_info.arg_name, field_info.arg_type, value, arena);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// --name format - might be boolean flag or take next arg as value
|
||||||
|
if (field_lookup.get(long_arg)) |field_info| {
|
||||||
|
if (field_info.arg_type == .bool) {
|
||||||
|
// Boolean flag - implicit true
|
||||||
|
const parsed = try ParsedValue.fromString(.bool, "true", arena);
|
||||||
|
try result.put(field_info.arg_name, parsed);
|
||||||
|
} else {
|
||||||
|
// Take next argument as value
|
||||||
|
if (i + 1 >= argv.len) return error.MissingArgumentValue;
|
||||||
|
i += 1;
|
||||||
|
const value = argv[i];
|
||||||
|
try parseLongArgWithValue(&result, field_info.arg_name, field_info.arg_type, value, arena);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Silently ignore unknown arguments (other modules may use them)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Short form: -x or -x value
|
||||||
|
else if (std.mem.startsWith(u8, arg, "-") and arg.len == 2) {
|
||||||
|
const short_char = arg[1];
|
||||||
|
const short_key = &[_]u8{short_char};
|
||||||
|
|
||||||
|
if (field_lookup.get(short_key)) |field_info| {
|
||||||
|
if (field_info.arg_type == .bool) {
|
||||||
|
// Boolean flag - implicit true
|
||||||
|
const parsed = try ParsedValue.fromString(.bool, "true", arena);
|
||||||
|
try result.put(field_info.arg_name, parsed);
|
||||||
|
} else {
|
||||||
|
// Take next argument as value
|
||||||
|
if (i + 1 >= argv.len) return error.MissingArgumentValue;
|
||||||
|
i += 1;
|
||||||
|
const value = argv[i];
|
||||||
|
|
||||||
|
const parsed = try ParsedValue.fromString(field_info.arg_type, value, arena);
|
||||||
|
try result.put(field_info.arg_name, parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Silently ignore unknown short flags
|
||||||
|
}
|
||||||
|
// Multi-flag short form: -abc (treat as -a -b -c)
|
||||||
|
else if (std.mem.startsWith(u8, arg, "-") and arg.len > 2) {
|
||||||
|
for (arg[1..]) |short_char| {
|
||||||
|
const short_key = &[_]u8{short_char};
|
||||||
|
|
||||||
|
if (field_lookup.get(short_key)) |field_info| {
|
||||||
|
// Multi-flag only works for boolean flags
|
||||||
|
if (field_info.arg_type != .bool) {
|
||||||
|
return error.InvalidArgumentFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = try ParsedValue.fromString(.bool, "true", arena);
|
||||||
|
try result.put(field_info.arg_name, parsed);
|
||||||
|
}
|
||||||
|
// Silently ignore unknown flags in multi-flag
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Ignore positional arguments (not supported by design)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a long argument with a value
|
||||||
|
/// All allocations use the arena allocator
|
||||||
|
fn parseLongArgWithValue(
|
||||||
|
result: *ParsedArguments,
|
||||||
|
arg_name: []const u8,
|
||||||
|
arg_type: ArgumentType,
|
||||||
|
value: []const u8,
|
||||||
|
arena: std.mem.Allocator,
|
||||||
|
) !void {
|
||||||
|
// Handle list types - support both comma-separated and repeated arguments
|
||||||
|
if (arg_type == .string_list) {
|
||||||
|
// Check if we already have a value for this argument
|
||||||
|
const existing = result.get(arg_name);
|
||||||
|
|
||||||
|
if (existing) |prev| {
|
||||||
|
// Append to existing list
|
||||||
|
var new_list = std.ArrayListUnmanaged([]const u8){};
|
||||||
|
defer new_list.deinit(arena);
|
||||||
|
|
||||||
|
// Add previous values (reuse the string pointers - arena owns them)
|
||||||
|
for (prev.string_list) |str| {
|
||||||
|
try new_list.append(arena, str);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse and add new values (comma-separated)
|
||||||
|
var iter = std.mem.splitSequence(u8, value, ",");
|
||||||
|
while (iter.next()) |item| {
|
||||||
|
const trimmed = std.mem.trim(u8, item, " \t");
|
||||||
|
const duped = try arena.dupe(u8, trimmed);
|
||||||
|
try new_list.append(arena, duped);
|
||||||
|
}
|
||||||
|
|
||||||
|
const final_list = try new_list.toOwnedSlice(arena);
|
||||||
|
|
||||||
|
// No need to free old array - arena owns it
|
||||||
|
|
||||||
|
// Put the new value
|
||||||
|
const parsed = ParsedValue{ .string_list = final_list };
|
||||||
|
try result.values.put(arg_name, parsed);
|
||||||
|
} else {
|
||||||
|
// First occurrence - parse comma-separated values
|
||||||
|
var list = std.ArrayListUnmanaged([]const u8){};
|
||||||
|
defer list.deinit(arena);
|
||||||
|
|
||||||
|
var iter = std.mem.splitSequence(u8, value, ",");
|
||||||
|
while (iter.next()) |item| {
|
||||||
|
const trimmed = std.mem.trim(u8, item, " \t");
|
||||||
|
const duped = try arena.dupe(u8, trimmed);
|
||||||
|
try list.append(arena, duped);
|
||||||
|
}
|
||||||
|
|
||||||
|
const final_list = try list.toOwnedSlice(arena);
|
||||||
|
const parsed = ParsedValue{ .string_list = final_list };
|
||||||
|
try result.put(arg_name, parsed);
|
||||||
|
}
|
||||||
|
} else if (arg_type == .enum_type) {
|
||||||
|
// For enum types, we need to store the string and let the populate function handle it
|
||||||
|
const duped_name = try arena.dupe(u8, value);
|
||||||
|
const parsed = ParsedValue{ .enum_type = .{ .name = duped_name, .value = 0 } };
|
||||||
|
try result.put(arg_name, parsed);
|
||||||
|
} else {
|
||||||
|
// Non-list type - just parse
|
||||||
|
const parsed = try ParsedValue.fromString(arg_type, value, arena);
|
||||||
|
try result.put(arg_name, parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Populate struct from parsed arguments
|
||||||
|
/// Strings are owned by the global arena and live until program exit
|
||||||
|
fn populateFromParsed(
|
||||||
|
comptime T: type,
|
||||||
|
parsed: ParsedArguments,
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
) !T {
|
||||||
|
_ = allocator; // Not used - arena owns all allocations
|
||||||
|
const type_info = @typeInfo(T);
|
||||||
|
var result: T = undefined;
|
||||||
|
|
||||||
|
inline for (type_info.@"struct".fields) |field| {
|
||||||
|
// Extract arg_name from metadata
|
||||||
|
const field_meta = if (metadata.hasFieldMeta(T, field.name))
|
||||||
|
metadata.getFieldMeta(T, field.name)
|
||||||
|
else
|
||||||
|
metadata.FieldMeta{};
|
||||||
|
|
||||||
|
const arg_name = if (field_meta.name) |custom| custom else field.name;
|
||||||
|
|
||||||
|
if (parsed.get(arg_name)) |value| {
|
||||||
|
// Special handling for enum types
|
||||||
|
const field_info = @typeInfo(field.type);
|
||||||
|
const is_optional = field_info == .optional;
|
||||||
|
const ActualType = if (is_optional) field_info.optional.child else field.type;
|
||||||
|
const actual_info = @typeInfo(ActualType);
|
||||||
|
|
||||||
|
if (actual_info == .@"enum") {
|
||||||
|
// Parse enum by name
|
||||||
|
const enum_name = value.enum_type.name;
|
||||||
|
inline for (actual_info.@"enum".fields) |enum_field| {
|
||||||
|
if (std.mem.eql(u8, enum_name, enum_field.name)) {
|
||||||
|
const enum_value = @field(ActualType, enum_field.name);
|
||||||
|
@field(result, field.name) = if (is_optional) enum_value else enum_value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return error.InvalidEnumValue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Convert to field type normally
|
||||||
|
@field(result, field.name) = value.toTypedValue(field.type);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Use default value
|
||||||
|
if (field.default_value_ptr) |default_ptr| {
|
||||||
|
const value_ptr: *const field.type = @ptrCast(@alignCast(default_ptr));
|
||||||
|
@field(result, field.name) = value_ptr.*;
|
||||||
|
} else {
|
||||||
|
// No default value and no parsed value
|
||||||
|
return error.MissingRequiredArgument;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse argv directly into a struct
|
||||||
|
/// This is the core parsing function - no registry needed
|
||||||
|
/// All string allocations live in a global arena until program exit
|
||||||
|
pub fn parse(
|
||||||
|
comptime T: type,
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
argv: []const [:0]const u8,
|
||||||
|
) !T {
|
||||||
|
// Check for --help / -h (caller handles help display)
|
||||||
|
if (isHelpRequested(argv)) {
|
||||||
|
return error.HelpRequested;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan argv and parse matching arguments
|
||||||
|
// All allocations go into the global arena
|
||||||
|
var parsed = try parseForStruct(T, argv, allocator);
|
||||||
|
defer parsed.deinit(); // Only deinits the HashMap, not the arena
|
||||||
|
|
||||||
|
// Populate struct with arena-allocated strings
|
||||||
|
return populateFromParsed(T, parsed, allocator);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,250 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const ArgumentType = @import("ArgumentType.zig").ArgumentType;
|
||||||
|
const ParsedValue = @import("ArgumentType.zig").ParsedValue;
|
||||||
|
const metadata = @import("metadata.zig");
|
||||||
|
const ArgumentRegistry = @import("ArgumentRegistry.zig").ArgumentRegistry;
|
||||||
|
|
||||||
|
/// Result of parsing a single argument
|
||||||
|
pub const ParseResult = struct {
|
||||||
|
arg_name: []const u8,
|
||||||
|
value: ParsedValue,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Parse argv and populate the registry with parsed values
|
||||||
|
/// Ignores unknown arguments (they may belong to modules not yet loaded)
|
||||||
|
pub fn parseArgv(registry: *ArgumentRegistry, argv: []const [:0]const u8) !void {
|
||||||
|
var i: usize = 1; // Skip program name
|
||||||
|
|
||||||
|
while (i < argv.len) : (i += 1) {
|
||||||
|
const arg = argv[i];
|
||||||
|
|
||||||
|
// Check for help flags
|
||||||
|
if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) {
|
||||||
|
registry.help_requested = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Long form: --name or --name=value
|
||||||
|
if (std.mem.startsWith(u8, arg, "--")) {
|
||||||
|
const long_arg = arg[2..];
|
||||||
|
|
||||||
|
// Check for --name=value format
|
||||||
|
if (std.mem.indexOf(u8, long_arg, "=")) |eq_idx| {
|
||||||
|
const name = long_arg[0..eq_idx];
|
||||||
|
const value = long_arg[eq_idx + 1 ..];
|
||||||
|
parseLongArgWithValue(registry, name, value) catch |err| {
|
||||||
|
// Ignore unknown arguments - they may belong to other modules
|
||||||
|
if (err == error.UnknownArgument) continue;
|
||||||
|
return err;
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// --name format - might be boolean flag or take next arg as value
|
||||||
|
const arg_meta = registry.getArgument(long_arg) orelse {
|
||||||
|
// Unknown argument - skip it
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (arg_meta.arg_type == .bool) {
|
||||||
|
// Boolean flag - implicit true
|
||||||
|
// Only store if not already parsed
|
||||||
|
if (registry.getParsedValue(arg_meta.arg_name) == null) {
|
||||||
|
const parsed = try ParsedValue.fromString(.bool, "true", registry.allocator);
|
||||||
|
try registry.storeParsedValue(arg_meta.arg_name, parsed);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Take next argument as value
|
||||||
|
if (i + 1 >= argv.len) {
|
||||||
|
// No value provided - skip this argument
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
const value = argv[i];
|
||||||
|
parseLongArgWithValue(registry, long_arg, value) catch |err| {
|
||||||
|
// Ignore unknown arguments
|
||||||
|
if (err == error.UnknownArgument) continue;
|
||||||
|
return err;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Short form: -x or -x value
|
||||||
|
else if (std.mem.startsWith(u8, arg, "-") and arg.len == 2) {
|
||||||
|
const short_char = arg[1];
|
||||||
|
const short_key = &[_]u8{short_char};
|
||||||
|
|
||||||
|
const arg_meta = registry.getArgument(short_key) orelse {
|
||||||
|
// Unknown argument - skip it
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (arg_meta.arg_type == .bool) {
|
||||||
|
// Boolean flag - implicit true
|
||||||
|
// Only store if not already parsed
|
||||||
|
if (registry.getParsedValue(arg_meta.arg_name) == null) {
|
||||||
|
const parsed = try ParsedValue.fromString(.bool, "true", registry.allocator);
|
||||||
|
try registry.storeParsedValue(arg_meta.arg_name, parsed);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Take next argument as value
|
||||||
|
if (i + 1 >= argv.len) {
|
||||||
|
// No value provided - skip this argument
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
const value = argv[i];
|
||||||
|
|
||||||
|
// Use parseLongArgWithValue which handles all types including lists and enums
|
||||||
|
parseLongArgWithValue(registry, short_key, value) catch |err| {
|
||||||
|
// Ignore unknown arguments
|
||||||
|
if (err == error.UnknownArgument) continue;
|
||||||
|
return err;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Multi-flag short form: -abc (treat as -a -b -c)
|
||||||
|
else if (std.mem.startsWith(u8, arg, "-") and arg.len > 2) {
|
||||||
|
for (arg[1..]) |short_char| {
|
||||||
|
const short_key = &[_]u8{short_char};
|
||||||
|
const arg_meta = registry.getArgument(short_key) orelse {
|
||||||
|
// Unknown argument - skip it
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Multi-flag only works for boolean flags
|
||||||
|
if (arg_meta.arg_type != .bool) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = try ParsedValue.fromString(.bool, "true", registry.allocator);
|
||||||
|
try registry.storeParsedValue(arg_meta.arg_name, parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Positional arguments not supported - just ignore them
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a long argument with a value
|
||||||
|
fn parseLongArgWithValue(registry: *ArgumentRegistry, name: []const u8, value: []const u8) !void {
|
||||||
|
const arg_meta = registry.getArgument(name) orelse return error.UnknownArgument;
|
||||||
|
|
||||||
|
// For non-list types, skip if already parsed (happens in multi-module scenarios)
|
||||||
|
// For list types, we allow appending within the same parse pass
|
||||||
|
const existing = registry.getParsedValue(arg_meta.arg_name);
|
||||||
|
if (arg_meta.arg_type != .string_list and existing != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle list types - support both comma-separated and repeated arguments
|
||||||
|
if (arg_meta.arg_type == .string_list) {
|
||||||
|
if (existing) |prev| {
|
||||||
|
// Append to existing list
|
||||||
|
var new_list = std.ArrayListUnmanaged([]const u8){};
|
||||||
|
defer new_list.deinit(registry.allocator);
|
||||||
|
|
||||||
|
// Add previous values (reuse the string pointers)
|
||||||
|
for (prev.string_list) |str| {
|
||||||
|
try new_list.append(registry.allocator, str);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse and add new values (comma-separated)
|
||||||
|
var iter = std.mem.splitSequence(u8, value, ",");
|
||||||
|
while (iter.next()) |item| {
|
||||||
|
const trimmed = std.mem.trim(u8, item, " \t");
|
||||||
|
const duped = try registry.allocator.dupe(u8, trimmed);
|
||||||
|
try new_list.append(registry.allocator, duped);
|
||||||
|
}
|
||||||
|
|
||||||
|
const final_list = try new_list.toOwnedSlice(registry.allocator);
|
||||||
|
|
||||||
|
// Free only the old array, not the strings (we reused them)
|
||||||
|
registry.allocator.free(prev.string_list);
|
||||||
|
|
||||||
|
// Put the new value directly (don't use storeParsedValue to avoid double-free)
|
||||||
|
const parsed = ParsedValue{ .string_list = final_list };
|
||||||
|
try registry.parsed_values.put(arg_meta.arg_name, parsed);
|
||||||
|
} else {
|
||||||
|
// First occurrence - parse comma-separated values
|
||||||
|
var list = std.ArrayListUnmanaged([]const u8){};
|
||||||
|
defer list.deinit(registry.allocator);
|
||||||
|
|
||||||
|
var iter = std.mem.splitSequence(u8, value, ",");
|
||||||
|
while (iter.next()) |item| {
|
||||||
|
const trimmed = std.mem.trim(u8, item, " \t");
|
||||||
|
const duped = try registry.allocator.dupe(u8, trimmed);
|
||||||
|
try list.append(registry.allocator, duped);
|
||||||
|
}
|
||||||
|
|
||||||
|
const final_list = try list.toOwnedSlice(registry.allocator);
|
||||||
|
const parsed = ParsedValue{ .string_list = final_list };
|
||||||
|
try registry.storeParsedValue(arg_meta.arg_name, parsed);
|
||||||
|
}
|
||||||
|
} else if (arg_meta.arg_type == .enum_type) {
|
||||||
|
// For enum types, we need to store the string and let the populate function handle it
|
||||||
|
const duped_name = try registry.allocator.dupe(u8, value);
|
||||||
|
const parsed = ParsedValue{ .enum_type = .{ .name = duped_name, .value = 0 } };
|
||||||
|
try registry.storeParsedValue(arg_meta.arg_name, parsed);
|
||||||
|
} else {
|
||||||
|
// Non-list type - just parse
|
||||||
|
const parsed = try ParsedValue.fromString(arg_meta.arg_type, value, registry.allocator);
|
||||||
|
try registry.storeParsedValue(arg_meta.arg_name, parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Populate a struct with parsed values
|
||||||
|
pub fn populateStruct(
|
||||||
|
comptime T: type,
|
||||||
|
registry: *const ArgumentRegistry,
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
) !T {
|
||||||
|
_ = allocator;
|
||||||
|
const type_info = @typeInfo(T);
|
||||||
|
if (type_info != .@"struct") {
|
||||||
|
@compileError("populateStruct requires a struct type");
|
||||||
|
}
|
||||||
|
|
||||||
|
var result: T = undefined;
|
||||||
|
|
||||||
|
inline for (type_info.@"struct".fields) |field| {
|
||||||
|
const field_meta = metadata.extractFieldMetadata(T, field);
|
||||||
|
|
||||||
|
// Try to get parsed value
|
||||||
|
if (registry.getParsedValue(field_meta.arg_name)) |parsed| {
|
||||||
|
// Special handling for enum types
|
||||||
|
const field_info = @typeInfo(field.type);
|
||||||
|
const is_optional = field_info == .optional;
|
||||||
|
const ActualType = if (is_optional) field_info.optional.child else field.type;
|
||||||
|
const actual_info = @typeInfo(ActualType);
|
||||||
|
|
||||||
|
if (actual_info == .@"enum") {
|
||||||
|
// Parse enum by name
|
||||||
|
const enum_name = parsed.enum_type.name;
|
||||||
|
inline for (actual_info.@"enum".fields) |enum_field| {
|
||||||
|
if (std.mem.eql(u8, enum_name, enum_field.name)) {
|
||||||
|
const enum_value = @field(ActualType, enum_field.name);
|
||||||
|
@field(result, field.name) = if (is_optional) enum_value else enum_value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return error.InvalidEnumValue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Convert to field type normally
|
||||||
|
@field(result, field.name) = parsed.toTypedValue(field.type);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Use default value
|
||||||
|
if (field.default_value_ptr) |default_ptr| {
|
||||||
|
const value_ptr: *const field.type = @ptrCast(@alignCast(default_ptr));
|
||||||
|
@field(result, field.name) = value_ptr.*;
|
||||||
|
} else {
|
||||||
|
// No default value and no parsed value
|
||||||
|
return error.MissingRequiredArgument;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,149 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
/// Convert a camelCase or snake_case identifier to kebab-case at compile time
|
||||||
|
/// Examples:
|
||||||
|
/// "verboseMode" -> "verbose-mode"
|
||||||
|
/// "output_file" -> "output-file"
|
||||||
|
/// "logLevel" -> "log-level"
|
||||||
|
/// "HTTPServer" -> "http-server"
|
||||||
|
/// Returns a comptime string literal that persists and can be used anywhere
|
||||||
|
pub fn toKebabCase(comptime name: []const u8) *const [kebabCaseLen(name):0]u8 {
|
||||||
|
comptime {
|
||||||
|
const len = kebabCaseLen(name);
|
||||||
|
var result: [len:0]u8 = undefined;
|
||||||
|
var result_len: usize = 0;
|
||||||
|
var prev_was_lower = false;
|
||||||
|
var prev_was_underscore = false;
|
||||||
|
|
||||||
|
for (name, 0..) |c, i| {
|
||||||
|
// Replace underscores with hyphens
|
||||||
|
if (c == '_') {
|
||||||
|
if (result_len > 0 and !prev_was_underscore) {
|
||||||
|
result[result_len] = '-';
|
||||||
|
result_len += 1;
|
||||||
|
}
|
||||||
|
prev_was_underscore = true;
|
||||||
|
prev_was_lower = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
prev_was_underscore = false;
|
||||||
|
|
||||||
|
// Add hyphen before uppercase letter if:
|
||||||
|
// 1. Not at the start
|
||||||
|
// 2. Previous char was lowercase (camelCase boundary)
|
||||||
|
// 3. OR next char is lowercase and current is uppercase (HTTPServer -> http-server)
|
||||||
|
if (std.ascii.isUpper(c)) {
|
||||||
|
const should_add_hyphen = result_len > 0 and (
|
||||||
|
prev_was_lower or
|
||||||
|
(i + 1 < name.len and std.ascii.isLower(name[i + 1]))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (should_add_hyphen) {
|
||||||
|
result[result_len] = '-';
|
||||||
|
result_len += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
result[result_len] = std.ascii.toLower(c);
|
||||||
|
result_len += 1;
|
||||||
|
prev_was_lower = false;
|
||||||
|
} else {
|
||||||
|
result[result_len] = c;
|
||||||
|
result_len += 1;
|
||||||
|
prev_was_lower = std.ascii.isLower(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result[result_len] = 0;
|
||||||
|
const final = result;
|
||||||
|
return &final;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculate the length needed for kebab-case version
|
||||||
|
fn kebabCaseLen(comptime name: []const u8) usize {
|
||||||
|
comptime {
|
||||||
|
if (name.len == 0) return 0;
|
||||||
|
|
||||||
|
var len: usize = 0;
|
||||||
|
var prev_was_lower = false;
|
||||||
|
var prev_was_underscore = false;
|
||||||
|
|
||||||
|
for (name, 0..) |c, i| {
|
||||||
|
if (c == '_') {
|
||||||
|
if (len > 0 and !prev_was_underscore) {
|
||||||
|
len += 1; // for hyphen
|
||||||
|
}
|
||||||
|
prev_was_underscore = true;
|
||||||
|
prev_was_lower = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
prev_was_underscore = false;
|
||||||
|
|
||||||
|
if (std.ascii.isUpper(c)) {
|
||||||
|
const should_add_hyphen = len > 0 and (
|
||||||
|
prev_was_lower or
|
||||||
|
(i + 1 < name.len and std.ascii.isLower(name[i + 1]))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (should_add_hyphen) {
|
||||||
|
len += 1; // for hyphen
|
||||||
|
}
|
||||||
|
|
||||||
|
len += 1; // for lowercase char
|
||||||
|
prev_was_lower = false;
|
||||||
|
} else {
|
||||||
|
len += 1;
|
||||||
|
prev_was_lower = std.ascii.isLower(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return len;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compile-time verification tests
|
||||||
|
comptime {
|
||||||
|
// Basic camelCase
|
||||||
|
const result1 = toKebabCase("verboseMode");
|
||||||
|
if (!std.mem.eql(u8, result1, "verbose-mode")) {
|
||||||
|
@compileError("toKebabCase failed: verboseMode");
|
||||||
|
}
|
||||||
|
|
||||||
|
// snake_case
|
||||||
|
const result2 = toKebabCase("output_file");
|
||||||
|
if (!std.mem.eql(u8, result2, "output-file")) {
|
||||||
|
@compileError("toKebabCase failed: output_file");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multiple uppercase (acronyms)
|
||||||
|
const result3 = toKebabCase("HTTPServer");
|
||||||
|
if (!std.mem.eql(u8, result3, "http-server")) {
|
||||||
|
@compileError("toKebabCase failed: HTTPServer");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Single word
|
||||||
|
const result4 = toKebabCase("verbose");
|
||||||
|
if (!std.mem.eql(u8, result4, "verbose")) {
|
||||||
|
@compileError("toKebabCase failed: verbose");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty string
|
||||||
|
const result5 = toKebabCase("");
|
||||||
|
if (!std.mem.eql(u8, result5, "")) {
|
||||||
|
@compileError("toKebabCase failed: empty string");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Already kebab-case
|
||||||
|
const result6 = toKebabCase("log-level");
|
||||||
|
if (!std.mem.eql(u8, result6, "log-level")) {
|
||||||
|
@compileError("toKebabCase failed: log-level");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mixed formats
|
||||||
|
const result7 = toKebabCase("parse_XMLFile");
|
||||||
|
if (!std.mem.eql(u8, result7, "parse-xml-file")) {
|
||||||
|
@compileError("toKebabCase failed: parse_XMLFile");
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue