all: start organizing into components and add user context support

The user can provide a context type and corresponding value that will
get passed into any executed callbacks. This allows for complex
behavior through side effects and provides a mechanism by which the
user can pass an allocator into argument handlers, etc.

There was also a lot of restructuring in this including a bit more
automagical behavior, like making parameters that wrap optional types
default to being optional. The start of automatic handler picking
(user overridable, of course) is in place as well.

Needing to specify the userdata context type makes things a bit more
verbose, and there's some other jank I'm interested in trying to
remove. I have some ideas, but I don't know how far I can go in my
abuse of the compiler.

However, this seems like it will be usable once I get around to writing
the help text generation.
This commit is contained in:
torque 2022-11-26 20:29:23 -08:00
parent e4b11a16ce
commit b1bac01257
No known key found for this signature in database
GPG Key ID: B365E9BF07B7886F
4 changed files with 383 additions and 244 deletions

View File

@ -1,60 +1,42 @@
const std = @import("std");
const noclip = @import("noclip");
const subData: noclip.CommandData = .{ .name = "subcommand", .help = "this a sub command\n" };
const context: []const u8 = "hello friend";
const ContextType = @TypeOf(context);
const subFlag = noclip.StringOption{
.name = "meta",
.short = "-m",
.handler = noclip.passthrough,
};
const subArg = noclip.StringArg{
.name = "sub",
.handler = noclip.passthrough,
};
const subSpec = .{ subFlag, subArg };
const subCommand = noclip.Command(subData, subSpec, void, subCallback);
const helpFlag = noclip.HelpFlag(.{ .UserContext = ContextType });
const cdata: noclip.CommandData = .{
.name = "main",
.help = "main CLI entry point\n",
};
const flagCheck = noclip.FlagOption{
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) = .{};
fn wrecker(zontext: ContextType, input: []const u8) ![]const u8 {
std.debug.print("ctx: {s}\n", .{zontext});
return input;
}
const cdata: noclip.CommandData = .{ .name = "main", .help = "main CLI entry point" };
const flagCheck: noclip.FlagOption(ContextType) = .{
.name = "flag",
.default = .{ .value = false },
.truthy = .{ .short = "-f", .long = "--flag" },
.falsy = .{ .long = "--no-flag" },
};
const inputOption = noclip.StringOption{
const inputOption: noclip.StringOption(ContextType) = .{
.name = "input",
.short = "-i",
.long = "--input",
.handler = wrecker,
.envVar = "OPTS_INPUT",
.handler = noclip.passthrough,
};
const outputOption = noclip.StringOption{
.name = "output",
.long = "--output",
.default = .{ .value = "waoh" },
.handler = noclip.passthrough,
};
const numberOption = noclip.ValuedOption(i32){
.name = "number",
.short = "-n",
.long = "--number",
.handler = noclip.intHandler(i32),
};
const argCheck = noclip.StringArg{
.name = "argument",
.handler = noclip.passthrough,
};
const argAgain = noclip.StringArg{
.name = "another",
.handler = noclip.passthrough,
};
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 = .{
noclip.defaultHelpFlag,
helpFlag,
flagCheck,
inputOption,
outputOption,
@ -64,14 +46,14 @@ const mainSpec = .{
subCommand,
};
pub fn subCallback(_: void, result: noclip.CommandResult(subSpec)) !void {
std.debug.print("subcommand {}!!!\n", .{result});
pub fn subCallback(_: ContextType, result: noclip.CommandResult(subSpec, ContextType)) !void {
std.debug.print("subcommand {any}!!!\n", .{result});
}
pub fn mainCommand(_: void, result: noclip.CommandResult(mainSpec)) !void {
pub fn mainCommand(_: ContextType, result: noclip.CommandResult(mainSpec, ContextType)) !void {
std.debug.print(
\\arguments: {{
\\ .flag = {}
\\ .flag = {any}
\\ .input = {s}
\\ .output = {s}
\\ .number = {d}
@ -92,7 +74,7 @@ pub fn mainCommand(_: void, result: noclip.CommandResult(mainSpec)) !void {
}
pub fn main() !void {
const command = noclip.Command(cdata, mainSpec, void, mainCommand);
var command: noclip.CommandParser(cdata, mainSpec, ContextType, mainCommand) = .{};
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
@ -101,5 +83,5 @@ pub fn main() !void {
var argit = try std.process.argsWithAllocator(allocator);
_ = argit.next();
try command.execute(allocator, std.process.ArgIterator, &argit, {});
try command.execute(allocator, std.process.ArgIterator, &argit, context);
}

38
source/handlers.zig Normal file
View File

@ -0,0 +1,38 @@
const std = @import("std");
const builtin = std.builtin;
const noclip = @import("./noclip.zig");
pub fn stringHandler(comptime UserContext: type) HandlerType(.{ .UserContext = UserContext, .Output = []const u8 }) {
return struct {
pub fn handler(_: UserContext, buf: []const u8) ![]const u8 {
return buf;
}
}.handler;
}
pub fn intHandler(comptime UserContext: type, comptime IntType: type) HandlerType(.{ .UserContext = UserContext, .Output = IntType }) {
return struct {
pub fn handler(_: UserContext, buf: []const u8) std.fmt.ParseIntError!IntType {
return try std.fmt.parseInt(IntType, buf, 0);
}
}.handler;
}
pub fn HandlerType(comptime args: noclip.ParameterArgs) type {
return *const fn (args.UserContext, []const u8) anyerror!args.Output;
}
pub fn getDefaultHandler(comptime args: noclip.ParameterArgs) ?HandlerType(args) {
switch (@typeInfo(args.Output)) {
.Optional => |info| return getDefaultHandler(.{ .Output = info.child, .UserContext = args.user }),
.Int => return intHandler(args.UserContext, args.Output),
.Pointer => |info| {
if (info.size == .Slice and info.child == u8) {
return stringHandler(args.UserContext);
}
return null;
},
else => return null,
}
}

109
source/meta.zig Normal file
View File

@ -0,0 +1,109 @@
const std = @import("std");
const StructField = std.builtin.Type.StructField;
/// Given a type and a struct literal of defaults to add, this function creates
/// a simulacrum type with additional defaults set on its fields.
///
/// This function cannot remove default values from fields, but it can add some
/// to fields that don't have them, and it can overwrite existing defaults
pub fn UpdateDefaults(comptime input: type, comptime defaults: anytype) type {
comptime {
const inputInfo = @typeInfo(input);
const fieldcount = switch (inputInfo) {
.Struct => |spec| {
if (spec.decls.len > 0) {
@compileError("UpdateDefaults only works on structs " ++
"without decls due to limitations in @Type.");
}
break spec.fields.len;
},
else => @compileError("can only add default value to struct type"),
};
var fields: [fieldcount]StructField = undefined;
for (inputInfo.Struct.fields) |field, idx| {
fields[idx] = .{
.name = field.name,
.field_type = field.field_type,
// the cast ostensibly does type checking for us. It also makes
// setting null defaults work, and it converts comptime_int to
// the appropriate type, which is nice for ergonomics. Not sure
// if it introduces weird edge cases. Probably it's fine?
.default_value = if (@hasField(@TypeOf(defaults), field.name))
@ptrCast(?*const anyopaque, &@as(field.field_type, @field(defaults, field.name)))
else
field.default_value,
.is_comptime = field.is_comptime,
.alignment = field.alignment,
};
}
return @Type(.{ .Struct = .{
.layout = inputInfo.Struct.layout,
.backing_integer = inputInfo.Struct.backing_integer,
.fields = &fields,
.decls = inputInfo.Struct.decls,
.is_tuple = inputInfo.Struct.is_tuple,
} });
}
}
test "add basic default" {
const Base = struct { a: u8 };
const Defaulted = UpdateDefaults(Base, .{ .a = 4 });
const value = Defaulted{};
try std.testing.expectEqual(@as(u8, 4), value.a);
}
test "overwrite basic default" {
const Base = struct { a: u8 = 0 };
const Defaulted = UpdateDefaults(Base, .{ .a = 1 });
const value = Defaulted{};
try std.testing.expectEqual(@as(u8, 1), value.a);
}
test "add string default" {
const Base = struct { a: []const u8 };
const Defaulted = UpdateDefaults(Base, .{ .a = "hello" });
const value = Defaulted{};
try std.testing.expectEqual(@as([]const u8, "hello"), value.a);
}
test "add null default" {
const Base = struct { a: ?u8 };
const Defaulted = UpdateDefaults(Base, .{ .a = null });
const value = Defaulted{};
try std.testing.expectEqual(@as(?u8, null), value.a);
}
test "add enum default" {
const Options = enum { good, bad };
const Base = struct { a: Options };
const Defaulted = UpdateDefaults(Base, .{ .a = .good });
const value = Defaulted{};
try std.testing.expectEqual(Options.good, value.a);
}
test "preserve existing default" {
const Base = struct { a: ?u8 = 2, b: u8 };
const Defaulted = UpdateDefaults(Base, .{ .b = 3 });
const value = Defaulted{};
try std.testing.expectEqual(@as(?u8, 2), value.a);
try std.testing.expectEqual(@as(?u8, 3), value.b);
}
test "add multiple defaults" {
const Base = struct { a: u8, b: i8, c: ?u8 };
const Defaulted = UpdateDefaults(Base, .{ .a = 3, .c = 2 });
const value = Defaulted{ .b = -1 };
try std.testing.expectEqual(@as(u8, 3), value.a);
try std.testing.expectEqual(@as(i8, -1), value.b);
try std.testing.expectEqual(@as(?u8, 2), value.c);
}

View File

@ -14,6 +14,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,
@ -22,34 +24,6 @@ const Brand = enum {
Command,
};
pub fn noopHandleGen(comptime ResultType: type) *const fn (buf: []const u8) anyerror!ResultType {
return struct {
pub fn handler(input: []const u8) anyerror!ResultType {
return input;
}
}.handler;
}
pub const noopHandler = noopHandleGen([]const u8);
pub const passthrough = noopHandler;
const noOptHandler = noopHandleGen(?[]const u8);
pub fn intHandler(comptime intType: type) *const fn (buf: []const u8) std.fmt.ParseIntError!intType {
return struct {
pub fn handler(buf: []const u8) std.fmt.ParseIntError!intType {
return try std.fmt.parseInt(intType, buf, 0);
}
}.handler;
}
pub fn intRadixHandler(comptime intType: type, radix: u8) *const fn (buf: []const u8) std.fmt.ParseIntError!intType {
return struct {
pub fn handler(buf: []const u8) std.fmt.ParseIntError!intType {
return try std.fmt.parseInt(intType, buf, radix);
}
}.handler;
}
pub const OptionError = error{
BadShortOption,
BadLongOption,
@ -59,102 +33,111 @@ pub const OptionError = error{
ExtraArguments,
};
pub const ArgCountCategory = enum {
None,
Some,
Many,
};
pub const ArgCount = union(ArgCountCategory) {
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 fn ValuedOption(comptime resultType: type) type {
return struct {
pub fn brand(_: @This()) Brand {
return .Option;
}
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,
// this fake optional is a workaround for a bug in the stage1 compiler
// (it doesn't handle nested optionals in struct fields correctly) and
// should be replaced with proper optionals as soon as stage2 is
// functional.
default: union(enum) { none: void, value: resultType } = .none,
// this is a combination conversion/validation callback.
// Should we try to pass a user context? Zig's bound functions
// don't seem to coerce nicely to this type, probably because
// they're no longer just a pointer. Any nontrivial type may need an
// allocator context passed.
handler: *const fn (input: []const u8) anyerror!resultType,
// 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,
eager: 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 ResultType(comptime _: @This()) type {
return resultType;
}
pub fn required(self: @This()) bool {
return self.default == .none;
fn required(self: @This()) bool {
return !@TypeOf(self).mayBeOptional and self.default == null;
}
};
return result;
}
pub const StringOption = ValuedOption([]const u8);
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. It could be lowered into a pair of
// ValuedOption(bool) though, if consuming a value became optional.
// 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,
};
pub const FlagOption = struct {
pub fn brand(_: @This()) Brand {
return .Flag;
}
// 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: union(enum) { none: void, value: bool } = .{ .value = false },
truthy: ShortLong = .{},
falsy: ShortLong = .{},
help: ?[]const u8 = null,
// should envVar be split into truthy/falsy the way the args are? otherwise
// we probably need to peek the value of the environmental variable to see
// if it is truthy or falsy. Honestly, looking at the value is probably
// required to avoid violating the principle of least astonishment because
// otherwise you can get `MY_VAR=false` causing `true` to be emitted, which
// looks and feels bad. But then we need to establish a truthiness baseline.
// case insensitive true/false is easy. What about yes/no? 0/1 (or nonzero).
// How about empty strings? I'd base on how it reads, and `MY_VAR= prog`
// reads falsy to me.
envVar: ?[]const u8 = null,
hideResult: bool = false,
eager: ?*const fn (cmd: CommandData) anyerror!void = null,
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 ResultType(comptime _: @This()) type {
return bool;
}
pub fn required(self: @This()) bool {
return self.default == .none;
}
};
pub fn produceHelp(cmd: CommandData) !void {
std.debug.print("{s}", .{cmd.help});
std.process.exit(0);
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.
@ -169,49 +152,59 @@ const HelpFlagArgs = struct {
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 {
return FlagOption{
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,
.eager = produceHelp(args.UserContext),
};
}
// but this does, which is kind of silly.
pub const defaultHelpFlag = HelpFlag(.{});
pub fn Argument(comptime resultType: type) type {
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 fn brand(_: @This()) Brand {
return .Argument;
}
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: union(enum) { none: void, value: resultType } = .none,
handler: *const fn (input: []const u8) anyerror!resultType,
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 ResultType(comptime _: @This()) type {
return resultType;
}
pub fn required(self: @This()) bool {
return self.default == .none;
fn required(self: @This()) bool {
return !@TypeOf(self).mayBeOptional and self.default == null;
}
};
}
pub const StringArg = Argument([]const u8);
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
@ -219,50 +212,40 @@ pub const CommandData = struct {
};
/// spec is a tuple of ValuedOption, FlagOption, and Argument
pub fn Command(
pub fn CommandParser(
comptime commandData: CommandData,
comptime spec: anytype,
comptime UdType: type,
comptime callback: *const fn (userdata: UdType, res: CommandResult(spec)) anyerror!void,
comptime UserContext: type,
comptime callback: *const fn (UserContext, CommandResult(spec, UserContext)) anyerror!void,
) type {
comptime var argCount = 0;
comptime var requiredOptions = 0;
comptime for (spec) |param| {
switch (param.brand()) {
switch (@TypeOf(param).brand) {
.Argument => argCount += 1,
.Option, .Flag => if (param.required()) {
requiredOptions += 1;
},
.Command => continue,
.Option, .Flag, .Command => continue,
}
};
const ResultType = CommandResult(spec);
const ResultType = CommandResult(spec, UserContext);
const RequiredType = RequiredTracker(spec);
const ParseState = enum { Mixed, ForcedArgs };
return struct {
pub fn brand() Brand {
return .Command;
}
// copy happens at comptime
pub var data: CommandData = commandData;
pub const brand: Brand = .Command;
pub const ContextType = UserContext;
// this should be copied at compile time
var data: CommandData = commandData;
/// parse command line arguments from an iterator
pub fn execute(alloc: std.mem.Allocator, comptime argit_type: type, argit: *argit_type, userdata: UdType) !void {
// we could precompute some tuples that would simplify some of the later logic:
// tuple of eager Options/Flags
// tuple of non-eager Options/Flags
// tuple of Arguments
// tuple of Commands
pub fn execute(self: @This(), alloc: std.mem.Allocator, comptime argit_type: type, argit: *argit_type, context: UserContext) !void {
try self.attachSubcommands(alloc);
var result: ResultType = createCommandresult();
var required: RequiredType = .{};
var parseState: ParseState = .Mixed;
try extractEnvVars(alloc, &result, &required);
try extractEnvVars(alloc, &result, &required, context);
var seenArgs: u32 = 0;
argloop: while (argit.next()) |arg| {
@ -287,10 +270,10 @@ pub fn Command(
if (arg[1] == '-') {
// we have a long flag or option
specloop: inline for (spec) |param| {
switch (comptime param.brand()) {
switch (@TypeOf(param).brand) {
.Option => {
// have to force lower the handler to runtime
var handler = param.handler;
// var handler = param.handler.?;
if (param.long) |flag| {
if (std.mem.eql(u8, flag, arg)) {
if (comptime param.required()) {
@ -299,7 +282,7 @@ pub fn Command(
const val = argit.next() orelse return OptionError.MissingArgument;
if (param.hideResult == false) {
@field(result, param.name) = try handler(val);
@field(result, param.name) = try param.handler.?(context, val);
}
continue :argloop;
}
@ -310,11 +293,7 @@ pub fn Command(
if (variant[0]) |flag| {
if (std.mem.eql(u8, flag, arg)) {
if (param.eager) |handler| {
try handler(data);
}
if (comptime param.required()) {
@field(required, param.name) = true;
try handler(context, data);
}
if (param.hideResult == false) {
@ -335,9 +314,9 @@ pub fn Command(
// we have a short flag, which may be multiple fused flags
shortloop: for (arg[1..]) |shorty, idx| {
specloop: inline for (spec) |param| {
switch (comptime param.brand()) {
switch (@TypeOf(param).brand) {
.Option => {
var handler = param.handler;
// var handler = param.handler.?;
if (param.short) |flag| {
if (flag[1] == shorty) {
if (comptime param.required()) {
@ -350,7 +329,7 @@ pub fn Command(
argit.next() orelse return OptionError.MissingArgument;
if (param.hideResult == false) {
@field(result, param.name) = try handler(val);
@field(result, param.name) = try param.handler.?(context, val);
}
continue :argloop;
}
@ -361,11 +340,7 @@ pub fn Command(
if (variant[0]) |flag| {
if (flag[1] == shorty) {
if (param.eager) |handler| {
try handler(data);
}
if (comptime param.required()) {
@field(required, param.name) = true;
try handler(context, data);
}
if (param.hideResult == false) {
@ -388,30 +363,33 @@ pub fn Command(
defer seenArgs += 1;
comptime var idx = 0;
inline for (spec) |param| {
switch (comptime param.brand()) {
switch (@TypeOf(param).brand) {
.Command => {
if (std.mem.eql(u8, @TypeOf(param).data.name, arg)) {
// we're calling a subcommand
try checkErrors(seenArgs, required);
try callback(context, result);
return param.execute(alloc, argit_type, argit, context);
}
},
.Argument => {
if (seenArgs == idx) {
var handler = param.handler;
@field(result, param.name) = try handler(arg);
if (comptime param.required()) {
@field(required, param.name) = true;
}
// var handler = param.handler;
@field(result, param.name) = try param.handler.?(context, arg);
continue :argloop;
}
idx += 1;
},
.Command => {
if (seenArgs == argCount and std.mem.eql(u8, param.data.name, arg)) {
// we're calling a subcommand
try checkErrors(seenArgs, required);
try callback(userdata, result);
return param.execute(alloc, argit_type, argit, userdata);
}
},
else => continue,
}
}
}
}
try checkErrors(seenArgs, required);
try callback(userdata, result);
try callback(context, result);
}
inline fn checkErrors(seenArgs: u32, required: RequiredType) OptionError!void {
@ -421,6 +399,8 @@ pub fn Command(
return OptionError.ExtraArguments;
}
describeError(required);
inline for (@typeInfo(@TypeOf(required)).Struct.fields) |field| {
if (@field(required, field.name) == false) {
return OptionError.MissingOption;
@ -428,15 +408,23 @@ pub fn Command(
}
}
fn attachSubcommands(alloc: std.mem.Allocator) !void {
pub fn describeError(required: RequiredType) void {
inline for (@typeInfo(@TypeOf(required)).Struct.fields) |field| {
if (@field(required, field.name) == false) {
std.debug.print("missing {s}\n", .{field.name});
}
}
}
fn attachSubcommands(_: @This(), alloc: std.mem.Allocator) !void {
if (data.subcommands == null) {
data.subcommands = std.ArrayList(*CommandData).init(alloc);
}
inline for (spec) |param| {
switch (comptime param.brand()) {
switch (@TypeOf(param).brand) {
.Command => {
try data.subcommands.append(&param);
try data.subcommands.?.append(&@TypeOf(param).data);
},
else => continue,
}
@ -468,31 +456,32 @@ pub fn Command(
return true;
}
fn extractEnvVars(alloc: std.mem.Allocator, result: *ResultType, required: *RequiredType) !void {
fn extractEnvVars(
alloc: std.mem.Allocator,
result: *ResultType,
required: *RequiredType,
context: UserContext,
) !void {
var env: std.process.EnvMap = try std.process.getEnvMap(alloc);
defer env.deinit();
inline for (spec) |param| {
switch (comptime param.brand()) {
const ParamType = @TypeOf(param);
switch (ParamType.brand) {
.Option => {
if (param.envVar) |want| {
if (env.get(want)) |value| {
if (comptime param.required()) {
if (param.required()) {
@field(required, param.name) = true;
}
var handler = param.handler;
@field(result, param.name) = try handler(value);
@field(result, param.name) = try param.handler.?(context, value);
}
}
},
.Flag => {
if (param.envVar) |want| {
if (env.get(want)) |value| {
if (comptime param.required()) {
@field(required, param.name) = true;
}
@field(result, param.name) = try scryTruthiness(alloc, value);
}
}
@ -505,13 +494,10 @@ pub fn Command(
inline fn createCommandresult() ResultType {
var result: ResultType = undefined;
inline for (spec) |param| {
switch (comptime param.brand()) {
switch (@TypeOf(param).brand) {
.Command => continue,
else => if (param.hideResult == false) {
@field(result, param.name) = switch (param.default) {
.none => continue,
.value => |val| val,
};
@field(result, param.name) = param.default orelse continue;
},
}
}
@ -520,16 +506,31 @@ pub fn Command(
};
}
pub fn CommandResult(comptime spec: anytype) type {
pub fn CommandResult(comptime spec: anytype, comptime UserContext: type) type {
comptime {
// not sure how to do this without iterating twice, so let's iterate
// twice
var outsize = 0;
for (spec) |param| {
switch (param.brand()) {
const ParamType = @TypeOf(param);
if (ParamType.ContextType != UserContext) {
@compileError("param \"" ++ param.name ++ "\" has wrong context type (wanted: " ++ @typeName(UserContext) ++ ", got: " ++ @typeName(ParamType.ContextType) ++ ")");
}
switch (ParamType.brand) {
.Argument, .Option => {
if (param.handler == null) {
@compileError("param \"" ++ param.name ++ "\" does not have a handler");
}
},
else => {},
}
switch (ParamType.brand) {
.Command => continue,
else => if (param.hideResult == false) {
outsize += 1;
else => {
if (param.hideResult == false) {
outsize += 1;
}
},
}
}
@ -538,22 +539,20 @@ pub fn CommandResult(comptime spec: anytype) type {
var idx = 0;
for (spec) |param| {
switch (param.brand()) {
const ParamType = @TypeOf(param);
switch (ParamType.brand) {
.Command => continue,
else => if (param.hideResult == true) continue,
}
const fieldType = param.ResultType();
const FieldType = ParamType.ResultType;
fields[idx] = .{
.name = param.name,
.field_type = fieldType,
.default_value = switch (param.default) {
.none => null,
.value => |val| @ptrCast(?*const anyopaque, &val),
},
.field_type = FieldType,
.default_value = @ptrCast(?*const anyopaque, &param.default),
.is_comptime = false,
.alignment = @alignOf(fieldType),
.alignment = @alignOf(FieldType),
};
idx += 1;
@ -574,10 +573,16 @@ fn RequiredTracker(comptime spec: anytype) type {
// twice
var outsize = 0;
for (spec) |param| {
switch (param.brand()) {
.Argument, .Command => continue,
else => {
if (param.required()) outsize += 1;
const ParamType = @TypeOf(param);
switch (ParamType.brand) {
// flags are always optional, and commands don't map into the
// output type.
.Flag, .Command => continue,
.Argument, .Option => if (param.required()) {
// if mayBeOptional is false, then the argument/option is
// required. Otherwise, we have to check if a default has
// been provided.
outsize += 1;
},
}
}
@ -586,9 +591,10 @@ fn RequiredTracker(comptime spec: anytype) type {
var idx = 0;
for (spec) |param| {
switch (param.brand()) {
.Argument, .Command => continue,
else => if (param.required()) {
const ParamType = @TypeOf(param);
switch (ParamType.brand) {
.Flag, .Command => continue,
.Argument, .Option => if (param.required()) {
fields[idx] = .{
.name = param.name,
.field_type = bool,
@ -610,3 +616,7 @@ fn RequiredTracker(comptime spec: anytype) type {
} });
}
}
test {
_ = meta;
}