303 lines
11 KiB
Zig
303 lines
11 KiB
Zig
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();
|
|
}
|
|
};
|