diff --git a/demo/demo.zig b/demo/demo.zig index 21adaa6..68f0017 100644 --- a/demo/demo.zig +++ b/demo/demo.zig @@ -4,53 +4,51 @@ const noclip = @import("noclip"); const context: []const u8 = "hello friend"; const ContextType = @TypeOf(context); -const helpFlag = noclip.HelpFlag(.{ .UserContext = ContextType }); +const subcommand = blk: { + var cmd = noclip.Command(ContextType, .{ .name = "subcommand", .help = "this a sub command" }); + cmd.add(cmd.defaultHelpFlag); + cmd.add(cmd.StringOption{ .name = "meta", .short = "-m" }); + cmd.add(cmd.StringArgument{ .name = "sub" }); + break :blk cmd; +}; -const subData: noclip.CommandData = .{ .name = "subcommand", .help = "this a sub command" }; -const subFlag: noclip.StringOption(ContextType) = .{ .name = "meta", .short = "-m" }; -const subArg: noclip.StringArg(ContextType) = .{ .name = "sub" }; -const subSpec = .{ helpFlag, subFlag, subArg }; -const subCommand: noclip.CommandParser(subData, subSpec, ContextType, subCallback) = .{}; +const command = blk: { + var cmd = noclip.Command(ContextType, .{ .name = "main", .help = "main CLI entry point" }); + cmd.add(cmd.Flag{ .name = "flag", .truthy = .{ .short = "-f", .long = "--flag" }, .falsy = .{ .long = "--no-flag" } }); + cmd.add(cmd.StringOption{ + .name = "input", + .short = "-i", + .long = "--input", + .handler = printHandler, + .envVar = "OPTS_INPUT", + }); + cmd.add(cmd.StringOption{ .name = "output", .long = "--output", .default = "waoh" }); + cmd.add(cmd.Option(i32){ .name = "number", .short = "-n", .long = "--number" }); + cmd.add(cmd.StringArgument{ .name = "argument" }); + cmd.add(cmd.Argument(u32){ .name = "another", .default = 0 }); -fn wrecker(zontext: ContextType, input: []const u8) ![]const u8 { - std.debug.print("ctx: {s}\n", .{zontext}); + cmd.add(subcommand.Parser(subCallback)); + break :blk cmd; +}; + +fn printHandler(ctx: ContextType, input: []const u8) ![]const u8 { + std.debug.print("ctx: {s}\n", .{ctx}); return input; } -const cdata: noclip.CommandData = .{ .name = "main", .help = "main CLI entry point" }; -const flagCheck: noclip.FlagOption(ContextType) = .{ - .name = "flag", - .truthy = .{ .short = "-f", .long = "--flag" }, - .falsy = .{ .long = "--no-flag" }, -}; -const inputOption: noclip.StringOption(ContextType) = .{ - .name = "input", - .short = "-i", - .long = "--input", - .handler = wrecker, - .envVar = "OPTS_INPUT", -}; -const outputOption: noclip.StringOption(ContextType) = .{ .name = "output", .long = "--output", .default = "waoh" }; -const numberOption: noclip.ValuedOption(.{ .Output = i32, .UserContext = ContextType }) = .{ .name = "number", .short = "-n", .long = "--number" }; -const argCheck: noclip.StringArg(ContextType) = .{ .name = "argument" }; -const argAgain: noclip.StringArg(ContextType) = .{ .name = "another", .default = "nope" }; - -const mainSpec = .{ - helpFlag, - flagCheck, - inputOption, - outputOption, - numberOption, - argCheck, - argAgain, - subCommand, -}; - -pub fn subCallback(_: ContextType, result: noclip.CommandResult(subSpec, ContextType)) !void { - std.debug.print("subcommand {any}!!!\n", .{result}); +pub fn subCallback(_: ContextType, result: subcommand.CommandResult()) !void { + std.debug.print( + \\subcommand: {{ + \\ .meta = {s} + \\ .sub = {s} + \\}} + \\ + , + .{ result.meta, result.sub }, + ); } -pub fn mainCommand(_: ContextType, result: noclip.CommandResult(mainSpec, ContextType)) !void { +pub fn mainCommand(_: ContextType, result: command.CommandResult()) !void { std.debug.print( \\arguments: {{ \\ .flag = {any} @@ -58,7 +56,7 @@ pub fn mainCommand(_: ContextType, result: noclip.CommandResult(mainSpec, Contex \\ .output = {s} \\ .number = {d} \\ .argument = {s} - \\ .another = {s} + \\ .another = {d} \\}} \\ , @@ -74,7 +72,7 @@ pub fn mainCommand(_: ContextType, result: noclip.CommandResult(mainSpec, Contex } pub fn main() !void { - var command: noclip.CommandParser(cdata, mainSpec, ContextType, mainCommand) = .{}; + var parser = command.Parser(mainCommand); var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); @@ -83,5 +81,5 @@ pub fn main() !void { var argit = try std.process.argsWithAllocator(allocator); _ = argit.next(); - try command.execute(allocator, std.process.ArgIterator, &argit, context); + try parser.execute(allocator, std.process.ArgIterator, &argit, context); } diff --git a/source/bakery.zig b/source/bakery.zig new file mode 100644 index 0000000..e507c12 --- /dev/null +++ b/source/bakery.zig @@ -0,0 +1,64 @@ +const meta = @import("./meta.zig"); +const params = @import("./params.zig"); +const noclip = @import("./noclip.zig"); + +fn GenCommand(comptime UserContext: type, comptime cData: params.CommandData) type { + return struct { + argspec: meta.MutableTuple = .{}, + + StringOption: type = params.Option(.{ .Output = []const u8, .UserContext = UserContext }), + StringArgument: type = params.Argument(.{ .Output = []const u8, .UserContext = UserContext }), + Flag: type = params.Flag(UserContext), + defaultHelpFlag: params.Flag(UserContext) = HelpFlag(.{}), + + // have to provide the first argument in order for these functions to be + // accessible from an instance, which is kind of annoying. + pub fn Option(comptime _: @This(), comptime Output: type) type { + return params.Option(.{ .Output = Output, .UserContext = UserContext }); + } + pub fn Argument(comptime _: @This(), comptime Output: type) type { + return params.Argument(.{ .Output = Output, .UserContext = UserContext }); + } + + pub fn HelpFlag(comptime args: params.HelpFlagArgs) params.Flag(UserContext) { + return params.HelpFlag(UserContext, args); + } + + // This is really only sort of conditionally useful. It would be nice + // to add the Subcommand directly to the argspec, except what we + // actually have to have is the subcommand.Parser, and that can't be + // created until all of the options are attached to that command. I + // believe we could handle it with an `inline for` construct in the + // parser executor, but I'm not particularly convinced that those + // contortions provide a particularly real benefit. The main change + // would be specifying the subcommands after the main command, whereas + // in the current state of things, they're generally defined before the + // main command. + pub fn Subcommand(comptime subData: params.CommandData) GenCommand(UserContext, subData) { + return Command(UserContext, subData); + } + + pub fn add(comptime self: *@This(), comptime parameter: anytype) void { + self.argspec.add(parameter); + } + + pub fn commandSpec(comptime self: @This()) self.argspec.TupleType() { + return self.argspec.realTuple(); + } + + pub fn CommandResult(comptime self: @This()) type { + return noclip.CommandResult(self.commandSpec(), UserContext); + } + + pub fn Parser( + comptime self: @This(), + comptime callback: *const fn (UserContext, noclip.CommandResult(self.commandSpec(), UserContext)) anyerror!void, + ) noclip.CommandParser(cData, self.commandSpec(), UserContext, callback) { + return noclip.CommandParser(cData, self.commandSpec(), UserContext, callback){}; + } + }; +} + +pub fn Command(comptime UserContext: type, comptime cData: params.CommandData) GenCommand(UserContext, cData) { + return GenCommand(UserContext, cData){}; +} diff --git a/source/handlers.zig b/source/handlers.zig index c85612f..6aa5acf 100644 --- a/source/handlers.zig +++ b/source/handlers.zig @@ -1,7 +1,7 @@ const std = @import("std"); const builtin = std.builtin; -const noclip = @import("./noclip.zig"); +const params = @import("./params.zig"); pub fn stringHandler(comptime UserContext: type) HandlerType(.{ .UserContext = UserContext, .Output = []const u8 }) { return struct { @@ -19,11 +19,11 @@ pub fn intHandler(comptime UserContext: type, comptime IntType: type) HandlerTyp }.handler; } -pub fn HandlerType(comptime args: noclip.ParameterArgs) type { +pub fn HandlerType(comptime args: params.ParameterArgs) type { return *const fn (args.UserContext, []const u8) anyerror!args.Output; } -pub fn getDefaultHandler(comptime args: noclip.ParameterArgs) ?HandlerType(args) { +pub fn getDefaultHandler(comptime args: params.ParameterArgs) ?HandlerType(args) { switch (@typeInfo(args.Output)) { .Optional => |info| return getDefaultHandler(.{ .Output = info.child, .UserContext = args.user }), .Int => return intHandler(args.UserContext, args.Output), diff --git a/source/meta.zig b/source/meta.zig index 4c565f8..c2ba074 100644 --- a/source/meta.zig +++ b/source/meta.zig @@ -10,12 +10,12 @@ pub fn UpdateDefaults(comptime input: type, comptime defaults: anytype) type { comptime { const inputInfo = @typeInfo(input); const fieldcount = switch (inputInfo) { - .Struct => |spec| { + .Struct => |spec| blk: { if (spec.decls.len > 0) { @compileError("UpdateDefaults only works on structs " ++ "without decls due to limitations in @Type."); } - break spec.fields.len; + break :blk spec.fields.len; }, else => @compileError("can only add default value to struct type"), }; @@ -48,6 +48,58 @@ pub fn UpdateDefaults(comptime input: type, comptime defaults: anytype) type { } } +/// Stores type-erased pointers to items in comptime extensible data structures, +/// which allows e.g. assembling a tuple through multiple calls rather than all +/// at once. +pub const MutableTuple = struct { + pointers: []const *const anyopaque = &[0]*const anyopaque{}, + types: []const type = &[0]type{}, + + pub fn add(comptime self: *@This(), comptime item: anytype) void { + self.pointers = &(@as([self.pointers.len]*const anyopaque, self.pointers[0..self.pointers.len].*) ++ [1]*const anyopaque{@as(*const anyopaque, &item)}); + self.types = &(@as([self.types.len]type, self.types[0..self.types.len].*) ++ [1]type{@TypeOf(item)}); + } + + pub fn retrieve(comptime self: @This(), comptime index: comptime_int) self.types[index] { + return @ptrCast(*const self.types[index], @alignCast(@alignOf(*const self.types[index]), self.pointers[index])).*; + } + + pub fn realTuple(comptime self: @This()) self.TupleType() { + comptime { + var result: self.TupleType() = undefined; + var idx = 0; + while (idx < self.types.len) : (idx += 1) { + result[idx] = self.retrieve(idx); + } + return result; + } + } + + pub fn TupleType(comptime self: @This()) type { + comptime { + var fields: [self.types.len]StructField = undefined; + for (self.types) |Type, idx| { + var num_buf: [128]u8 = undefined; + fields[idx] = .{ + .name = std.fmt.bufPrint(&num_buf, "{d}", .{idx}) catch unreachable, + .field_type = Type, + .default_value = null, + // TODO: is this the right thing to do? + .is_comptime = false, + .alignment = if (@sizeOf(Type) > 0) @alignOf(Type) else 0, + }; + } + + return @Type(.{ .Struct = .{ + .layout = .Auto, + .fields = &fields, + .decls = &.{}, + .is_tuple = true, + } }); + } + } +}; + test "add basic default" { const Base = struct { a: u8 }; const Defaulted = UpdateDefaults(Base, .{ .a = 4 }); diff --git a/source/noclip.zig b/source/noclip.zig index 9824a81..086f650 100644 --- a/source/noclip.zig +++ b/source/noclip.zig @@ -15,14 +15,8 @@ const std = @import("std"); const StructField = std.builtin.Type.StructField; pub const meta = @import("./meta.zig"); -pub const handlers = @import("./handlers.zig"); - -const Brand = enum { - Option, - Flag, - Argument, - Command, -}; +pub const params = @import("./params.zig"); +pub const Command = @import("./bakery.zig").Command; pub const OptionError = error{ BadShortOption, @@ -33,187 +27,9 @@ pub const OptionError = error{ ExtraArguments, }; -pub const ArgCount = union(enum) { - // TODO: how is this different than .Some = 0? - None: void, - Some: u32, - // TODO: how is this meaningfully different than .Some = 2 ** 32 - 1? (it - // is unlikely anyone would specify 4 billion arguments on the command line, - // or that the command line would tolerate such a thing particularly well) - Many: void, -}; - -pub const ParameterArgs = struct { - Output: type, - UserContext: type, -}; - -pub fn ValuedOption(comptime args: ParameterArgs) type { - // We use a combination of the resultType and default value to decide if an - // option must be provided to the command line. The default is specified - // when the type is constructed, so we cannot definitively decide it here. - // It can be checked (along with the handler function) when constructing - // the CommandResult type and thus be reasonably compile-time checked. - - comptime var result = struct { - pub const brand: Brand = .Option; - pub const mayBeOptional: bool = switch (@typeInfo(args.Output)) { - .Optional => true, - else => false, - }; - pub const ResultType: type = args.Output; - pub const ContextType: type = args.UserContext; - - name: []const u8, - // Should this be unconditionally made an optional type? Adding an extra - // layer of optional here doesn't seem to give us any advantage that I - // can think of. An argument is optional if either mayBeOptional is true - // or default is not null. - default: (if (mayBeOptional) args.Output else ?args.Output) = null, - // this is optional so that null can be provided as a default if there's - // not a sane default handler that can be selected (or generated). The - // handler can never actually be null, so we'll check for that when - // creating CommandResult and cause a compileError there if the handler - // is null. That will allow us to force unwrap these safely in the - // parsing funcion. - handler: ?handlers.HandlerType(args) = handlers.getDefaultHandler(args), - short: ?*const [2]u8 = null, - long: ?[]const u8 = null, - help: ?[]const u8 = null, - - envVar: ?[]const u8 = null, - hideResult: bool = false, - - // TODO: for ArgCount.Some > 1 semantics: automatically wrap args.Output - // in an array? Eliminates the need for an allocator, but precludes - // memory management techniques that may be better. - args: ArgCount = .{ .Some = 1 }, - - fn required(self: @This()) bool { - return !@TypeOf(self).mayBeOptional and self.default == null; - } - }; - - return result; -} - -pub fn StringOption(comptime UserContext: type) type { - return ValuedOption(.{ .Output = []const u8, .UserContext = UserContext }); -} - -// this could be ValuedOption(bool) except it allows truthy/falsy flag variants -// and it doesn't want to parse a value. with some contortions, it could be -// lowered into a pair of ValuedOption(bool), if we allowed multiple different -// arguments to specify the same output field name. - -const ShortLong = struct { - short: ?*const [2]u8 = null, - long: ?[]const u8 = null, -}; - -// Flags don't have a conversion callback, -pub fn FlagOption(comptime UserContext: type) type { - return struct { - pub const brand: Brand = .Flag; - // TODO: it may in some cases be useful to distinguish if the flag has been - // entirely unspecified, but I can't think of any right now. - pub const ResultType: type = bool; - pub const ContextType: type = UserContext; - - name: []const u8, - default: bool = false, - truthy: ShortLong = .{}, - falsy: ShortLong = .{}, - help: ?[]const u8 = null, - envVar: ?[]const u8 = null, - hideResult: bool = false, - eager: ?*const fn (UserContext, CommandData) anyerror!void = null, - }; -} - -pub fn produceHelp(comptime UserContext: type) *const fn (UserContext, CommandData) anyerror!void { - return struct { - pub fn handler(_: UserContext, data: CommandData) !void { - std.debug.print("{s}\n", .{data.help}); - std.process.exit(0); - } - }.handler; -} - -// I haven't really figured out a way not to special case the help flag. -// Everything else assumes that it can be handled in a vacuum without worrying -// about intermediates (and must be so, as we don't have a deterministic order -// for assembling the result. We could make the parse order deterministic, but -// I suspect it would require increasing the parser complexity a fair amount). -// Flag types are created on the fly, so we can only actually hand pre-composed -// help text to whatever callback this provides. -const HelpFlagArgs = struct { - name: []const u8 = "help", - short: ?*const [2]u8 = "-h", - long: ?[]const u8 = "--help", - help: []const u8 = "print this help message", - UserContext: type, -}; - -// this doesn't work in situ, -pub fn HelpFlag(comptime args: HelpFlagArgs) FlagOption(args.UserContext) { - return FlagOption(args.UserContext){ - .name = args.name, - .truthy = .{ .short = args.short, .long = args.long }, - .help = args.help, - .hideResult = true, - .eager = produceHelp(args.UserContext), - }; -} - -// but this does, which is kind of silly. -pub const defaultHelpFlag = HelpFlag(.{}); - -pub fn Argument(comptime args: ParameterArgs) type { - // NOTE: optional arguments are kind of weird, since they're identified by - // the order they're specified on the command line rather than by a named - // flag. As long as the order is not violated, it's perfectly safe to omit - // them if the provided specification supplies a default value. - - return struct { - pub const brand: Brand = .Argument; - pub const mayBeOptional: bool = switch (@typeInfo(args.Output)) { - .Optional => true, - else => false, - }; - pub const ResultType: type = args.Output; - pub const ContextType: type = args.UserContext; - - name: []const u8, - default: (if (mayBeOptional) args.Output else ?args.Output) = null, - handler: ?handlers.HandlerType(args) = handlers.getDefaultHandler(args), - help: ?[]const u8 = null, - hideResult: bool = false, - // allow loading arguments from environmental variables? I don't think - // it's possible to come up with sane semantics for this. - - fn required(self: @This()) bool { - return !@TypeOf(self).mayBeOptional and self.default == null; - } - }; -} - -pub fn StringArg(comptime UserContext: type) type { - return Argument(.{ .Output = []const u8, .UserContext = UserContext }); -} - -pub const CommandData = struct { - pub const brand: Brand = .Command; - - name: []const u8, - help: []const u8 = "", - // cheesy way to allow deferred initialization of the subcommands - subcommands: ?std.ArrayList(*CommandData) = null, -}; - -/// spec is a tuple of ValuedOption, FlagOption, and Argument +/// spec is a tuple of Option, Flag, and Argument pub fn CommandParser( - comptime commandData: CommandData, + comptime commandData: params.CommandData, comptime spec: anytype, comptime UserContext: type, comptime callback: *const fn (UserContext, CommandResult(spec, UserContext)) anyerror!void, @@ -232,10 +48,10 @@ pub fn CommandParser( const ParseState = enum { Mixed, ForcedArgs }; return struct { - pub const brand: Brand = .Command; + pub const brand: params.Brand = .Command; pub const ContextType = UserContext; // this should be copied at compile time - var data: CommandData = commandData; + var data: params.CommandData = commandData; /// parse command line arguments from an iterator pub fn execute(self: @This(), alloc: std.mem.Allocator, comptime argit_type: type, argit: *argit_type, context: UserContext) !void { @@ -392,6 +208,10 @@ pub fn CommandParser( try callback(context, result); } + pub fn OutType() type { + return CommandResult(spec, UserContext); + } + inline fn checkErrors(seenArgs: u32, required: RequiredType) OptionError!void { if (seenArgs < argCount) { return OptionError.MissingArgument; @@ -418,7 +238,7 @@ pub fn CommandParser( fn attachSubcommands(_: @This(), alloc: std.mem.Allocator) !void { if (data.subcommands == null) { - data.subcommands = std.ArrayList(*CommandData).init(alloc); + data.subcommands = std.ArrayList(*params.CommandData).init(alloc); } inline for (spec) |param| { diff --git a/source/params.zig b/source/params.zig new file mode 100644 index 0000000..d174eca --- /dev/null +++ b/source/params.zig @@ -0,0 +1,184 @@ +const std = @import("std"); +const handlers = @import("./handlers.zig"); + +pub const Brand = enum { + Option, + Flag, + Argument, + Command, +}; + +pub const ArgCount = union(enum) { + // TODO: how is this different than .Some = 0? + None: void, + Some: u32, + // TODO: how is this meaningfully different than .Some = 2 ** 32 - 1? (it + // is unlikely anyone would specify 4 billion arguments on the command line, + // or that the command line would tolerate such a thing particularly well) + Many: void, +}; + +pub const ParameterArgs = struct { + Output: type, + UserContext: type, +}; + +pub fn Option(comptime args: ParameterArgs) type { + // We use a combination of the resultType and default value to decide if an + // option must be provided to the command line. The default is specified + // when the type is constructed, so we cannot definitively decide it here. + // It can be checked (along with the handler function) when constructing + // the CommandResult type and thus be reasonably compile-time checked. + + comptime var result = struct { + pub const brand: Brand = .Option; + pub const mayBeOptional: bool = switch (@typeInfo(args.Output)) { + .Optional => true, + else => false, + }; + pub const ResultType: type = args.Output; + pub const ContextType: type = args.UserContext; + + name: []const u8, + // Should this be unconditionally made an optional type? Adding an extra + // layer of optional here doesn't seem to give us any advantage that I + // can think of. An argument is optional if either mayBeOptional is true + // or default is not null. + default: (if (mayBeOptional) args.Output else ?args.Output) = null, + // this is optional so that null can be provided as a default if there's + // not a sane default handler that can be selected (or generated). The + // handler can never actually be null, so we'll check for that when + // creating CommandResult and cause a compileError there if the handler + // is null. That will allow us to force unwrap these safely in the + // parsing funcion. + handler: ?handlers.HandlerType(args) = handlers.getDefaultHandler(args), + short: ?*const [2]u8 = null, + long: ?[]const u8 = null, + help: ?[]const u8 = null, + + envVar: ?[]const u8 = null, + hideResult: bool = false, + + // TODO: for ArgCount.Some > 1 semantics: automatically wrap args.Output + // in an array? Eliminates the need for an allocator, but precludes + // memory management techniques that may be better. + args: ArgCount = .{ .Some = 1 }, + + pub fn required(self: @This()) bool { + return !@TypeOf(self).mayBeOptional and self.default == null; + } + }; + + return result; +} + +pub fn StringOption(comptime UserContext: type) type { + return Option(.{ .Output = []const u8, .UserContext = UserContext }); +} + +// this could be Option(bool) except it allows truthy/falsy flag variants +// and it doesn't want to parse a value. with some contortions, it could be +// lowered into a pair of Option(bool), if we allowed multiple different +// arguments to specify the same output field name. + +const ShortLong = struct { + short: ?*const [2]u8 = null, + long: ?[]const u8 = null, +}; + +// Flags don't have a conversion callback, +pub fn Flag(comptime UserContext: type) type { + return struct { + pub const brand: Brand = .Flag; + // TODO: it may in some cases be useful to distinguish if the flag has been + // entirely unspecified, but I can't think of any right now. + pub const ResultType: type = bool; + pub const ContextType: type = UserContext; + + name: []const u8, + default: bool = false, + truthy: ShortLong = .{}, + falsy: ShortLong = .{}, + help: ?[]const u8 = null, + envVar: ?[]const u8 = null, + hideResult: bool = false, + eager: ?*const fn (UserContext, CommandData) anyerror!void = null, + }; +} + +pub fn produceHelp(comptime UserContext: type) *const fn (UserContext, CommandData) anyerror!void { + return struct { + pub fn handler(_: UserContext, data: CommandData) !void { + std.debug.print("{s}\n", .{data.help}); + std.process.exit(0); + } + }.handler; +} + +// I haven't really figured out a way not to special case the help flag. +// Everything else assumes that it can be handled in a vacuum without worrying +// about intermediates (and must be so, as we don't have a deterministic order +// for assembling the result. We could make the parse order deterministic, but +// I suspect it would require increasing the parser complexity a fair amount). +// Flag types are created on the fly, so we can only actually hand pre-composed +// help text to whatever callback this provides. +pub const HelpFlagArgs = struct { + name: []const u8 = "help", + short: ?*const [2]u8 = "-h", + long: ?[]const u8 = "--help", + help: []const u8 = "print this help message", +}; + +pub fn HelpFlag(comptime UserContext: type, comptime args: HelpFlagArgs) Flag(UserContext) { + return Flag(UserContext){ + .name = args.name, + .truthy = .{ .short = args.short, .long = args.long }, + .help = args.help, + .hideResult = true, + .eager = produceHelp(UserContext), + }; +} + +pub const defaultHelpFlag = HelpFlag(.{}); + +pub fn Argument(comptime args: ParameterArgs) type { + // NOTE: optional arguments are kind of weird, since they're identified by + // the order they're specified on the command line rather than by a named + // flag. As long as the order is not violated, it's perfectly safe to omit + // them if the provided specification supplies a default value. + + return struct { + pub const brand: Brand = .Argument; + pub const mayBeOptional: bool = switch (@typeInfo(args.Output)) { + .Optional => true, + else => false, + }; + pub const ResultType: type = args.Output; + pub const ContextType: type = args.UserContext; + + name: []const u8, + default: (if (mayBeOptional) args.Output else ?args.Output) = null, + handler: ?handlers.HandlerType(args) = handlers.getDefaultHandler(args), + help: ?[]const u8 = null, + hideResult: bool = false, + // allow loading arguments from environmental variables? I don't think + // it's possible to come up with sane semantics for this. + + pub fn required(self: @This()) bool { + return !@TypeOf(self).mayBeOptional and self.default == null; + } + }; +} + +pub fn StringArg(comptime UserContext: type) type { + return Argument(.{ .Output = []const u8, .UserContext = UserContext }); +} + +pub const CommandData = struct { + pub const brand: Brand = .Command; + + name: []const u8, + help: []const u8 = "", + // cheesy way to allow deferred initialization of the subcommands + subcommands: ?std.ArrayList(*CommandData) = null, +};