From 2c0842f5d402548b93e9bb10de53fa294674367d Mon Sep 17 00:00:00 2001 From: torque Date: Tue, 28 Mar 2023 23:35:54 -0700 Subject: [PATCH] the wheels look like they're spinning because they are Had to refactor the multi-value parameter stuff to push some of the key information in to the generics structure, as they necessarily change the conversion function signature. Some code has gotten folded together by being a bit sloppier with inputs and outputs. This should put us well on our way to having functioning value conversion, which I think is the main major feature remaining besides help text generation. Hopefully I won't need to rewrite everything like this again. While this design seems to be on track to incorporate all of the main features I am interested in, it has been a lot of work to wrangle it around, and there is still a lot of work left before I can put a bow on it. --- source/converters.zig | 39 +- source/doodle.zig | 926 +++++++++++++++++++++++------------------- source/meta.zig | 6 + 3 files changed, 536 insertions(+), 435 deletions(-) diff --git a/source/converters.zig b/source/converters.zig index 357bca0..b658f78 100644 --- a/source/converters.zig +++ b/source/converters.zig @@ -8,11 +8,18 @@ pub const ConversionError = error{ }; pub fn ConverterSignature(comptime gen: ParameterGenerics) type { - return *const fn (gen.ContextType, []const u8) ConversionError!gen.ResultType(); + return *const fn (gen.UserContext, gen.IntermediateType()) ConversionError!gen.ConvertedType(); +} + +pub fn FlagConverterSignature(comptime UserContext: type, comptime multi: bool) type { + comptime if (multi) + return *const fn (UserContext, std.ArrayList([]const u8)) ConversionError!std.ArrayList(bool) + else + return *const fn (UserContext, []const u8) ConversionError!bool; } pub fn default_converter(comptime gen: ParameterGenerics) ?ConverterSignature(gen) { - return switch (@typeInfo(gen.ResultType())) { + return switch (@typeInfo(gen.OutputType)) { .Bool => flag_converter(gen), .Int => int_converter(gen), .Pointer => |info| if (info.size == .Slice and info.child == u8) @@ -24,9 +31,23 @@ pub fn default_converter(comptime gen: ParameterGenerics) ?ConverterSignature(ge }; } +// fn multi_converter(comptime gen: ParameterGenerics) ?ConverterSignature(gen) { +// const converter = default_converter(gen) orelse @compileError("no default converter"); + +// return struct { +// pub fn handler(_: UserContext, input: std.ArrayList([]const u8)) ConversionError!std.ArrayList(OutputType) { +// var output = std.ArrayList(OutputType).initCapacity(input.allocator, input.items.len) catch return ConversionError.BadValue; + +// for (input.items) |item| { +// output.appendAssumeCapacity() +// } +// } +// }.handler; +// } + fn flag_converter(comptime gen: ParameterGenerics) ConverterSignature(gen) { return struct { - pub fn handler(_: gen.ContextType, input: []const u8) ConversionError!bool { + pub fn handler(_: gen.UserContext, input: []const u8) ConversionError!bool { // treat an empty string as falsy if (input.len == 0) return false; @@ -46,29 +67,29 @@ fn flag_converter(comptime gen: ParameterGenerics) ConverterSignature(gen) { fn string_converter(comptime gen: ParameterGenerics) ConverterSignature(gen) { return struct { - pub fn handler(_: gen.ContextType, value: []const u8) ConversionError![]const u8 { + pub fn handler(_: gen.UserContext, value: []const u8) ConversionError![]const u8 { return value; } }.handler; } fn int_converter(comptime gen: ParameterGenerics) ConverterSignature(gen) { - const IntType = gen.ResultType(); + const IntType = gen.OutputType; comptime std.debug.assert(@typeInfo(IntType) == .Int); return struct { - pub fn handler(_: gen.ContextType, value: []const u8) ConversionError!IntType { + pub fn handler(_: gen.UserContext, value: []const u8) ConversionError!IntType { return std.fmt.parseInt(IntType, value, 0) catch return ConversionError.BadValue; } }.handler; } fn choice_converter(comptime gen: ParameterGenerics) ConverterSignature(gen) { - const EnumType = gen.ResultType(); + const EnumType = gen.OutputType; return struct { - pub fn handler(_: gen.ContextType, value: []const u8) ConversionError!EnumType { - return std.meta.stringToEnum(gen.ResultType(), value) orelse ConversionError.BadValue; + pub fn handler(_: gen.UserContext, value: []const u8) ConversionError!EnumType { + return std.meta.stringToEnum(gen.ConvertedType(), value) orelse ConversionError.BadValue; } }.handler; } diff --git a/source/doodle.zig b/source/doodle.zig index 13a352d..b308431 100644 --- a/source/doodle.zig +++ b/source/doodle.zig @@ -6,12 +6,6 @@ const ncmeta = @import("./meta.zig"); const ConverterSignature = converters.ConverterSignature; -const ParameterType = enum { - Nominal, - Ordinal, - Executable, -}; - const Errors = error{ BadConfiguration, MissingTag, @@ -23,20 +17,30 @@ const Errors = error{ const ParseError = error{ UnexpectedFailure, EmptyArgs, - ValueMissing, - UnexpectedValue, + MissingValue, + ExtraValue, FusedShortTagValueMissing, UnknownLongTagParameter, UnknownShortTagParameter, }; +const ParameterType = enum { + Nominal, + Ordinal, + Executable, +}; + // in theory, we could also have a flexible value count, which could be followed by // any number of fixed args and be well-defined. `mv` is a classic example // of this pattern. But putting that logic in the parser seems to add a lot of // complexity for little gain. The `mv` use case can be much more easily handled // with a greedy value and then splitting in the value handler. const ValueCount = union(enum) { + flag: void, + count: void, fixed: u32, + // variable value delimited by a character, e.g. `find -exec +` style + // delimited: []const u8 greedy: void, }; @@ -46,7 +50,7 @@ const FlagBias = enum { unbiased, pub fn string(comptime self: @This()) []const u8 { - return switch (self) { + return switch (comptime self) { .truthy => "true", .falsy => "false", else => @compileError("flag tag with unbiased bias?"), @@ -54,59 +58,88 @@ const FlagBias = enum { } }; -const OptionResult = union(enum) { - Value: type, - flag: FlagBias, -}; - pub const ParameterGenerics = struct { - ContextType: type = void, - result: OptionResult = .{ .Value = []const u8 }, + UserContext: type = void, + OutputType: type = void, param_type: ParameterType, + value_count: ValueCount, + /// allow this named parameter to be passed multiple times. + /// values will be appended when it is encountered. If false, only the + /// final encountered instance will be used. + multi: bool, - pub fn no_context(comptime self: @This()) bool { - return self.ContextType == void; + pub fn has_context(comptime self: @This()) bool { + return comptime self.UserContext != void; } pub fn is_flag(comptime self: @This()) bool { - return self.result == .flag; + return comptime switch (self.value_count) { + .flag, .count => true, + .fixed, .greedy => false, + }; } - pub fn ResultType(comptime self: @This()) type { - return switch (self.result) { - .Value => |res| res, + pub fn ConvertedType(comptime self: @This()) type { + // is this the correct way to collapse this? + return comptime if (self.multi and self.value_count != .count) + std.ArrayList(self.ReturnValue()) + else + self.ReturnValue(); + } + + pub fn IntermediateType(comptime self: @This()) type { + return comptime if (self.multi and self.value_count != .count) + std.ArrayList(self.IntermediateValue()) + else + self.IntermediateValue(); + } + + pub fn ReturnValue(comptime self: @This()) type { + return comptime switch (self.value_count) { .flag => bool, + .count => usize, + .fixed => |count| switch (count) { + 0 => @compileError("bad fixed-zero parameter"), + 1 => self.OutputType, + // it's actually impossible to use a list in the general case + // because the result may have varying types. A tuple would + // work, but cannot be iterated over without inline for. It may + // be worth adding a ".structured" value count for a type that + // consumes many inputs but produces a single output. It would + // be nice to parse a tag into a struct directly. For that use + // case, the output type must be decoupled from the input type. + else => self.OutputType, + }, + .greedy => std.ArrayList(self.OutputType), + }; + } + + pub fn IntermediateValue(comptime self: @This()) type { + return comptime switch (self.value_count) { + .flag => []const u8, + .count => usize, + .fixed => |count| switch (count) { + 0 => @compileError("bad fixed-zero parameter"), + 1 => []const u8, + else => std.ArrayList([]const u8), + }, + .greedy => return std.ArrayList([]const u8), + }; + } + + pub fn nonscalar(comptime self: @This()) bool { + return comptime switch (self.value_count) { + .flag, .count => false, + .fixed => |count| switch (count) { + 0 => @compileError("bad fixed-zero parameter"), + 1 => false, + else => true, + }, + .greedy => true, }; } }; -const ValuedGenericsBasis = struct { ContextType: type = void, Result: type }; -const FlagGenericsBasis = struct { ContextType: type = void, flag_bias: FlagBias = .truthy }; - -fn tag_generics(comptime basis: ValuedGenericsBasis) ParameterGenerics { - return ParameterGenerics{ - .ContextType = basis.ContextType, - .result = .{ .Value = basis.Result }, - .param_type = .Nominal, - }; -} - -fn flag_generics(comptime basis: FlagGenericsBasis) ParameterGenerics { - return ParameterGenerics{ - .ContextType = basis.ContextType, - .result = .{ .flag = basis.flag_bias }, - .param_type = .Nominal, - }; -} - -fn arg_generics(comptime basis: ValuedGenericsBasis) ParameterGenerics { - return ParameterGenerics{ - .ContextType = basis.ContextType, - .result = .{ .Value = basis.Result }, - .param_type = .Ordinal, - }; -} - fn OptionConfig(comptime generics: ParameterGenerics) type { return struct { name: []const u8, @@ -114,99 +147,103 @@ fn OptionConfig(comptime generics: ParameterGenerics) type { short_tag: ?[]const u8 = null, long_tag: ?[]const u8 = null, env_var: ?[]const u8 = null, - - value_count: ValueCount = if (generics.is_flag()) .{ .fixed = 0 } else .{ .fixed = 1 }, - default: ?generics.ResultType() = null, - converter: ?ConverterSignature(generics) = null, description: []const u8 = "", // description for output in help text + default: ?generics.OutputType = null, + converter: ?ConverterSignature(generics) = null, + eager: bool = false, required: bool = generics.param_type == .Ordinal, global: bool = false, - multi: bool = false, + + exposed: bool = true, + secret: bool = false, + nice_type_name: []const u8 = @typeName(generics.OutputType), + flag_bias: FlagBias = .unbiased, + }; +} + +fn FlagConfig(comptime generics: ParameterGenerics) type { + const ShortLongPair = struct { + short_tag: ?[]const u8 = null, + long_tag: ?[]const u8 = null, + }; + + return struct { + name: []const u8, + + truthy: ?ShortLongPair = null, + falsy: ?ShortLongPair = null, + env_var: ?[]const u8 = null, + description: []const u8 = "", + + default: ?bool = null, + converter: ?ConverterSignature(generics) = null, + + eager: bool = false, + required: bool = false, + global: bool = false, + exposed: bool = true, secret: bool = false, - nice_type_name: []const u8 = @typeName(generics.ResultType()), }; } fn OptionType(comptime generics: ParameterGenerics) type { return struct { + pub const G: ParameterGenerics = generics; pub const param_type: ParameterType = generics.param_type; pub const is_flag: bool = generics.is_flag(); - pub const flag_bias: FlagBias = if (is_flag) generics.result.flag else .unbiased; + pub const value_count: ValueCount = generics.value_count; + pub const multi: bool = generics.multi; name: []const u8, short_tag: ?[]const u8, long_tag: ?[]const u8, env_var: ?[]const u8, - /// description for output in help text description: []const u8, - default: ?generics.ResultType(), - converter: ConverterSignature(generics), - /// number of values this option wants to consume - value_count: ValueCount, + default: ?generics.OutputType, + converter: ConverterSignature(generics), /// the option converter will be run eagerly, before full command line /// validation. eager: bool, - /// the option cannot be omitted from the command line. required: bool, - /// this option is parsed in a pre-parsing pass that consumes it. It /// may be present anywhere on the command line. A different way to /// solve this problem is by using an environment variable. It must be /// a tagged option. global: bool, - /// allow this named parameter to be passed multiple times. - /// values will be appended when it is encountered. If false, only the - /// final encountered instance will be used. - multi: bool, + /// if false, do not expose the resulting value in the output type. /// the converter must have side effects for this option to do anything. exposed: bool, /// do not print help for this parameter secret: bool, - nice_type_name: []const u8, // friendly type name (string better than []const u8) + /// friendly type name ("string" is better than "[]const u8") + nice_type_name: []const u8, + /// internal field for handling flag value biasing. Do not overwrite unless you + /// want weird things to happen. + flag_bias: FlagBias, - pub fn ResultType(comptime self: @This()) type { - // is this the correct way to collapse this? - return comptime if (self.multi) - std.ArrayList(self._RType()) - else - self._RType(); - } - - inline fn _RType(comptime self: @This()) type { - comptime switch (self.value_count) { - .fixed => |count| { - return switch (count) { - 0, 1 => generics.ResultType(), - // TODO: use an ArrayList instead? it generalizes a bit better - // (i.e. can use the same codepath for multi-fixed and greedy) - else => [count]generics.ResultType(), - }; - }, - .greedy => return std.ArrayList(generics.ResultType()), - }; + pub fn IntermediateValue(comptime _: @This()) type { + return generics.IntermediateValue(); } }; } fn check_short(comptime short_tag: ?[]const u8) void { - if (short_tag) |short| { - if (short.len != 2 or short[0] != '-') @compileError("bad short tag" ++ short); - } + const short = comptime short_tag orelse return; + if (short.len != 2 or short[0] != '-') @compileError("bad short tag" ++ short); } fn check_long(comptime long_tag: ?[]const u8) void { - if (long_tag) |long| { - if (long.len < 3 or long[0] != '-' or long[1] != '-') @compileError("bad long tag" ++ long); - } + const long = comptime long_tag orelse return; + if (long.len < 3 or long[0] != '-' or long[1] != '-') @compileError("bad long tag" ++ long); } fn make_option(comptime generics: ParameterGenerics, comptime opts: OptionConfig(generics)) OptionType(generics) { @@ -227,223 +264,237 @@ fn make_option(comptime generics: ParameterGenerics, comptime opts: OptionConfig // and the OptionConfig, is the OptionConfig is just unvalidated parameters, // whereas the OptionType is an instance of an object that has been // validated. - const converter = opts.converter orelse converters.default_converter(generics) orelse { - @compileLog(opts); - @compileError("implement me"); - }; + const converter = opts.converter orelse + (converters.default_converter(generics) orelse @compileError("no converter provided for " ++ opts.name ++ "and no default exists")); return OptionType(generics){ .name = opts.name, + // .short_tag = opts.short_tag, .long_tag = opts.long_tag, .env_var = opts.env_var, + // .description = opts.description, .default = opts.default, .converter = converter, - .value_count = opts.value_count, + // .eager = opts.eager, .required = opts.required, - .multi = opts.multi, - .exposed = opts.exposed, .global = opts.global, + // + .exposed = opts.exposed, .secret = opts.secret, .nice_type_name = opts.nice_type_name, + .flag_bias = opts.flag_bias, }; } -fn make_argument(comptime generics: ParameterGenerics, comptime opts: OptionConfig(generics)) OptionType(generics) { - // TODO: it would technically be possible to support specification of - // ordered arguments through environmental variables, but it doesn't really - // make a lot of sense. The algorithm would consume the env var greedily - if (opts.short_tag != null or opts.long_tag != null or opts.env_var != null) { - @compileLog(opts); - @compileError("argument " ++ opts.name ++ " must not have a long or short tag or an env var"); +fn make_argument( + comptime generics: ParameterGenerics, + comptime opts: OptionConfig(generics), +) OptionType(generics) { + comptime { + if (opts.short_tag != null or opts.long_tag != null or opts.env_var != null) { + @compileError("argument " ++ opts.name ++ " must not have a long or short tag or an env var"); + } + + const converter = opts.converter orelse + (converters.default_converter(generics) orelse @compileError("no converter provided for " ++ opts.name ++ "and no default exists")); + + if (generics.multi == true) + @compileError("argument " ++ opts.name ++ " cannot be multi"); + + return OptionType(generics){ + .name = opts.name, + // + .short_tag = opts.short_tag, + .long_tag = opts.long_tag, + .env_var = opts.env_var, + // + .description = opts.description, + .default = opts.default, + .converter = converter, + // + .eager = opts.eager, + .required = opts.required, + .global = opts.global, + // + .exposed = opts.exposed, + .secret = opts.secret, + .nice_type_name = opts.nice_type_name, + .flag_bias = .unbiased, + }; } - - const converter = opts.converter orelse converters.default_converter(generics) orelse { - @compileLog(opts); - @compileError("implement me"); - }; - - if (opts.multi == true) @compileError("argument " ++ opts.name ++ " cannot be multi"); - - return OptionType(generics){ - .name = opts.name, - .short_tag = opts.short_tag, - .long_tag = opts.long_tag, - .env_var = opts.env_var, - .description = opts.description, - .default = opts.default, - .converter = converter, - .value_count = opts.value_count, - .eager = opts.eager, - .required = opts.required, - .multi = opts.multi, - .global = opts.global, - .exposed = opts.exposed, - .secret = opts.secret, - .nice_type_name = opts.nice_type_name, - }; } -const ShortLongPair = struct { - short_tag: ?[]const u8 = null, - long_tag: ?[]const u8 = null, -}; - -fn FlagBuilderArgs(comptime ContextType: type) type { +fn BuilderGenerics(comptime UserContext: type) type { return struct { - name: []const u8, - truthy: ?ShortLongPair = null, - falsy: ?ShortLongPair = null, - env_var: ?[]const u8 = null, - description: []const u8 = "", - - default: ?bool = null, - converter: ?ConverterSignature(flag_generics(.{ .ContextType = ContextType })) = null, - eager: bool = false, - required: bool = false, - global: bool = false, + OutputType: type = void, + value_count: ValueCount = .{ .fixed = 1 }, multi: bool = false, - exposed: bool = true, - secret: bool = false, + + pub fn arg_gen(comptime self: @This()) ParameterGenerics { + if (self.OutputType == void) @compileError("argument must have OutputType specified"); + if (self.value_count == .flag) @compileError("argument may not be a flag"); + if (self.value_count == .count) @compileError("argument may not be a count"); + + return ParameterGenerics{ + .UserContext = UserContext, + .OutputType = self.OutputType, + .param_type = .Ordinal, + .value_count = self.value_count, + .multi = false, + }; + } + + pub fn opt_gen(comptime self: @This()) ParameterGenerics { + if (self.OutputType == void) @compileError("option must have OutputType specified"); + if (self.value_count == .flag) @compileError("option may not be a flag"); + + return ParameterGenerics{ + .UserContext = UserContext, + .OutputType = self.OutputType, + .param_type = .Nominal, + .value_count = self.value_count, + .multi = self.multi, + }; + } + + pub fn count_gen(comptime _: @This()) ParameterGenerics { + return ParameterGenerics{ + .UserContext = UserContext, + .OutputType = usize, + .param_type = .Nominal, + .value_count = .count, + .multi = true, + }; + } + + pub fn flag_gen(comptime self: @This()) ParameterGenerics { + return ParameterGenerics{ + .UserContext = UserContext, + .OutputType = bool, + .param_type = .Nominal, + .value_count = .flag, + .multi = self.multi, + }; + } }; } -fn CommandBuilder(comptime ContextType: type) type { +fn CommandBuilder(comptime UserContext: type) type { return struct { param_spec: ncmeta.MutableTuple = .{}, - pub const UserContextType = ContextType; + pub const UserContextType = UserContext; pub fn add_argument( comptime self: *@This(), - comptime Result: type, - comptime args: OptionConfig(arg_generics(.{ .ContextType = ContextType, .Result = Result })), + comptime bgen: BuilderGenerics(UserContext), + comptime config: OptionConfig(bgen.arg_gen()), ) void { - self.param_spec.add(make_argument( - arg_generics(.{ .ContextType = ContextType, .Result = Result }), - args, - )); + self.param_spec.add(make_argument(bgen.arg_gen(), config)); } pub fn add_option( comptime self: *@This(), - comptime Result: type, - comptime args: OptionConfig(tag_generics(.{ .ContextType = ContextType, .Result = Result })), + comptime bgen: BuilderGenerics(UserContext), + comptime config: OptionConfig(bgen.opt_gen()), ) void { - const generics = tag_generics(.{ .ContextType = ContextType, .Result = Result }); - if (comptime args.value_count == .fixed and args.value_count.fixed == 0) { + if (comptime bgen.value_count == .fixed and bgen.value_count.fixed == 0) { @compileError( "please use add_flag rather than add_option to " ++ "create a 0-argument option", ); } - self.param_spec.add(make_option(generics, args)); + self.param_spec.add(make_option(bgen.opt_gen(), config)); } pub fn set_help_flag( comptime self: *@This(), - comptime args: OptionConfig(flag_generics(.{ .ContextType = ContextType, .flag_bias = .truthy })), + comptime bgen: BuilderGenerics(UserContext), + comptime config: FlagConfig(bgen.flag_gen()), ) void { _ = self; - _ = args; + _ = config; } pub fn add_flag( comptime self: *@This(), - comptime build_args: FlagBuilderArgs(ContextType), + comptime bgen: BuilderGenerics(UserContext), + comptime config: FlagConfig(bgen.flag_gen()), ) void { - if (build_args.truthy == null and build_args.falsy == null and build_args.env_var == null) { - @compileError( - "flag " ++ - build_args.name ++ - " must have at least one of truthy flags, falsy flags, or env_var flags", - ); - } - - if (build_args.truthy) |truthy_pair| { - if (truthy_pair.short_tag == null and truthy_pair.long_tag == null) { + comptime { + if (config.truthy == null and config.falsy == null and config.env_var == null) { @compileError( "flag " ++ - build_args.name ++ - " truthy pair must have at least short or long tags set", + config.name ++ + " must have at least one of truthy flags, falsy flags, or env_var flags", ); } - const generics = flag_generics(.{ .ContextType = ContextType, .flag_bias = .truthy }); - - const args = OptionConfig(generics){ - .name = build_args.name, - .short_tag = truthy_pair.short_tag, - .long_tag = truthy_pair.long_tag, - .env_var = null, - .description = build_args.description, - .value_count = .{ .fixed = 0 }, - .default = build_args.default, - .converter = build_args.converter, - .eager = build_args.eager, - .required = build_args.required, - .global = build_args.global, - .multi = build_args.multi, - .exposed = build_args.exposed, - .secret = build_args.secret, - }; - - self.param_spec.add(make_option(generics, args)); - } - - if (build_args.falsy) |falsy_pair| { - if (falsy_pair.short_tag == null and falsy_pair.long_tag == null) { - @compileError( - "flag " ++ - build_args.name ++ - " falsy pair must have at least short or long tags set", - ); - } - - const generics = flag_generics(.{ .ContextType = ContextType, .flag_bias = .falsy }); - const args = OptionConfig(generics){ - .name = build_args.name, - .short_tag = falsy_pair.short_tag, - .long_tag = falsy_pair.long_tag, - .env_var = null, - .description = build_args.description, - .value_count = .{ .fixed = 0 }, - .default = build_args.default, - .converter = build_args.converter, - .eager = build_args.eager, - .required = build_args.required, - .global = build_args.global, - .multi = build_args.multi, - .exposed = build_args.exposed, - .secret = build_args.secret, - }; - - self.param_spec.add(make_option(generics, args)); - } - - if (build_args.env_var) |env_var| { - const generics = flag_generics(.{ .ContextType = ContextType, .flag_bias = .unbiased }); - const args = OptionConfig(generics){ - .name = build_args.name, + const generics = bgen.flag_gen(); + var args = OptionConfig(generics){ + .name = config.name, + // .short_tag = null, .long_tag = null, - .env_var = env_var, - .description = build_args.description, - .value_count = .{ .fixed = 0 }, - .default = build_args.default, - .converter = build_args.converter, - .eager = build_args.eager, - .required = build_args.required, - .global = build_args.global, - .multi = build_args.multi, - .secret = build_args.secret, - .exposed = build_args.exposed, + .env_var = null, + // + .description = config.description, + .default = config.default, + .converter = config.converter, + // + .eager = config.eager, + .required = config.required, + .global = config.global, + // + .exposed = config.exposed, + .secret = config.secret, + .nice_type_name = "flag", }; - self.param_spec.add(make_option(generics, args)); + if (config.truthy) |truthy_pair| { + if (truthy_pair.short_tag == null and truthy_pair.long_tag == null) { + @compileError( + "flag " ++ + config.name ++ + " truthy pair must have at least short or long tags set", + ); + } + + args.short_tag = truthy_pair.short_tag; + args.long_tag = truthy_pair.long_tag; + args.flag_bias = .truthy; + + self.param_spec.add(make_option(generics, args)); + } + + if (config.falsy) |falsy_pair| { + if (falsy_pair.short_tag == null and falsy_pair.long_tag == null) { + @compileError( + "flag " ++ + config.name ++ + " falsy pair must have at least short or long tags set", + ); + } + + args.short_tag = falsy_pair.short_tag; + args.long_tag = falsy_pair.long_tag; + args.flag_bias = .falsy; + + self.param_spec.add(make_option(generics, args)); + } + + if (config.env_var) |env_var| { + args.short_tag = null; + args.long_tag = null; + args.env_var = env_var; + args.flag_bias = .unbiased; + + self.param_spec.add(make_option(generics, args)); + } } } @@ -452,7 +503,7 @@ fn CommandBuilder(comptime ContextType: type) type { } pub fn CallbackSignature(comptime self: @This()) type { - return *const fn (ContextType, self.Output()) anyerror!void; + return *const fn (*UserContext, self.Output()) anyerror!void; } pub fn Output(comptime self: @This()) type { @@ -471,17 +522,16 @@ fn CommandBuilder(comptime ContextType: type) type { const PType = @TypeOf(param); if (PType.is_flag) { var peek = idx + 1; - var bais_seen: [ncmeta.enum_length(FlagBias)]bool = [_]bool{false} ** ncmeta.enum_length(FlagBias); - bais_seen[@enumToInt(PType.flag_bias)] = true; + var bias_seen: [ncmeta.enum_length(FlagBias)]bool = [_]bool{false} ** ncmeta.enum_length(FlagBias); + bias_seen[@enumToInt(param.flag_bias)] = true; while (peek < spec.len) : (peek += 1) { const peek_param = spec[peek]; - const PeekType = @TypeOf(peek_param); - if (PeekType.is_flag and std.mem.eql(u8, param.name, peek_param.name)) { - if (bais_seen[@enumToInt(PeekType.flag_bias)] == true) { + if (@TypeOf(peek_param).is_flag and std.mem.eql(u8, param.name, peek_param.name)) { + if (bias_seen[@enumToInt(peek_param.flag_bias)] == true) { @compileError("redundant flag!!!! " ++ param.name); } else { - bais_seen[@enumToInt(PeekType.flag_bias)] = true; + bias_seen[@enumToInt(peek_param.flag_bias)] = true; } flag_skip += 1; } else { @@ -494,9 +544,9 @@ fn CommandBuilder(comptime ContextType: type) type { // the optional wrapper is an interesting idea for required // fields. I do not foresee this greatly increasing complexity here. const FieldType = if (param.required) - param.ResultType() + PType.G.ConvertedType() else - ?param.ResultType(); + ?PType.G.ConvertedType(); // the wacky comptime slice extension hack fields = &(@as([fields.len]StructField, fields[0..fields.len].*) ++ [1]StructField{.{ @@ -532,17 +582,16 @@ fn CommandBuilder(comptime ContextType: type) type { const PType = @TypeOf(param); if (PType.is_flag) { var peek = idx + 1; - var bais_seen: [ncmeta.enum_length(FlagBias)]bool = [_]bool{false} ** ncmeta.enum_length(FlagBias); - bais_seen[@enumToInt(PType.flag_bias)] = true; + var bias_seen: [ncmeta.enum_length(FlagBias)]bool = [_]bool{false} ** ncmeta.enum_length(FlagBias); + bias_seen[@enumToInt(param.flag_bias)] = true; while (peek < spec.len) : (peek += 1) { const peek_param = spec[peek]; - const PeekType = @TypeOf(peek_param); - if (PeekType.is_flag and std.mem.eql(u8, param.name, peek_param.name)) { - if (bais_seen[@enumToInt(PeekType.flag_bias)] == true) { + if (@TypeOf(peek_param).is_flag and std.mem.eql(u8, param.name, peek_param.name)) { + if (bias_seen[@enumToInt(peek_param.flag_bias)] == true) { @compileError("redundant flag!!!! " ++ param.name); } else { - bais_seen[@enumToInt(PeekType.flag_bias)] = true; + bias_seen[@enumToInt(peek_param.flag_bias)] = true; } flag_skip += 1; } else { @@ -551,24 +600,21 @@ fn CommandBuilder(comptime ContextType: type) type { } } - // This needs to be reconciled with options that take many - // arguments. We could make all of these be ArrayLists of string - // slices instead... but that makes the parsing code much more allocation heavy. - // The problem is essentially that `--long=multi,value` and `--long multi value` - // evaluate to a different number of strings for the same number of arguments. - - const FieldType = switch (param.value_count) { - .fixed => |val| switch (val) { - 0, 1 => []const u8, - else => std.ArrayList([]const u8), - }, - else => std.ArrayList([]const u8), - }; + const FieldType = if (PType.value_count == .count) + PType.G.IntermediateType() + else + ?PType.G.IntermediateType(); fields = &(@as([fields.len]StructField, fields[0..fields.len].*) ++ [1]StructField{.{ .name = param.name, - .type = ?FieldType, - .default_value = @ptrCast(?*const anyopaque, &@as(?[]const u8, null)), + .type = FieldType, + .default_value = @ptrCast( + ?*const anyopaque, + &@as( + FieldType, + if (PType.value_count == .count) 0 else null, + ), + ), .is_comptime = false, .alignment = @alignOf(?[]const u8), }}); @@ -593,35 +639,49 @@ fn CommandBuilder(comptime ContextType: type) type { }; } -fn push_unparsed_multi(comptime T: type, comptime field: []const u8, intermediate: *T, value: []const u8, alloc: std.mem.Allocator) !void { - if (@field(intermediate, field) == null) { - @field(intermediate, field) = std.ArrayList([]const u8).init(alloc); - } - - try @field(intermediate, field).?.append(value); -} - -fn push_unparsed_value(comptime T: type, comptime param: anytype, intermediate: *T, value: []const u8, alloc: std.mem.Allocator) ParseError!void { - switch (comptime param.value_count) { - .fixed => |val| switch (val) { - 0, 1 => @field(intermediate, param.name) = value, - else => push_unparsed_multi(T, param.name, intermediate, value, alloc) catch return ParseError.UnexpectedFailure, - }, - else => push_unparsed_multi(T, param.name, intermediate, value, alloc) catch return ParseError.UnexpectedFailure, - } -} - -fn ParserInterface(comptime ContextType: type) type { +// This is a slightly annoying hack to work around the fact that there's no way to +// provide a field value conditionally. +fn ParserInterfaceImpl(comptime Interface: type) type { return struct { - ctx: *anyopaque, - methods: *const Interface, + pub fn execute(self: Interface) anyerror!void { + return try self.methods.execute(self.ctx, self.context); + } + }; +} - const Interface = struct { - execute: *const fn (ctx: *anyopaque, context: ContextType) anyerror!void, +fn ParserInterface(comptime UserContext: type) type { + const InterfaceVtable = struct { + execute: *const fn (ctx: *anyopaque, context: *UserContext) anyerror!void, + }; + + // we can actually bind the user context object in the interface, since + // it is exclusively parameterized around the context type. + return if (@typeInfo(UserContext) == .Void) + struct { + ctx: *anyopaque, + context: *UserContext = @constCast(&void{}), + methods: *const InterfaceVtable, + + pub usingnamespace ParserInterfaceImpl(@This()); + } + else + struct { + ctx: *anyopaque, + context: *UserContext, + methods: *const InterfaceVtable, + + pub usingnamespace ParserInterfaceImpl(@This()); }; +} - pub fn execute(self: @This(), context: ContextType) anyerror!void { - return try self.methods.execute(self.ctx, context); +fn InterfaceGen(comptime ParserType: type, comptime UserContext: type) type { + return if (@typeInfo(UserContext) == .Void) struct { + pub fn interface(self: *ParserType) ParserInterface(UserContext) { + return .{ .ctx = self, .methods = &.{ .execute = ParserType.wrap_execute } }; + } + } else struct { + pub fn interface(self: *ParserType, context: *UserContext) ParserInterface(UserContext) { + return .{ .ctx = self, .context = context, .methods = &.{ .execute = ParserType.wrap_execute } }; } }; } @@ -629,11 +689,11 @@ fn ParserInterface(comptime ContextType: type) type { // the parser is generated by the bind method of the CommandBuilder, so we can // be extremely type-sloppy here, which simplifies the signature. fn Parser(comptime command: anytype, comptime callback: anytype) type { - return struct { - const ContextType = @TypeOf(command).UserContextType; - const Intermediate = command.Intermediate(); - const Output = command.Output(); + const UserContext = @TypeOf(command).UserContextType; + const Intermediate = command.Intermediate(); + const Output = command.Output(); + return struct { intermediate: Intermediate = .{}, output: Output = undefined, consumed_args: u32 = 0, @@ -645,16 +705,16 @@ fn Parser(comptime command: anytype, comptime callback: anytype) type { // self.subcommands // } - pub fn interface(self: *@This()) ParserInterface(ContextType) { - return .{ .ctx = self, .methods = &.{ .execute = wrap_execute } }; - } + // This is a slightly annoying hack to work around the fact that there's no way to + // provide a method signature conditionally. + pub usingnamespace InterfaceGen(@This(), UserContext); - fn wrap_execute(ctx: *anyopaque, context: ContextType) anyerror!void { + fn wrap_execute(ctx: *anyopaque, context: *UserContext) anyerror!void { const self = @ptrCast(*@This(), @alignCast(@alignOf(@This()), ctx)); return try self.execute(context); } - pub fn execute(self: *@This(), context: ContextType) anyerror!void { + pub fn execute(self: *@This(), context: *UserContext) anyerror!void { const args = try std.process.argsAlloc(self.allocator); defer std.process.argsFree(self.allocator, args); var env = try std.process.getEnvMap(self.allocator); @@ -673,20 +733,27 @@ fn Parser(comptime command: anytype, comptime callback: anytype) type { inline for (@typeInfo(@TypeOf(self.intermediate)).Struct.fields) |field| { // @compileLog(@typeName(field.type)); - if (comptime std.mem.startsWith(u8, @typeName(field.type), "?array_list.ArrayList")) { - if (@field(self.intermediate, field.name)) |list| { - std.debug.print("{s}: [\n", .{field.name}); - for (list.items) |item| std.debug.print(" {s},\n", .{item}); - std.debug.print("]\n", .{}); - } else { - std.debug.print("{s}: null\n", .{field.name}); - } + if (@field(self.intermediate, field.name) == null) { + std.debug.print("{s}: null,\n", .{field.name}); } else { - std.debug.print("{s}: {?s}\n", .{ field.name, @field(self.intermediate, field.name) }); + std.debug.print("{s}: ", .{field.name}); + self.print_value(@field(self.intermediate, field.name).?, ""); } } } + fn print_value(self: @This(), value: anytype, comptime indent: []const u8) void { + if (comptime @hasField(@TypeOf(value), "items")) { + std.debug.print("{s}[\n", .{indent}); + for (value.items) |item| { + self.print_value(item, indent ++ " "); + } + std.debug.print("{s}]\n", .{indent}); + } else { + std.debug.print("{s}{s}\n", .{ indent, value }); + } + } + pub fn parse( self: *@This(), args: [][:0]u8, @@ -769,32 +836,14 @@ fn Parser(comptime command: anytype, comptime callback: anytype) type { comptime if (PType.param_type != .Nominal or param.long_tag == null) continue; const tag = param.long_tag.?; - if (comptime PType.is_flag) { - if (std.mem.eql(u8, arg, tag)) { - try self.apply_param_values(param, comptime PType.flag_bias.string(), argit); - return; - } - } else { - if (std.mem.startsWith(u8, arg, tag)) match: { - // TODO: in case of --long=value we should split value - // on comma, so e.g. --long=one,two which is kinda docker-style. - // This adds complexity. Note that --long=one,two will also - // parse as a single value because we take a different - // codepath. In that case presumably the converter will choke if - // it needs to. Ideally the multi-value stuff would all be - // shoved into the converter layer, but we can't do that due to - // needing to consume multiple argv values in some cases. This - // could be an opportunity to become opinionated about CLI flag - // styles, but I will not do that for the time being. - if (arg.len == tag.len) { - const next = argit.next() orelse return ParseError.ValueMissing; - try self.apply_param_values(param, next, argit); - } else if (arg[tag.len] == '=') { - try self.apply_fused_values(param, arg[tag.len + 1 ..]); - } else break :match; + if (std.mem.startsWith(u8, arg, tag)) match: { + if (arg.len == tag.len) { + try self.apply_param_values(param, argit, false); + } else if (arg[tag.len] == '=') { + try self.apply_fused_values(param, arg[tag.len + 1 ..]); + } else break :match; - return; - } + return; } } @@ -813,18 +862,13 @@ fn Parser(comptime command: anytype, comptime callback: anytype) type { comptime if (PType.param_type != .Nominal or param.short_tag == null) continue; const tag = param.short_tag.?; - if (comptime PType.is_flag) { - if (arg == tag[1]) { - try self.apply_param_values(param, comptime PType.flag_bias.string(), argit); - return; - } - } else { - if (arg == tag[1]) { - if (remaining > 0) return ParseError.FusedShortTagValueMissing; - const next = argit.next() orelse return ParseError.ValueMissing; - try self.apply_param_values(param, next, argit); - return; - } + if (arg == tag[1]) { + if (comptime !PType.is_flag) + if (remaining > 0) + return ParseError.FusedShortTagValueMissing; + + try self.apply_param_values(param, argit, false); + return; } } @@ -836,12 +880,15 @@ fn Parser(comptime command: anytype, comptime callback: anytype) type { arg: []const u8, argit: *ncmeta.SliceIterator([][:0]u8), ) ParseError!void { + _ = arg; + comptime var arg_index: u32 = 0; inline for (comptime command.generate()) |param| { comptime if (@TypeOf(param).param_type != .Ordinal) continue; if (self.consumed_args == arg_index) { - try self.apply_param_values(param, arg, argit); + argit.rewind(); + try self.apply_param_values(param, argit, false); self.consumed_args += 1; return; } @@ -852,76 +899,73 @@ fn Parser(comptime command: anytype, comptime callback: anytype) type { // look for subcommands now } - inline fn apply_param_values(self: *@This(), comptime param: anytype, value: []const u8, argit: *ncmeta.SliceIterator([][:0]u8)) ParseError!void { - try push_unparsed_value(Intermediate, param, &self.intermediate, value, self.allocator); - switch (comptime param.value_count) { + inline fn push_intermediate_value( + self: *@This(), + comptime param: anytype, + value: param.IntermediateValue(), + ) ParseError!void { + if (comptime @TypeOf(param).G.multi) { + if (@field(self.intermediate, param.name) == null) { + @field(self.intermediate, param.name) = param.IntermediateType().init(self.allocator); + } + @field(self.intermediate, param.name).?.append(value) catch return ParseError.UnexpectedFailure; + } else if (comptime @TypeOf(param).G.nonscalar()) { + if (@field(self.intermediate, param.name)) |list| list.deinit(); + @field(self.intermediate, param.name) = value; + } else { + @field(self.intermediate, param.name) = value; + } + } + + inline fn apply_param_values( + self: *@This(), + comptime param: anytype, + argit: anytype, + bounded: bool, + ) ParseError!void { + switch (comptime @TypeOf(param).G.value_count) { + .flag => try self.push_intermediate_value(param, comptime param.flag_bias.string()), + .count => @field(self.intermediate, param.name) += 1, .fixed => |count| switch (count) { - 0, 1 => return, + 0 => return ParseError.ExtraValue, + 1 => try self.push_intermediate_value(param, argit.next() orelse return ParseError.MissingValue), else => |total| { - var consumed: u32 = 1; + var list = std.ArrayList([]const u8).initCapacity(self.allocator, total) catch + return ParseError.UnexpectedFailure; + + var consumed: u32 = 0; while (consumed < total) : (consumed += 1) { - const next = argit.next() orelse return ParseError.ValueMissing; - try push_unparsed_value( - Intermediate, - param, - &self.intermediate, - next, - self.allocator, - ); + const next = argit.next() orelse return ParseError.MissingValue; + list.append(next) catch return ParseError.UnexpectedFailure; } + if (bounded and argit.next() != null) return ParseError.ExtraValue; + + try self.push_intermediate_value(param, list); }, }, .greedy => { - while (argit.next()) |next| { - try push_unparsed_value( - Intermediate, - param, - &self.intermediate, - next, - self.allocator, - ); - } + var list = std.ArrayList([]const u8).init(self.allocator); + while (argit.next()) |next| list.append(next) catch return ParseError.UnexpectedFailure; + try self.push_intermediate_value(param, list); }, } } - inline fn apply_fused_values(self: *@This(), comptime param: anytype, value: []const u8) ParseError!void { - switch (comptime param.value_count) { - .fixed => |count| switch (count) { - 0 => return ParseError.UnexpectedValue, - 1 => try push_unparsed_value(Intermediate, param, &self.intermediate, value, self.allocator), - else => |total| { - var seen: u32 = 0; - var iterator = std.mem.split(u8, value, ","); - while (iterator.next()) |next| { - try push_unparsed_value(Intermediate, param, &self.intermediate, next, self.allocator); - seen += 1; - } - if (seen < total) return ParseError.ValueMissing else if (seen > total) return ParseError.UnexpectedValue; - }, - }, - .greedy => { - // huh. this is just an unchecked version of the fixed-many case. - var iterator = std.mem.split(u8, value, ","); - while (iterator.next()) |next| { - try push_unparsed_value(Intermediate, param, &self.intermediate, next, self.allocator); - } - }, - } + inline fn apply_fused_values( + self: *@This(), + comptime param: anytype, + value: []const u8, + ) ParseError!void { + var iter = std.mem.split(u8, value, ","); + return try self.apply_param_values(param, &iter, true); } fn read_environment(self: *@This(), env: std.process.EnvMap) !void { inline for (comptime command.generate()) |param| { if (comptime param.env_var) |env_var| { + if (@field(self.intermediate, param.name) != null) return; const val = env.get(env_var) orelse return; - - push_unparsed_value( - Intermediate, - param, - &self.intermediate, - val, - self.allocator, - ) catch return ParseError.UnexpectedFailure; + try self.apply_fused_values(param, val); return; } } @@ -933,46 +977,75 @@ fn HelpBuilder(comptime command: anytype) type { _ = command; } -pub fn command_builder(comptime ContextType: type) CommandBuilder(ContextType) { - return CommandBuilder(ContextType){}; +pub fn command_builder(comptime UserContext: type) CommandBuilder(UserContext) { + return CommandBuilder(UserContext){}; } const Choice = enum { first, second }; +fn fixed_output(_: u32, _: std.ArrayList([]const u8)) converters.ConversionError!u8 { + return 0; +} + +fn greedy_output(_: u32, input: std.ArrayList([]const u8)) converters.ConversionError!std.ArrayList([]const u8) { + var output = std.ArrayList([]const u8).initCapacity(input.allocator, 1) catch + return converters.ConversionError.BadValue; + + output.appendAssumeCapacity("hello"); + return output; +} + const cli = cmd: { - var cmd = command_builder(void); - cmd.add_option(u8, .{ + var cmd = command_builder(u32); + cmd.add_option(.{ + .OutputType = u8, + .value_count = .{ .fixed = 2 }, + }, .{ .name = "test", .short_tag = "-t", .long_tag = "--test", .env_var = "NOCLIP_TEST", - .value_count = .{ .fixed = 2 }, + .converter = fixed_output, }); - cmd.add_option(Choice, .{ + cmd.add_option(.{ .OutputType = Choice }, .{ .name = "choice", .short_tag = "-c", .long_tag = "--choice", .env_var = "NOCLIP_CHOICE", }); - cmd.add_flag(.{ + // cmd.add_option(.{ .OutputType = u8, .multi = true }, .{ + // .name = "multi", + // .short_tag = "-m", + // .long_tag = "--multi", + // .env_var = "NOCLIP_MULTI", + // }); + cmd.add_flag(.{}, .{ .name = "flag", .truthy = .{ .short_tag = "-f", .long_tag = "--flag" }, .falsy = .{ .long_tag = "--no-flag" }, .env_var = "NOCLIP_FLAG", }); - cmd.add_argument([]const u8, .{ - .name = "arg", - // .value_count = .{ .fixed = 3 }, + // cmd.add_flag(.{ .multi = true }, .{ + // .name = "multiflag", + // .truthy = .{ .short_tag = "-M" }, + // .env_var = "NOCLIP_MULTIFLAG", + // .multi = true, + // }); + cmd.add_argument(.{ + .OutputType = []const u8, .value_count = .greedy, + }, .{ + .name = "arg", + .converter = greedy_output, }); break :cmd cmd; }; -fn cli_handler(_: void, result: cli.Output()) !void { +fn cli_handler(context: *u32, result: cli.Output()) !void { _ = result; - std.debug.print("callback is working\n", .{}); + std.debug.print("callback is working {d}\n", .{context.*}); } pub fn main() !void { @@ -981,6 +1054,7 @@ pub fn main() !void { const allocator = arena.allocator(); var parser = cli.bind(cli_handler, allocator); - const iface = parser.interface(); - try iface.execute({}); + var context: u32 = 2; + const iface = parser.interface(&context); + try iface.execute(); } diff --git a/source/meta.zig b/source/meta.zig index a0e885a..b3482a9 100644 --- a/source/meta.zig +++ b/source/meta.zig @@ -79,6 +79,12 @@ pub fn SliceIterator(comptime T: type) type { return self.data[self.index]; } + pub fn rewind(self: *@This()) void { + if (self.index == 0) return; + + self.index -= 1; + } + pub fn skip(self: *@This()) void { if (self.index == self.data.len) return;