This commit is contained in:
peterino2 2026-02-09 13:18:10 -08:00
commit 9997e65d71
15 changed files with 2993 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
.zig-cache
.zig-out

607
AGENTS.md Normal file
View File

@ -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

204
build.zig Normal file
View File

@ -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);
}
}

86
examples/README.md Normal file
View File

@ -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)

View File

@ -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", .{});
}

79
examples/multi_module.zig Normal file
View File

@ -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", .{});
}

302
src/ArgumentRegistry.zig Normal file
View File

@ -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();
}
};

213
src/ArgumentType.zig Normal file
View File

@ -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);
}

74
src/errors.zig Normal file
View File

@ -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,
};
}
};
}

198
src/help.zig Normal file
View File

@ -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);
}

48
src/main.zig Normal file
View File

@ -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");
}

331
src/metadata.zig Normal file
View File

@ -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,
};
}

354
src/parse.zig Normal file
View File

@ -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);
}

250
src/parsing.zig Normal file
View File

@ -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;
}

149
src/utils.zig Normal file
View File

@ -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");
}
}