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