diff --git a/build.zig b/build.zig index 7c5fb58..d010f6c 100644 --- a/build.zig +++ b/build.zig @@ -1,27 +1,12 @@ const std = @import("std"); pub fn build(b: *std.build.Builder) void { - const demo = b.step("demo", "noclip demo"); - const tests = b.step("test", "Run unit tests"); - const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); - const exe = b.addSharedLibrary(.{ - .name = "noclip", - .root_source_file = .{ .path = "source/noclip.zig" }, - .target = target, - .optimize = optimize, - }); - exe.install(); - - const demo_exe = b.addExecutable(.{ - .name = "noclip-demo", - .root_source_file = .{ .path = "source/doodle.zig" }, - }); - const install_demo = b.addInstallArtifact(demo_exe); - demo.dependOn(&install_demo.step); + demo(b, target, optimize); + const tests = b.step("test", "Run unit tests"); const lib_tests = b.addTest(.{ .name = "tests", .root_source_file = .{ .path = "source/noclip.zig" }, @@ -31,3 +16,19 @@ pub fn build(b: *std.build.Builder) void { tests.dependOn(&lib_tests.step); } + +fn demo(b: *std.build.Builder, target: anytype, optimize: anytype) void { + const demo_step = b.step("demo", "Build and install CLI demo program"); + const noclip = b.createModule(.{ .source_file = .{ .path = "source/noclip.zig" } }); + + const exe = b.addExecutable(.{ + .name = "noclip-demo", + .root_source_file = .{ .path = "demo/demo.zig" }, + .target = target, + .optimize = optimize, + }); + exe.addModule("noclip", noclip); + const install_demo = b.addInstallArtifact(exe); + + demo_step.dependOn(&install_demo.step); +} diff --git a/demo/demo.zig b/demo/demo.zig index 237f22c..61563ed 100644 --- a/demo/demo.zig +++ b/demo/demo.zig @@ -1,97 +1,89 @@ const std = @import("std"); const noclip = @import("noclip"); -const context: []const u8 = "hello friend"; -const ContextType = @TypeOf(context); +const CommandBuilder = noclip.CommandBuilder; -const subcommand = blk: { - var cmd = noclip.Command(ContextType, .{ .name = "verb", .help = "this a sub command" }); - cmd.add(cmd.defaultHelpFlag); - cmd.add(cmd.StringOption{ .name = "meta", .short = "-m" }); - cmd.add(cmd.StringArgument{ .name = "argument" }); - cmd.add(cmd.Argument(u32){ .name = "another", .default = 0 }); - break :blk cmd; +const Choice = enum { first, second }; + +const cli = cmd: { + var cmd = CommandBuilder(u32).init(); + cmd.add_option(.{ .OutputType = struct { u8, u8 } }, .{ + .name = "test", + .short_tag = "-t", + .long_tag = "--test", + .env_var = "NOCLIP_TEST", + }); + cmd.add_option(.{ .OutputType = Choice }, .{ + .name = "choice", + .short_tag = "-c", + .long_tag = "--choice", + .env_var = "NOCLIP_CHOICE", + }); + cmd.add_option(.{ .OutputType = u32 }, .{ + .name = "default", + .short_tag = "-d", + .long_tag = "--default", + .env_var = "NOCLIP_DEFAULT", + .default = 100, + }); + 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_flag(.{ .multi = true }, .{ + .name = "multiflag", + .truthy = .{ .short_tag = "-M" }, + .env_var = "NOCLIP_MULTIFLAG", + }); + cmd.add_argument(.{ .OutputType = []const u8 }, .{ + .name = "arg", + }); + + break :cmd cmd; }; -const command = blk: { - var cmd = noclip.Command(ContextType, .{ - .name = "main", - .help = - \\This is the main CLI entry point for the noclip demo - \\ - \\This program demonstrates the major features of noclip both in terms of API - \\usage (in its source code) and argument parsing (in its execution). - , +const subcommand = cmd: { + var cmd = CommandBuilder(void).init(); + cmd.add_flag(.{}, .{ + .name = "flag", + .truthy = .{ .short_tag = "-f", .long_tag = "--flag" }, + .falsy = .{ .long_tag = "--no-flag" }, + .env_var = "NOCLIP_SUBFLAG", }); - cmd.add(cmd.defaultHelpFlag); - 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", - .help = "some generic input", - .default = "in", - .envVar = "OPTS_INPUT", - }); - cmd.add(cmd.StringOption{ - .name = "output", - .long = "--output", - .default = "waoh", - .help = "name of the output", - }); - cmd.add(cmd.Option(i32){ - .name = "number", - .short = "-n", - .long = "--number", - .help = "a number", - .default = 0, - }); - - cmd.add(subcommand.Parser(subCallback)); - break :blk cmd; + cmd.add_argument(.{ .OutputType = []const u8 }, .{ .name = "argument" }); + break :cmd cmd; }; -fn printHandler(ctx: ContextType, input: []const u8) ![]const u8 { - std.debug.print("ctx: {s}\n", .{ctx}); - return input; +fn sub_handler(_: *void, result: subcommand.Output()) !void { + std.debug.print("subcommand: {s}\n", .{result.argument}); } -pub fn subCallback(_: ContextType, result: subcommand.CommandResult()) !void { - std.debug.print( - \\subcommand: {{ - \\ .meta = {s} - \\ .argument = {s} - \\ .another = {d} - \\}} - \\ - , - .{ result.meta, result.argument, result.another }, - ); -} +fn cli_handler(context: *u32, result: cli.Output()) !void { + _ = context; -pub fn mainCommand(_: ContextType, result: command.CommandResult()) !void { - // std.debug.print("{any}", .{result}); - std.debug.print( - \\arguments: {{ - \\ .flag = {any} - \\ .input = {s} - \\ .output = {s} - \\ .number = {d} - \\}} - \\ - , - .{ result.flag, result.input, result.output, result.number }, - ); + std.debug.print("callback is working {any}\n", .{result.choice}); + std.debug.print("callback is working {d}\n", .{result.default}); } pub fn main() !void { - var parser = command.Parser(mainCommand); - var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); - const allocator = arena.allocator(); - var argit = try std.process.argsWithAllocator(allocator); - try parser.execute(allocator, std.process.ArgIterator, &argit, context); + var parser = cli.bind(cli_handler, allocator); + var context: u32 = 2; + + var subcon = subcommand.bind(sub_handler, allocator); + try parser.add_subcommand("verb", subcon.interface()); + + const iface = parser.interface(&context); + try iface.execute(); } diff --git a/source/bakery.zig b/source/bakery.zig deleted file mode 100644 index 1553223..0000000 --- a/source/bakery.zig +++ /dev/null @@ -1,63 +0,0 @@ -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.StringOption(UserContext), - StringArgument: type = params.StringArg(UserContext), - Flag: type = params.Flag(UserContext), - defaultHelpFlag: params.Flag(UserContext) = HelpFlag(undefined, .{}), - - 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 _: @This(), 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/command.zig b/source/command.zig new file mode 100644 index 0000000..0f671eb --- /dev/null +++ b/source/command.zig @@ -0,0 +1,387 @@ +const std = @import("std"); +const StructField = std.builtin.Type.StructField; + +const ncmeta = @import("./meta.zig"); +const parameters = @import("./parameters.zig"); +const parser = @import("./parser.zig"); + +const ValueCount = parameters.ValueCount; +const ParameterGenerics = parameters.ParameterGenerics; +const OptionConfig = parameters.OptionConfig; +const FlagConfig = parameters.FlagConfig; +const FlagBias = parameters.FlagBias; +const make_option = parameters.make_option; +const make_argument = parameters.make_argument; + +const Parser = parser.Parser; +const ParserInterface = parser.ParserInterface; + +fn BuilderGenerics(comptime UserContext: type) type { + return struct { + OutputType: type = void, + value_count: ValueCount = .{ .fixed = 1 }, + multi: 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 = ParameterGenerics.fixed_value_count(self.OutputType, self.value_count), + .multi = self.multi, + }; + } + + 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 = ParameterGenerics.fixed_value_count(self.OutputType, 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, + }; + } + }; +} + +pub fn CommandBuilder(comptime UserContext: type) type { + return struct { + param_spec: ncmeta.MutableTuple = .{}, + + pub const UserContextType = UserContext; + + pub fn init() @This() { + return .{}; + } + + pub fn add_argument( + comptime self: *@This(), + comptime bgen: BuilderGenerics(UserContext), + comptime config: OptionConfig(bgen.arg_gen()), + ) void { + self.param_spec.add(make_argument(bgen.arg_gen(), config)); + } + + pub fn add_option( + comptime self: *@This(), + comptime bgen: BuilderGenerics(UserContext), + comptime config: OptionConfig(bgen.opt_gen()), + ) void { + 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(bgen.opt_gen(), config)); + } + + pub fn set_help_flag( + comptime self: *@This(), + comptime bgen: BuilderGenerics(UserContext), + comptime config: FlagConfig(bgen.flag_gen()), + ) void { + _ = self; + _ = config; + } + + pub fn add_flag( + comptime self: *@This(), + comptime bgen: BuilderGenerics(UserContext), + comptime config: FlagConfig(bgen.flag_gen()), + ) void { + comptime { + if (config.truthy == null and config.falsy == null and config.env_var == null) { + @compileError( + "flag " ++ + config.name ++ + " must have at least one of truthy flags, falsy flags, or env_var flags", + ); + } + + const generics = bgen.flag_gen(); + var args = OptionConfig(generics){ + .name = config.name, + // + .short_tag = null, + .long_tag = null, + .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", + }; + + 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| { + // @compileLog(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)); + } + } + } + + pub fn generate(comptime self: @This()) self.param_spec.TupleType() { + return self.param_spec.realTuple(); + } + + pub fn CallbackSignature(comptime self: @This()) type { + return *const fn (*UserContext, self.Output()) anyerror!void; + } + + pub fn Output(comptime self: @This()) type { + comptime { + const spec = self.generate(); + var fields: []const StructField = &[_]StructField{}; + var flag_skip = 0; + + var tag_fields: []const StructField = &[_]StructField{}; + var env_var_fields: []const StructField = &[_]StructField{}; + + paramloop: for (spec, 0..) |param, idx| { + // these three blocks are to check for redundantly defined tags and + // environment variables. This only works within a command. It + // doesn't support compile time checks for conflict into + // subcommands because those are attached at runtime. also, only + // global tags and env_vars would conflict, which is less common. + if (param.short_tag) |short| + tag_fields = tag_fields ++ &[_]StructField{.{ + .name = short, + .type = void, + .default_value = null, + .is_comptime = false, + .alignment = 0, + }}; + + if (param.long_tag) |long| + tag_fields = tag_fields ++ &[_]StructField{.{ + .name = long, + .type = void, + .default_value = null, + .is_comptime = false, + .alignment = 0, + }}; + + if (param.env_var) |env_var| + env_var_fields = env_var_fields ++ &[_]StructField{.{ + .name = env_var, + .type = void, + .default_value = null, + .is_comptime = false, + .alignment = 0, + }}; + + if (!param.exposed) continue :paramloop; + while (flag_skip > 0) { + flag_skip -= 1; + continue :paramloop; + } + + const PType = @TypeOf(param); + if (PType.is_flag) { + var peek = idx + 1; + 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]; + + 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 { + bias_seen[@enumToInt(peek_param.flag_bias)] = true; + } + flag_skip += 1; + } else { + break; + } + } + } + + // the default field is already the optional type. Stripping + // the optional wrapper is an interesting idea for required + // fields. I do not foresee this greatly increasing complexity here. + const FieldType = if (param.required or param.default != null) + PType.G.ConvertedType() + else + ?PType.G.ConvertedType(); + + const default = if (param.default) |def| &@as(FieldType, def) else @as(?*const anyopaque, null); + + fields = fields ++ &[_]StructField{.{ + .name = param.name, + .type = FieldType, + .default_value = default, + .is_comptime = false, + .alignment = @alignOf(FieldType), + }}; + } + + _ = @Type(.{ .Struct = .{ + .layout = .Auto, + .fields = tag_fields, + .decls = &.{}, + .is_tuple = false, + } }); + + _ = @Type(.{ .Struct = .{ + .layout = .Auto, + .fields = env_var_fields, + .decls = &.{}, + .is_tuple = false, + } }); + + return @Type(.{ .Struct = .{ + .layout = .Auto, + .fields = fields, + .decls = &.{}, + .is_tuple = false, + } }); + } + } + + pub fn Intermediate(comptime self: @This()) type { + comptime { + const spec = self.generate(); + var fields: []const StructField = &[0]StructField{}; + var flag_skip = 0; + + paramloop: for (spec, 0..) |param, idx| { + while (flag_skip > 0) { + flag_skip -= 1; + continue :paramloop; + } + + const PType = @TypeOf(param); + if (PType.is_flag) { + var peek = idx + 1; + 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]; + + 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 { + bias_seen[@enumToInt(peek_param.flag_bias)] = true; + } + flag_skip += 1; + } else { + break; + } + } + } + + 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( + FieldType, + if (PType.value_count == .count) 0 else null, + ), + ), + .is_comptime = false, + .alignment = @alignOf(?[]const u8), + }}); + } + + return @Type(.{ .Struct = .{ + .layout = .Auto, + .fields = fields, + .decls = &.{}, + .is_tuple = false, + } }); + } + } + + pub fn bind( + comptime self: @This(), + comptime callback: self.CallbackSignature(), + allocator: std.mem.Allocator, + ) Parser(self, callback) { + return Parser(self, callback){ + .allocator = allocator, + .subcommands = std.hash_map.StringHashMap(ParserInterface).init(allocator), + }; + } + }; +} diff --git a/source/converters.zig b/source/converters.zig index 42217e6..7ff847c 100644 --- a/source/converters.zig +++ b/source/converters.zig @@ -1,13 +1,14 @@ const std = @import("std"); -const ParameterGenerics = @import("./doodle.zig").ParameterGenerics; -const CommandError = @import("./doodle.zig").Errors; -const ValueCount = @import("./doodle.zig").ValueCount; -const ParseError = @import("./doodle.zig").ParseError; +const ConversionError = @import("./errors.zig").ConversionError; const ncmeta = @import("./meta.zig"); +const parameters = @import("./parameters.zig"); + +const ValueCount = parameters.ValueCount; +const ParameterGenerics = parameters.ParameterGenerics; pub fn ConverterSignature(comptime gen: ParameterGenerics) type { - return *const fn (*gen.UserContext, gen.IntermediateType()) ParseError!gen.ConvertedType(); + return *const fn (*gen.UserContext, gen.IntermediateType()) ConversionError!gen.ConvertedType(); } pub fn default_converter(comptime gen: ParameterGenerics) ?ConverterSignature(gen) { @@ -39,9 +40,9 @@ fn multi_converter(comptime gen: ParameterGenerics) ?ConverterSignature(gen) { const Intermediate = gen.IntermediateType(); return struct { - pub fn handler(context: *gen.UserContext, input: Intermediate) ParseError!std.ArrayList(gen.OutputType) { + pub fn handler(context: *gen.UserContext, input: Intermediate) ConversionError!std.ArrayList(gen.OutputType) { var output = std.ArrayList(gen.OutputType).initCapacity(input.allocator, input.items.len) catch - return ParseError.ConversionFailed; + return ConversionError.ConversionFailed; for (input.items) |item| { output.appendAssumeCapacity(try converter(context, item)); @@ -54,7 +55,7 @@ fn multi_converter(comptime gen: ParameterGenerics) ?ConverterSignature(gen) { fn flag_converter(comptime gen: ParameterGenerics) ConverterSignature(gen) { return struct { - pub fn handler(_: *gen.UserContext, input: []const u8) ParseError!bool { + pub fn handler(_: *gen.UserContext, input: []const u8) ConversionError!bool { // treat an empty string as falsy if (input.len == 0) return false; @@ -74,7 +75,7 @@ fn flag_converter(comptime gen: ParameterGenerics) ConverterSignature(gen) { fn string_converter(comptime gen: ParameterGenerics) ConverterSignature(gen) { return struct { - pub fn handler(_: *gen.UserContext, value: []const u8) ParseError![]const u8 { + pub fn handler(_: *gen.UserContext, value: []const u8) ConversionError![]const u8 { return value; } }.handler; @@ -84,8 +85,8 @@ fn int_converter(comptime gen: ParameterGenerics) ConverterSignature(gen) { const IntType = gen.OutputType; return struct { - pub fn handler(_: *gen.UserContext, value: []const u8) ParseError!IntType { - return std.fmt.parseInt(IntType, value, 0) catch return ParseError.ConversionFailed; + pub fn handler(_: *gen.UserContext, value: []const u8) ConversionError!IntType { + return std.fmt.parseInt(IntType, value, 0) catch return ConversionError.ConversionFailed; } }.handler; } @@ -96,8 +97,8 @@ fn struct_converter(comptime gen: ParameterGenerics) ConverterSignature(gen) { const Intermediate = gen.IntermediateType(); return struct { - pub fn handler(context: *gen.UserContext, value: Intermediate) ParseError!StructType { - if (value.items.len != type_info.fields.len) return ParseError.ConversionFailed; + pub fn handler(context: *gen.UserContext, value: Intermediate) ConversionError!StructType { + if (value.items.len != type_info.fields.len) return ConversionError.ConversionFailed; var result: StructType = undefined; inline for (comptime type_info.fields, 0..) |field, idx| { @@ -121,8 +122,8 @@ fn choice_converter(comptime gen: ParameterGenerics) ConverterSignature(gen) { const EnumType = gen.OutputType; return struct { - pub fn handler(_: *gen.UserContext, value: []const u8) ParseError!EnumType { - return std.meta.stringToEnum(gen.ConvertedType(), value) orelse ParseError.ConversionFailed; + pub fn handler(_: *gen.UserContext, value: []const u8) ConversionError!EnumType { + return std.meta.stringToEnum(gen.ConvertedType(), value) orelse ConversionError.ConversionFailed; } }.handler; } diff --git a/source/doodle.zig b/source/doodle.zig deleted file mode 100644 index f3dfb78..0000000 --- a/source/doodle.zig +++ /dev/null @@ -1,1152 +0,0 @@ -const std = @import("std"); -const StructField = std.builtin.Type.StructField; - -const converters = @import("./converters.zig"); -const ncmeta = @import("./meta.zig"); - -const ConverterSignature = converters.ConverterSignature; - -pub const ParseError = error{ - UnexpectedFailure, - EmptyArgs, - MissingValue, - ExtraValue, - FusedShortTagValueMissing, - UnknownLongTagParameter, - UnknownShortTagParameter, - RequiredMissing, - ConversionFailed, -}; - -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 multi value and then splitting in the value handler. -const ValueCount = union(enum) { - flag: void, - count: void, - fixed: u32, -}; - -const FlagBias = enum { - falsy, - truthy, - unbiased, - - pub fn string(comptime self: @This()) []const u8 { - return switch (comptime self) { - .truthy => "true", - .falsy => "false", - else => @compileError("flag tag with unbiased bias?"), - }; - } -}; - -pub const ParameterGenerics = struct { - 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, - // since we now use multi in place of greedy values for simplicity, we may want to - // convert this an enum or add an additional flag to distinguish between the - // many-to-many and the many-to-one cases. - - pub fn fixed_value_count(comptime OutputType: type, comptime value_count: ValueCount) ValueCount { - return comptime if (value_count == .fixed) - switch (@typeInfo(OutputType)) { - .Struct => |info| .{ .fixed = info.fields.len }, - .Array => |info| .{ .fixed = info.len }, - // TODO: this is a bit sloppy, but it can be refined later. - // .Pointer covers slices, which may be a many-to-many conversion. - .Pointer => value_count, - else => .{ .fixed = 1 }, - } - else - value_count; - } - - pub fn clone_without_multi(comptime self: @This()) @This() { - return .{ .UserContext = self.UserContext, .OutputType = self.OutputType, .param_type = self.param_type, .value_count = self.value_count, .multi = false }; - } - - pub fn has_context(comptime self: @This()) bool { - return comptime self.UserContext != void; - } - - pub fn is_flag(comptime self: @This()) bool { - return comptime switch (self.value_count) { - .flag, .count => true, - .fixed => false, - }; - } - - 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, - }, - }; - } - - 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), - }, - }; - } - - 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, - }, - }; - } -}; - -fn OptionConfig(comptime generics: ParameterGenerics) type { - return struct { - name: []const u8, - - short_tag: ?[]const u8 = null, - long_tag: ?[]const u8 = null, - env_var: ?[]const u8 = 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, - - 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, - }; -} - -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 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.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, - - /// 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, - - /// 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 IntermediateValue(comptime _: @This()) type { - return generics.IntermediateValue(); - } - }; -} - -fn check_short(comptime short_tag: ?[]const u8) void { - 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 { - 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) { - if (opts.short_tag == null and opts.long_tag == null and opts.env_var == null) { - @compileError( - "option " ++ - opts.name ++ - " must have at least one of a short tag, a long tag, or an environment variable", - ); - } - - check_short(opts.short_tag); - check_long(opts.long_tag); - - // perform the logic to create the default converter here? Could be done - // when creating the OptionConfig instead. Need to do it here because there - // may be an error. That's the essential distinction between the OptionType - // 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 @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, - // - .eager = opts.eager, - .required = opts.required, - .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) { - 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"); - } - - if (opts.global) { - @compileError("argument " ++ opts.name ++ " cannot be global"); - } - - 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, - // - .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, - }; - } -} - -fn BuilderGenerics(comptime UserContext: type) type { - return struct { - OutputType: type = void, - value_count: ValueCount = .{ .fixed = 1 }, - multi: 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 = ParameterGenerics.fixed_value_count(self.OutputType, self.value_count), - .multi = self.multi, - }; - } - - 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 = ParameterGenerics.fixed_value_count(self.OutputType, 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 UserContext: type) type { - return struct { - param_spec: ncmeta.MutableTuple = .{}, - - pub const UserContextType = UserContext; - - pub fn add_argument( - comptime self: *@This(), - comptime bgen: BuilderGenerics(UserContext), - comptime config: OptionConfig(bgen.arg_gen()), - ) void { - self.param_spec.add(make_argument(bgen.arg_gen(), config)); - } - - pub fn add_option( - comptime self: *@This(), - comptime bgen: BuilderGenerics(UserContext), - comptime config: OptionConfig(bgen.opt_gen()), - ) void { - 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(bgen.opt_gen(), config)); - } - - pub fn set_help_flag( - comptime self: *@This(), - comptime bgen: BuilderGenerics(UserContext), - comptime config: FlagConfig(bgen.flag_gen()), - ) void { - _ = self; - _ = config; - } - - pub fn add_flag( - comptime self: *@This(), - comptime bgen: BuilderGenerics(UserContext), - comptime config: FlagConfig(bgen.flag_gen()), - ) void { - comptime { - if (config.truthy == null and config.falsy == null and config.env_var == null) { - @compileError( - "flag " ++ - config.name ++ - " must have at least one of truthy flags, falsy flags, or env_var flags", - ); - } - - const generics = bgen.flag_gen(); - var args = OptionConfig(generics){ - .name = config.name, - // - .short_tag = null, - .long_tag = null, - .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", - }; - - 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| { - // @compileLog(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)); - } - } - } - - fn generate(comptime self: @This()) self.param_spec.TupleType() { - return self.param_spec.realTuple(); - } - - pub fn CallbackSignature(comptime self: @This()) type { - return *const fn (*UserContext, self.Output()) anyerror!void; - } - - pub fn Output(comptime self: @This()) type { - comptime { - const spec = self.generate(); - var fields: []const StructField = &[0]StructField{}; - var flag_skip = 0; - - paramloop: for (spec, 0..) |param, idx| { - if (!param.exposed) continue :paramloop; - while (flag_skip > 0) { - flag_skip -= 1; - continue :paramloop; - } - - const PType = @TypeOf(param); - if (PType.is_flag) { - var peek = idx + 1; - 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]; - - 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 { - bias_seen[@enumToInt(peek_param.flag_bias)] = true; - } - flag_skip += 1; - } else { - break; - } - } - } - - // the default field is already the optional type. Stripping - // the optional wrapper is an interesting idea for required - // fields. I do not foresee this greatly increasing complexity here. - const FieldType = if (param.required) - PType.G.ConvertedType() - else - ?PType.G.ConvertedType(); - - // the wacky comptime slice extension hack - fields = &(@as([fields.len]StructField, fields[0..fields.len].*) ++ [1]StructField{.{ - .name = param.name, - .type = FieldType, - .default_value = @ptrCast(?*const anyopaque, ¶m.default), - .is_comptime = false, - .alignment = @alignOf(FieldType), - }}); - } - - return @Type(.{ .Struct = .{ - .layout = .Auto, - .fields = fields, - .decls = &.{}, - .is_tuple = false, - } }); - } - } - - pub fn Intermediate(comptime self: @This()) type { - comptime { - const spec = self.generate(); - var fields: []const StructField = &[0]StructField{}; - var flag_skip = 0; - - paramloop: for (spec, 0..) |param, idx| { - while (flag_skip > 0) { - flag_skip -= 1; - continue :paramloop; - } - - const PType = @TypeOf(param); - if (PType.is_flag) { - var peek = idx + 1; - 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]; - - 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 { - bias_seen[@enumToInt(peek_param.flag_bias)] = true; - } - flag_skip += 1; - } else { - break; - } - } - } - - 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( - FieldType, - if (PType.value_count == .count) 0 else null, - ), - ), - .is_comptime = false, - .alignment = @alignOf(?[]const u8), - }}); - } - - return @Type(.{ .Struct = .{ - .layout = .Auto, - .fields = fields, - .decls = &.{}, - .is_tuple = false, - } }); - } - } - - pub fn bind( - comptime self: @This(), - comptime callback: self.CallbackSignature(), - allocator: std.mem.Allocator, - ) Parser(self, callback) { - return Parser(self, callback){ - .allocator = allocator, - .subcommands = std.hash_map.StringHashMap(ParserInterface).init(allocator), - }; - } - }; -} - -const ParserInterface = struct { - const Vtable = struct { - execute: *const fn (parser: *anyopaque, context: *anyopaque) anyerror!void, - parse: *const fn (parser: *anyopaque, context: *anyopaque, args: [][:0]u8, env: std.process.EnvMap) anyerror!void, - finish: *const fn (parser: *anyopaque, context: *anyopaque) anyerror!void, - }; - - parser: *anyopaque, - context: *anyopaque, - methods: *const Vtable, - - pub fn execute(self: @This()) anyerror!void { - return try self.methods.execute(self.parser, self.context); - } - - pub fn parse(self: @This(), args: [][:0]u8, env: std.process.EnvMap) anyerror!void { - return try self.methods.parse(self.parser, self.context, args, env); - } - - pub fn finish(self: @This()) anyerror!void { - return try self.methods.finish(self.parser, self.context); - } -}; - -fn InterfaceGen(comptime ParserType: type, comptime UserContext: type) type { - return if (@typeInfo(UserContext) == .Void) struct { - pub fn interface(self: *ParserType) ParserInterface { - return .{ - .parser = self, - .context = @constCast(&void{}), - .methods = &.{ - .execute = ParserType.wrap_execute, - .parse = ParserType.wrap_parse, - .finish = ParserType.wrap_finish, - }, - }; - } - } else struct { - pub fn interface(self: *ParserType, context: *UserContext) ParserInterface { - return .{ - .parser = self, - .context = context, - .methods = &.{ - .execute = ParserType.wrap_execute, - .parse = ParserType.wrap_parse, - .finish = ParserType.wrap_finish, - }, - }; - } - }; -} - -// 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 { - const UserContext = @TypeOf(command).UserContextType; - const Intermediate = command.Intermediate(); - const Output = command.Output(); - const parameters = command.generate(); - - return struct { - intermediate: Intermediate = .{}, - output: Output = undefined, - consumed_args: u32 = 0, - progname: ?[]const u8 = null, - has_global_tags: bool = false, - allocator: std.mem.Allocator, - subcommands: std.hash_map.StringHashMap(ParserInterface), - subcommand: ?ParserInterface = null, - - pub fn add_subcommand(self: *@This(), verb: []const u8, parser: ParserInterface) !void { - try self.subcommands.put(verb, parser); - } - - // 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(parser: *anyopaque, ctx: *anyopaque) anyerror!void { - const self = @ptrCast(*@This(), @alignCast(@alignOf(*@This()), parser)); - - // this is a slightly annoying hack to work around the problem that void has - // 0 alignment, which alignCast chokes on. - const context = if (@alignOf(UserContext) > 0) - @ptrCast(*UserContext, @alignCast(@alignOf(UserContext), ctx)) - else - @ptrCast(*UserContext, ctx); - return try self.execute(context); - } - - fn wrap_parse(parser: *anyopaque, ctx: *anyopaque, args: [][:0]u8, env: std.process.EnvMap) anyerror!void { - const self = @ptrCast(*@This(), @alignCast(@alignOf(@This()), parser)); - const context = if (@alignOf(UserContext) > 0) - @ptrCast(*UserContext, @alignCast(@alignOf(UserContext), ctx)) - else - @ptrCast(*UserContext, ctx); - return try self.subparse(context, args, env); - } - - fn wrap_finish(parser: *anyopaque, ctx: *anyopaque) anyerror!void { - const self = @ptrCast(*@This(), @alignCast(@alignOf(@This()), parser)); - const context = if (@alignOf(UserContext) > 0) - @ptrCast(*UserContext, @alignCast(@alignOf(UserContext), ctx)) - else - @ptrCast(*UserContext, ctx); - return try self.finish(context); - } - - pub fn subparse(self: *@This(), context: *UserContext, args: [][:0]u8, env: std.process.EnvMap) anyerror!void { - const sliceto = try self.parse(args); - try self.read_environment(env); - try self.convert(context); - - inline for (@typeInfo(@TypeOf(self.intermediate)).Struct.fields) |field| { - if (@field(self.intermediate, field.name) == null) { - std.debug.print("{s}: null,\n", .{field.name}); - } else { - std.debug.print("{s}: ", .{field.name}); - self.print_value(@field(self.intermediate, field.name).?, ""); - } - } - - if (self.subcommand) |verb| try verb.parse(args[sliceto..], env); - } - - pub fn finish(self: *@This(), context: *UserContext) anyerror!void { - try callback(context, self.output); - if (self.subcommand) |verb| try verb.finish(); - } - - 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); - defer env.deinit(); - - if (args.len < 1) return ParseError.EmptyArgs; - - self.progname = args[0]; - - try self.subparse(context, args[1..], env); - try self.finish(context); - } - - 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, - ) anyerror!usize { - // run pre-parse pass if we have any global parameters - // try self.preparse() - - var forced_ordinal = false; - var argit = ncmeta.SliceIterator(@TypeOf(args)).wrap(args); - - // there are a LOT of different parsing strategies that can be adopted to - // handle "incorrect" command lines. For example, a --long-style named - // argument could be parsed as an ordered argument if it doesn't match any - // of the specified tag names. However, if the user has not passed `--` - // then it's more likely the erroneous flag is a typo or some other - // erroneous input and should be treated as such. Similarly, handling the - // pair `--long-style --some-value`. if long_style takes one value, - // should --some-value be treated as the value, or should we assume the - // user forgot the value and is specifying a second tag? Getting too clever - // with context (e.g. checking if --some-value is a known tag name) - // probably also violates the principle of least astonishment, as if it - // doesn't match, it could very likely be a typo or other erroneous input. - // In this case we have an out, sort of, as --long-style=--some-value is - // unambiguous in purpose. However, this approach misses for short flags, - // unless we also support a -l=--some-value syntax, which I don't like and - // don't think is a common convention. In this case, I think it is - // reasonable to consume the value without getting fancy, - // e.g. -l --some-value produces 'long_style: "--some-value"'. Odds are, if - // the command line was specified incorrectly, the error will cascade - // through somewhere. - - // another consideration is how to deal with mixed --named and positional - // arguments. Theoretically, fixed quantity positional arguments can be - // unambiguously interspersed with named arguments, but that feels sloppy. - // If a positional argument needs to start with --, we have the -- argument - // to force positional parsing. - - argloop: while (argit.next()) |arg| { - if (!forced_ordinal and std.mem.eql(u8, arg, "--")) { - forced_ordinal = true; - continue :argloop; - } - - if (!forced_ordinal and arg.len > 1 and arg[0] == '-') { - if (arg.len > 2 and arg[1] == '-') { - try self.parse_long_tag(arg, &argit); - continue :argloop; - } else if (arg.len > 1) { - for (arg[1..], 1..) |short, idx| { - try self.parse_short_tag(short, arg.len - idx - 1, &argit); - } - continue :argloop; - } - - // if we've fallen through to here then we will be parsing ordinals - // exclusively from here on out. - forced_ordinal = true; - } - - if (try self.parse_ordinals(arg, &argit)) |verb| { - self.subcommand = verb; - // TODO: return slice of remaining or offset index - return argit.index; - } - } - - return 0; - } - - inline fn parse_long_tag( - self: *@This(), - arg: []const u8, - argit: *ncmeta.SliceIterator([][:0]u8), - ) ParseError!void { - inline for (comptime parameters) |param| { - const PType = @TypeOf(param); - // removing the comptime here causes the compiler to die - comptime if (PType.param_type != .Nominal or param.long_tag == null) continue; - const tag = param.long_tag.?; - - 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 ParseError.UnknownLongTagParameter; - } - - inline fn parse_short_tag( - self: *@This(), - arg: u8, - remaining: usize, - argit: *ncmeta.SliceIterator([][:0]u8), - ) ParseError!void { - inline for (comptime parameters) |param| { - const PType = @TypeOf(param); - // removing the comptime here causes the compiler to die - comptime if (PType.param_type != .Nominal or param.short_tag == null) continue; - const tag = param.short_tag.?; - - if (arg == tag[1]) { - if (comptime !PType.is_flag) - if (remaining > 0) - return ParseError.FusedShortTagValueMissing; - - try self.apply_param_values(param, argit, false); - return; - } - } - - return ParseError.UnknownShortTagParameter; - } - - inline fn parse_ordinals( - self: *@This(), - arg: []const u8, - argit: *ncmeta.SliceIterator([][:0]u8), - ) ParseError!?ParserInterface { - comptime var arg_index: u32 = 0; - inline for (comptime parameters) |param| { - comptime if (@TypeOf(param).param_type != .Ordinal) continue; - - if (self.consumed_args == arg_index) { - argit.rewind(); - if (comptime @TypeOf(param).G.multi) { - while (argit.peek()) |_| try self.apply_param_values(param, argit, false); - } else { - try self.apply_param_values(param, argit, false); - } - self.consumed_args += 1; - return null; - } - - arg_index += 1; - } - - return self.subcommands.get(arg) orelse ParseError.ExtraValue; - } - - inline fn push_intermediate_value( - self: *@This(), - comptime param: anytype, - // @TypeOf(param).G.IntermediateValue() should work but appears to trigger a - // compiler bug: expected pointer, found 'u1' - value: param.IntermediateValue(), - ) ParseError!void { - const gen = @TypeOf(param).G; - if (comptime gen.multi) { - if (@field(self.intermediate, param.name) == null) { - @field(self.intermediate, param.name) = gen.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 => return ParseError.ExtraValue, - 1 => try self.push_intermediate_value(param, argit.next() orelse return ParseError.MissingValue), - else => |total| { - 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.MissingValue; - list.append(next) catch return ParseError.UnexpectedFailure; - } - if (bounded and argit.next() != null) return ParseError.ExtraValue; - - try self.push_intermediate_value(param, list); - }, - }, - } - } - - 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 parameters) |param| { - if (comptime param.env_var) |env_var| blk: { - if (@field(self.intermediate, param.name) != null) break :blk; - const val = env.get(env_var) orelse break :blk; - if (comptime @TypeOf(param).G.value_count == .flag) { - try self.push_intermediate_value(param, val); - } else { - try self.apply_fused_values(param, val); - } - } - } - } - - fn convert(self: *@This(), context: *UserContext) ParseError!void { - inline for (comptime parameters) |param| { - if (comptime param.eager) { - try self.convert_param(param, context); - } - } - - inline for (comptime parameters) |param| { - if (comptime !param.eager) { - try self.convert_param(param, context); - } - } - } - - fn convert_param(self: *@This(), comptime param: anytype, context: *UserContext) ParseError!void { - if (@field(self.intermediate, param.name)) |intermediate| { - @field(self.output, param.name) = try param.converter(context, intermediate); - } else { - if (comptime param.required) { - return ParseError.RequiredMissing; - } else { - @field(self.output, param.name) = null; - return; - } - } - } - }; -} - -fn HelpBuilder(comptime command: anytype) type { - _ = command; -} - -pub fn command_builder(comptime UserContext: type) CommandBuilder(UserContext) { - return CommandBuilder(UserContext){}; -} - -const Choice = enum { first, second }; - -const cli = cmd: { - var cmd = command_builder(u32); - cmd.add_option(.{ .OutputType = struct { u8, u8 } }, .{ - .name = "test", - .short_tag = "-t", - .long_tag = "--test", - .env_var = "NOCLIP_TEST", - }); - cmd.add_option(.{ .OutputType = Choice }, .{ - .name = "choice", - .short_tag = "-c", - .long_tag = "--choice", - .env_var = "NOCLIP_CHOICE", - }); - 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_flag(.{ .multi = true }, .{ - .name = "multiflag", - .truthy = .{ .short_tag = "-M" }, - .env_var = "NOCLIP_MULTIFLAG", - }); - cmd.add_argument(.{ .OutputType = []const u8 }, .{ - .name = "arg", - }); - - break :cmd cmd; -}; - -const subcommand = cmd: { - var cmd = command_builder(void); - cmd.add_flag(.{}, .{ - .name = "flag", - .truthy = .{ .short_tag = "-f", .long_tag = "--flag" }, - .falsy = .{ .long_tag = "--no-flag" }, - .env_var = "NOCLIP_SUBFLAG", - }); - cmd.add_argument(.{ .OutputType = []const u8 }, .{ .name = "argument" }); - break :cmd cmd; -}; - -fn sub_handler(_: *void, result: subcommand.Output()) !void { - std.debug.print("subcommand: {s}\n", .{result.argument}); -} - -fn cli_handler(context: *u32, result: cli.Output()) !void { - _ = context; - - // std.debug.print("callback is working {any}\n", .{result.multi.?.items}); - // std.debug.print("callback is working {any}\n", .{result.multiflag.?.items}); - std.debug.print("callback is working {any}\n", .{result.choice}); -} - -pub fn main() !void { - var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); - defer arena.deinit(); - const allocator = arena.allocator(); - - var parser = cli.bind(cli_handler, allocator); - var context: u32 = 2; - - var subcon = subcommand.bind(sub_handler, allocator); - try parser.add_subcommand("verb", subcon.interface()); - - const iface = parser.interface(&context); - try iface.execute(); -} diff --git a/source/errors.zig b/source/errors.zig new file mode 100644 index 0000000..d54e11a --- /dev/null +++ b/source/errors.zig @@ -0,0 +1,16 @@ +pub const ConversionError = error { + ConversionFailed, +}; + +pub const ParseError = error{ + UnexpectedFailure, + EmptyArgs, + MissingValue, + ExtraValue, + FusedShortTagValueMissing, + UnknownLongTagParameter, + UnknownShortTagParameter, + RequiredParameterMissing, +}; + +pub const NoclipError = ParseError || ConversionError; diff --git a/source/handlers.zig b/source/handlers.zig deleted file mode 100644 index 6aa5acf..0000000 --- a/source/handlers.zig +++ /dev/null @@ -1,38 +0,0 @@ -const std = @import("std"); -const builtin = std.builtin; - -const params = @import("./params.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: params.ParameterArgs) type { - return *const fn (args.UserContext, []const u8) anyerror!args.Output; -} - -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), - .Pointer => |info| { - if (info.size == .Slice and info.child == u8) { - return stringHandler(args.UserContext); - } - return null; - }, - else => return null, - } -} diff --git a/source/help.zig b/source/help.zig new file mode 100644 index 0000000..3ed22f7 --- /dev/null +++ b/source/help.zig @@ -0,0 +1,4 @@ + +fn HelpBuilder(comptime command: anytype) type { + _ = command; +} diff --git a/source/meta.zig b/source/meta.zig index 68f3adb..c7a8bc3 100644 --- a/source/meta.zig +++ b/source/meta.zig @@ -117,8 +117,8 @@ pub const MutableTuple = struct { 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)}); + self.pointers = self.pointers ++ &[_]*const anyopaque{@as(*const anyopaque, &item)}; + self.types = self.types ++ &[_]type{@TypeOf(item)}; } pub fn retrieve(comptime self: @This(), comptime index: comptime_int) self.types[index] { diff --git a/source/noclip.zig b/source/noclip.zig index 44af794..3b4842f 100644 --- a/source/noclip.zig +++ b/source/noclip.zig @@ -1,717 +1,9 @@ -// Copyright (c) 2022 torque - -// Permission to use, copy, modify, and/or distribute this software for any -// purpose with or without fee is hereby granted, provided that the above -// copyright notice and this permission notice appear in all copies. - -// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE -// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -// PERFORMANCE OF THIS SOFTWARE. - -const std = @import("std"); -const StructField = std.builtin.Type.StructField; -pub const meta = @import("./meta.zig"); -pub const params = @import("./params.zig"); -pub const Command = @import("./bakery.zig").Command; - -pub const OptionError = error{ - BadShortOption, - BadLongOption, - UnknownOption, - MissingOption, - MissingArgument, - ExtraArguments, -}; - -/// spec is a tuple of Option, Flag, and Argument -pub fn CommandParser( - comptime commandData: params.CommandData, - comptime spec: anytype, - comptime UserContext: type, - comptime callback: *const fn (UserContext, CommandResult(spec, UserContext)) anyerror!void, -) type { - const param_count: struct { - opts: comptime_int, - args: comptime_int, - subs: comptime_int, - } = comptime comp: { - var optc = 0; - var argc = 0; - var subc = 0; - for (spec) |param| { - switch (@TypeOf(param).brand) { - .Argument => argc += 1, - .Option, .Flag => optc += 1, - .Command => subc += 1, - } - } - break :comp .{ .opts = optc, .args = argc, .subs = subc }; - }; - - const ResultType = CommandResult(spec, UserContext); - const RequiredType = RequiredTracker(spec); - - const ParseState = enum { Mixed, ForcedArgs }; - - return struct { - pub const brand: params.Brand = .Command; - pub const ContextType = UserContext; - // this should be copied at compile time - var data: params.CommandData = commandData; - - pub fn execute(self: @This(), alloc: std.mem.Allocator, comptime argit_type: type, argit: *argit_type, context: UserContext) !void { - return try self.internal_execute(alloc, argit_type, argit, context, null); - } - - fn internal_execute(self: @This(), alloc: std.mem.Allocator, comptime argit_type: type, argit: *argit_type, context: UserContext, prog: ?[]const u8) !void { - try self.attachSubcommands(alloc); - - var result: ResultType = createCommandresult(); - var required: RequiredType = .{}; - var parseState: ParseState = .Mixed; - - try extractEnvVars(alloc, &result, &required, context); - - // TODO: this does not even slightly work with subcommands - const progName = prog orelse std.fs.path.basename(argit.next() orelse @panic("base, name?")); - - // TODO: only do this if the help flag has been passed. Alternatively, try - // to assemble this at comptime? - var helpDescription: params.CommandData = .{ .name = data.name }; - try buildHelpDescription(progName, &helpDescription, alloc); - defer alloc.free(helpDescription.help); - - var seenArgs: u32 = 0; - argloop: while (argit.next()) |arg| { - if (parseState == .Mixed and arg.len > 1 and arg[0] == '-') { - if (std.mem.eql(u8, "--", arg)) { - // TODO: the way this works, -- only forces argument - // parsing until a subcommand is found. This seems - // reasonable to me, but it may be unexpected that - // `command -a -- subcommand -b` parses b as an option - // flag. We could propagate the forced args flag to - // subcommands, but I'm not sure that would be better. - // - // Another option is to stop parsing altogether when -- - // is hit, but that means that subcommands cannot be - // invoked at the same time as forced arguments, which - // seems worse somehow, as it affects macroscopic CLI - // behavior. - parseState = .ForcedArgs; - continue :argloop; - } - - if (arg[1] == '-') { - // we have a long flag or option - specloop: inline for (spec) |param| { - switch (@TypeOf(param).brand) { - .Option => { - if (param.long) |flag| { - if (std.mem.startsWith(u8, arg, flag) and (flag.len == arg.len or arg[flag.len] == '=')) { - const val = if (flag.len == arg.len) - argit.next() orelse return OptionError.MissingArgument - else - arg[flag.len + 1 ..]; - - if (comptime param.required()) { - @field(required, param.name) = true; - } - - if (param.hideResult == false) { - @field(result, param.name) = try param.handler.?(context, val); - } - continue :argloop; - } - } - }, - .Flag => { - inline for (.{ .{ param.truthy.long, true }, .{ param.falsy.long, false } }) |variant| { - if (variant[0]) |flag| { - if (std.mem.eql(u8, flag, arg)) { - if (param.eager) |handler| { - try handler(context, helpDescription); - } - - if (param.hideResult == false) { - @field(result, param.name) = variant[1]; - } - continue :argloop; - } - } - } - }, - .Argument, .Command => continue :specloop, - } - } - - // nothing matched - return OptionError.UnknownOption; - } else { - // we have a short flag, which may be multiple fused flags - shortloop: for (arg[1..], 0..) |shorty, idx| { - specloop: inline for (spec) |param| { - switch (@TypeOf(param).brand) { - .Option => { - if (param.short) |flag| { - if (flag[1] == shorty) { - if (comptime param.required()) { - @field(required, param.name) = true; - } - - const val = if (arg.len > (idx + 2)) - arg[(idx + 2)..] - else - argit.next() orelse return OptionError.MissingArgument; - - if (param.hideResult == false) { - @field(result, param.name) = try param.handler.?(context, val); - } - continue :argloop; - } - } - }, - .Flag => { - inline for (.{ .{ param.truthy.short, true }, .{ param.falsy.short, false } }) |variant| { - if (variant[0]) |flag| { - if (flag[1] == shorty) { - if (param.eager) |handler| { - try handler(context, helpDescription); - } - - if (param.hideResult == false) { - @field(result, param.name) = variant[1]; - } - continue :shortloop; - } - } - } - }, - .Argument, .Command => continue :specloop, - } - } - // nothing matched - return OptionError.UnknownOption; - } - } - } else { - // we have a subcommand or an Argument. Arguments are parsed first, exclusively. - defer seenArgs += 1; - comptime var idx = 0; - inline for (spec) |param| { - switch (@TypeOf(param).brand) { - .Command => { - const name = @TypeOf(param).data.name; - if (std.mem.eql(u8, name, arg)) { - // we're calling a subcommand - try checkErrors(seenArgs, required); - try callback(context, result); - - const combined = try std.mem.join(alloc, " ", &[_][]const u8{ progName, name }); - defer alloc.free(combined); - return param.internal_execute(alloc, argit_type, argit, context, combined); - } - }, - .Argument => { - if (seenArgs == idx) { - if (comptime param.required()) { - @field(required, param.name) = true; - } - @field(result, param.name) = try param.handler.?(context, arg); - continue :argloop; - } - idx += 1; - }, - else => continue, - } - } - } - } - try checkErrors(seenArgs, required); - try callback(context, result); - } - - fn buildHelpDescription( - progName: []const u8, - inData: *params.CommandData, - alloc: std.mem.Allocator, - ) !void { - var seen: u32 = 0; - var maxlen: usize = 0; - - var argnames: [param_count.args][]const u8 = undefined; - var args: [param_count.args]ParamRow = undefined; - inline for (spec) |param| { - switch (@TypeOf(param).brand) { - .Argument => { - argnames[seen] = param.name; - args[seen] = try describeArgument(param, alloc); - maxlen = @max(args[seen].flags.len, maxlen); - seen += 1; - }, - else => continue, - } - } - - seen = 0; - var rows: [param_count.opts]ParamRow = undefined; - inline for (spec) |param| { - const describer = switch (@TypeOf(param).brand) { - .Option => describeOption, - .Flag => describeFlag, - else => continue, - }; - rows[seen] = try describer(param, alloc); - maxlen = @max(rows[seen].flags.len, maxlen); - seen += 1; - } - - seen = 0; - var subs: [param_count.subs]ParamRow = undefined; - inline for (spec) |param| { - switch (@TypeOf(param).brand) { - .Command => { - subs[seen] = try describeSubcommand(param, alloc); - maxlen = @max(subs[seen].flags.len, maxlen); - seen += 1; - }, - else => continue, - } - } - - var buffer = std.ArrayList(u8).init(alloc); - const writer = buffer.writer(); - - for (argnames) |name| { - try std.fmt.format(writer, " <{s}>", .{name}); - } - - const short_args = try buffer.toOwnedSlice(); - defer alloc.free(short_args); - - try std.fmt.format( - writer, - "Usage: {s}{s}{s}{s}\n\n", - .{ - progName, - if (param_count.opts > 0) " [options]" else "", - if (param_count.args > 0) short_args else "", - if (param_count.subs > 0) " [subcommand] ..." else "", - }, - ); - - try writer.writeAll(data.help); - if (!std.mem.endsWith(u8, data.help, "\n")) { - try writer.writeAll("\n"); - } - - if (param_count.args > 0) { - try writer.writeAll("\nArguments:\n"); - - for (args) |arg| { - defer arg.deinit(alloc); - try std.fmt.format( - writer, - " {[0]s: <[1]}{[2]s}\n", - .{ arg.flags, maxlen + 2, arg.description }, - ); - } - } - - if (param_count.opts > 0) { - try writer.writeAll("\nOptions:\n"); - - for (rows) |row| { - defer row.deinit(alloc); - try std.fmt.format( - writer, - " {[0]s: <[1]}{[2]s}\n", - .{ row.flags, maxlen + 2, row.description }, - ); - } - } - - if (param_count.subs > 0) { - try writer.writeAll("\nSubcommands:\n"); - // try std.fmt.format(writer, "\nSubcommands {d}:\n", .{param_count.subs}); - for (subs) |sub| { - defer sub.deinit(alloc); - try std.fmt.format( - writer, - " {[0]s: <[1]}{[2]s}\n", - .{ sub.flags, maxlen + 2, sub.description }, - ); - } - } - - inData.help = try buffer.toOwnedSlice(); - } - - const ParamRow = struct { - flags: []const u8, - description: []const u8, - - pub fn deinit(self: @This(), alloc: std.mem.Allocator) void { - alloc.free(self.flags); - alloc.free(self.description); - } - }; - - fn describeArgument(comptime param: anytype, alloc: std.mem.Allocator) !ParamRow { - var buffer = std.ArrayList(u8).init(alloc); - const writer = buffer.writer(); - - try writer.writeAll(param.name); - try std.fmt.format(writer, " ({s})", .{param.type_name()}); - - const flags = try buffer.toOwnedSlice(); - - if (param.help) |help| { - try writer.writeAll(help); - } - if (param.required()) { - try writer.writeAll(" [required]"); - } - const description = try buffer.toOwnedSlice(); - - return ParamRow{ .flags = flags, .description = description }; - } - - fn describeOption(comptime param: anytype, alloc: std.mem.Allocator) !ParamRow { - var buffer = std.ArrayList(u8).init(alloc); - const writer = buffer.writer(); - - if (param.envVar) |varName| { - try std.fmt.format(writer, "{s}", .{varName}); - } - if (param.short) |short| { - if (buffer.items.len > 0) { - try writer.writeAll(", "); - } - try writer.writeAll(short); - } - if (param.long) |long| { - if (buffer.items.len > 0) { - try writer.writeAll(", "); - } - try writer.writeAll(long); - } - try std.fmt.format(writer, " ({s})", .{param.type_name()}); - - const flags = try buffer.toOwnedSlice(); - - if (param.help) |help| { - try writer.writeAll(help); - } - if (param.required()) { - try writer.writeAll(" [required]"); - } - const description = try buffer.toOwnedSlice(); - - return ParamRow{ .flags = flags, .description = description }; - } - - fn describeFlag(comptime param: anytype, alloc: std.mem.Allocator) !ParamRow { - var buffer = std.ArrayList(u8).init(alloc); - const writer = buffer.writer(); - - var truthy_seen: bool = false; - var falsy_seen: bool = false; - - if (param.truthy.short) |short| { - try writer.writeAll(short); - truthy_seen = true; - } - if (param.truthy.long) |long| { - if (truthy_seen) { - try writer.writeAll(", "); - } - try writer.writeAll(long); - truthy_seen = true; - } - - if (param.falsy.short) |short| { - if (truthy_seen) { - try writer.writeAll("/"); - } - try writer.writeAll(short); - falsy_seen = true; - } - if (param.falsy.long) |long| { - if (falsy_seen) { - try writer.writeAll(", "); - } else if (truthy_seen) { - try writer.writeAll("/"); - } - try writer.writeAll(long); - falsy_seen = true; - } - - if (param.envVar) |varName| { - try std.fmt.format(writer, " ({s})", .{varName}); - } - - const flags = try buffer.toOwnedSlice(); - - if (param.help) |help| { - try writer.writeAll(help); - } - if (param.required()) { - try writer.writeAll(" [required]"); - } - const description = try buffer.toOwnedSlice(); - - return ParamRow{ .flags = flags, .description = description }; - } - - fn describeSubcommand(comptime param: anytype, alloc: std.mem.Allocator) !ParamRow { - var buffer = std.ArrayList(u8).init(alloc); - const writer = buffer.writer(); - - const paramdata = @TypeOf(param).data; - - try writer.writeAll(paramdata.name); - - const flags = try buffer.toOwnedSlice(); - - try writer.writeAll(paramdata.help); - const description = try buffer.toOwnedSlice(); - - return ParamRow{ .flags = flags, .description = description }; - } - - pub fn OutType() type { - return CommandResult(spec, UserContext); - } - - inline fn checkErrors(seenArgs: u32, required: RequiredType) OptionError!void { - if (seenArgs < param_count.args) { - return OptionError.MissingArgument; - } else if (seenArgs > param_count.args) { - return OptionError.ExtraArguments; - } - - describeError(required); - - inline for (@typeInfo(@TypeOf(required)).Struct.fields) |field| { - if (@field(required, field.name) == false) { - return OptionError.MissingOption; - } - } - } - - 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(*params.CommandData).init(alloc); - } - - inline for (spec) |param| { - switch (@TypeOf(param).brand) { - .Command => { - try data.subcommands.?.append(&@TypeOf(param).data); - }, - else => continue, - } - } - } - - fn scryTruthiness(input: []const u8) bool { - // empty string is falsy. - if (input.len == 0) return false; - - if (input.len <= 5) { - var lowerBuf: [5]u8 = undefined; - const comp = std.ascii.lowerString(&lowerBuf, input); - - inline for ([_][]const u8{ "false", "no", "0" }) |candidate| { - if (std.mem.eql(u8, comp, candidate)) { - return false; - } - } - } - // TODO: actually try float conversion on input string? This seems - // really silly to me, in the context of the shell, but for example - // MY_VAR=0 evaluates to false but MY_VAR=0.0 evaluates to true. And - // if we accept multiple representations of zero, a whole can of - // worms gets opened. Should 0x0 be falsy? 0o0? That's a lot of - // goofy edge cases. - - // any nonempty value is considered to be truthy. - return true; - } - - 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| { - const ParamType = @TypeOf(param); - switch (ParamType.brand) { - .Option => { - if (param.envVar) |want| { - if (env.get(want)) |value| { - if (comptime param.required()) { - @field(required, param.name) = true; - } - - @field(result, param.name) = try param.handler.?(context, value); - } - } - }, - .Flag => { - if (param.envVar) |want| { - if (env.get(want)) |value| { - @field(result, param.name) = scryTruthiness(value); - } - } - }, - .Argument, .Command => continue, - } - } - } - - inline fn createCommandresult() ResultType { - var result: ResultType = undefined; - inline for (spec) |param| { - switch (@TypeOf(param).brand) { - .Command => continue, - else => if (param.hideResult == false) { - @field(result, param.name) = param.default orelse continue; - }, - } - } - return result; - } - }; -} - -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| { - 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; - } - }, - } - } - - var fields: [outsize]StructField = undefined; - - var idx = 0; - for (spec) |param| { - const ParamType = @TypeOf(param); - switch (ParamType.brand) { - .Command => continue, - else => if (param.hideResult == true) continue, - } - - const FieldType = ParamType.ResultType; - - fields[idx] = .{ - .name = param.name, - .type = FieldType, - .default_value = @ptrCast(?*const anyopaque, ¶m.default), - .is_comptime = false, - .alignment = @alignOf(FieldType), - }; - - idx += 1; - } - - return @Type(.{ .Struct = .{ - .layout = .Auto, - .fields = &fields, - .decls = &.{}, - .is_tuple = false, - } }); - } -} - -fn RequiredTracker(comptime spec: anytype) type { - comptime { - // not sure how to do this without iterating twice, so let's iterate - // twice - var outsize = 0; - for (spec) |param| { - 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; - }, - } - } - - var fields: [outsize]StructField = undefined; - - var idx = 0; - for (spec) |param| { - const ParamType = @TypeOf(param); - switch (ParamType.brand) { - .Flag, .Command => continue, - .Argument, .Option => if (param.required()) { - fields[idx] = .{ - .name = param.name, - .type = bool, - .default_value = &false, - .is_comptime = false, - .alignment = @alignOf(bool), - }; - - idx += 1; - }, - } - } - - return @Type(.{ .Struct = .{ - .layout = .Auto, - .fields = &fields, - .decls = &.{}, - .is_tuple = false, - } }); - } -} - -test { - _ = meta; -} +const command = @import("./command.zig"); +const converters = @import("./converters.zig"); +const errors = @import("./errors.zig"); +const help = @import("./help.zig"); +const ncmeta = @import("./meta.zig"); +const parameters = @import("./parameters.zig"); +const parser = @import("./parser.zig"); + +pub const CommandBuilder = command.CommandBuilder; diff --git a/source/parameters.zig b/source/parameters.zig new file mode 100644 index 0000000..3048ac2 --- /dev/null +++ b/source/parameters.zig @@ -0,0 +1,329 @@ +const std = @import("std"); + +const converters = @import("./converters.zig"); +const ConverterSignature = converters.ConverterSignature; + +const ParameterType = enum { + Nominal, + Ordinal, + Executable, +}; + +pub const ValueCount = union(enum) { + flag: void, + count: void, + fixed: u32, +}; + +pub const FlagBias = enum { + falsy, + truthy, + unbiased, + + pub fn string(comptime self: @This()) []const u8 { + return switch (comptime self) { + .truthy => "true", + .falsy => "false", + else => @compileError("flag tag with unbiased bias?"), + }; + } +}; + +pub const ParameterGenerics = struct { + 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, + // since we now use multi in place of greedy values for simplicity, we may want to + // convert this an enum or add an additional flag to distinguish between the + // many-to-many and the many-to-one cases. + + pub fn fixed_value_count(comptime OutputType: type, comptime value_count: ValueCount) ValueCount { + return comptime if (value_count == .fixed) + switch (@typeInfo(OutputType)) { + .Struct => |info| .{ .fixed = info.fields.len }, + .Array => |info| .{ .fixed = info.len }, + // TODO: this is a bit sloppy, but it can be refined later. + // .Pointer covers slices, which may be a many-to-many conversion. + .Pointer => value_count, + else => .{ .fixed = 1 }, + } + else + value_count; + } + + pub fn clone_without_multi(comptime self: @This()) @This() { + return .{ .UserContext = self.UserContext, .OutputType = self.OutputType, .param_type = self.param_type, .value_count = self.value_count, .multi = false }; + } + + pub fn has_context(comptime self: @This()) bool { + return comptime self.UserContext != void; + } + + pub fn is_flag(comptime self: @This()) bool { + return comptime switch (self.value_count) { + .flag, .count => true, + .fixed => false, + }; + } + + 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, + }, + }; + } + + 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), + }, + }; + } + + 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, + }, + }; + } +}; + +// Consider a "namespace" parameter e.g. -Dfoo=val style. The namespace would be "D" and +// it takes the place of the second "-", but otherwise this is a long-style parameter. +// Could be parsed as forced-fused. Would work for flags as well, e.g. -fno-flag +pub fn OptionConfig(comptime generics: ParameterGenerics) type { + return struct { + name: []const u8, + + short_tag: ?[]const u8 = null, + long_tag: ?[]const u8 = null, + env_var: ?[]const u8 = 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, + + exposed: bool = true, + secret: bool = false, + nice_type_name: []const u8 = @typeName(generics.OutputType), + flag_bias: FlagBias = .unbiased, + }; +} + +pub 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, + }; +} + +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 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.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, + + /// 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, + + /// 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 IntermediateValue(comptime _: @This()) type { + return generics.IntermediateValue(); + } + }; +} + +fn check_short(comptime short_tag: ?[]const u8) void { + 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 { + const long = comptime long_tag orelse return; + if (long.len < 3 or long[0] != '-' or long[1] != '-') @compileError("bad long tag" ++ long); +} + +pub fn make_option(comptime generics: ParameterGenerics, comptime opts: OptionConfig(generics)) OptionType(generics) { + if (opts.short_tag == null and opts.long_tag == null and opts.env_var == null) { + @compileError( + "option " ++ + opts.name ++ + " must have at least one of a short tag, a long tag, or an environment variable", + ); + } + + check_short(opts.short_tag); + check_long(opts.long_tag); + + // perform the logic to create the default converter here? Could be done + // when creating the OptionConfig instead. Need to do it here because there + // may be an error. That's the essential distinction between the OptionType + // 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 @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, + // + .eager = opts.eager, + .required = opts.required, + .global = opts.global, + // + .exposed = opts.exposed, + .secret = opts.secret, + .nice_type_name = opts.nice_type_name, + .flag_bias = opts.flag_bias, + }; +} + +pub 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"); + } + + if (opts.global) { + @compileError("argument " ++ opts.name ++ " cannot be global"); + } + + 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, + // + .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, + }; + } +} diff --git a/source/params.zig b/source/params.zig deleted file mode 100644 index 4eb45c3..0000000 --- a/source/params.zig +++ /dev/null @@ -1,204 +0,0 @@ -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, - nice_type: ?[]const u8 = null, -}; - -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; - 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; - } - - pub fn type_name(self: @This()) []const u8 { - if (args.nice_type) |name| return name; - return @typeName(@TypeOf(self).ResultType); - } - }; - - return result; -} - -pub fn StringOption(comptime UserContext: type) type { - return Option(.{ .Output = []const u8, .UserContext = UserContext, .nice_type = "string" }); -} - -// 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 required(self: @This()) bool { - if (self.default) |_| return false; - return true; - } - - pub fn type_name(_: @This()) []const u8 { - return "bool"; - } - }; -} - -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 and exit", -}; - -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; - 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 type_name(self: @This()) []const u8 { - if (args.nice_type) |name| return name; - return @typeName(@TypeOf(self).ResultType); - } - }; -} - -pub fn StringArg(comptime UserContext: type) type { - return Argument(.{ .Output = []const u8, .UserContext = UserContext, .nice_type = "string" }); -} - -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, -}; diff --git a/source/parser.zig b/source/parser.zig new file mode 100644 index 0000000..4178423 --- /dev/null +++ b/source/parser.zig @@ -0,0 +1,417 @@ +const std = @import("std"); + +const ncmeta = @import("./meta.zig"); +const errors = @import("./errors.zig"); + +const ParseError = errors.ParseError; +const NoclipError = errors.NoclipError; + +pub const ParserInterface = struct { + const Vtable = struct { + execute: *const fn (parser: *anyopaque, context: *anyopaque) anyerror!void, + parse: *const fn (parser: *anyopaque, context: *anyopaque, args: [][:0]u8, env: std.process.EnvMap) anyerror!void, + finish: *const fn (parser: *anyopaque, context: *anyopaque) anyerror!void, + }; + + parser: *anyopaque, + context: *anyopaque, + methods: *const Vtable, + + pub fn execute(self: @This()) anyerror!void { + return try self.methods.execute(self.parser, self.context); + } + + pub fn parse(self: @This(), args: [][:0]u8, env: std.process.EnvMap) anyerror!void { + return try self.methods.parse(self.parser, self.context, args, env); + } + + pub fn finish(self: @This()) anyerror!void { + return try self.methods.finish(self.parser, self.context); + } +}; + +fn InterfaceGen(comptime ParserType: type, comptime UserContext: type) type { + return if (@typeInfo(UserContext) == .Void) struct { + pub fn interface(self: *ParserType) ParserInterface { + return .{ + .parser = self, + .context = @constCast(&void{}), + .methods = &.{ + .execute = ParserType.wrap_execute, + .parse = ParserType.wrap_parse, + .finish = ParserType.wrap_finish, + }, + }; + } + } else struct { + pub fn interface(self: *ParserType, context: *UserContext) ParserInterface { + return .{ + .parser = self, + .context = context, + .methods = &.{ + .execute = ParserType.wrap_execute, + .parse = ParserType.wrap_parse, + .finish = ParserType.wrap_finish, + }, + }; + } + }; +} + +// the parser is generated by the bind method of the CommandBuilder, so we can +// be extremely type-sloppy here, which simplifies the signature. +pub fn Parser(comptime command: anytype, comptime callback: anytype) type { + const UserContext = @TypeOf(command).UserContextType; + const Intermediate = command.Intermediate(); + const Output = command.Output(); + const parameters = command.generate(); + + return struct { + intermediate: Intermediate = .{}, + output: Output = undefined, + consumed_args: u32 = 0, + progname: ?[]const u8 = null, + has_global_tags: bool = false, + allocator: std.mem.Allocator, + subcommands: std.hash_map.StringHashMap(ParserInterface), + subcommand: ?ParserInterface = null, + + pub fn add_subcommand(self: *@This(), verb: []const u8, parser: ParserInterface) !void { + try self.subcommands.put(verb, parser); + } + + // 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(parser: *anyopaque, ctx: *anyopaque) anyerror!void { + const self = @ptrCast(*@This(), @alignCast(@alignOf(*@This()), parser)); + + // this is a slightly annoying hack to work around the problem that void has + // 0 alignment, which alignCast chokes on. + const context = if (@alignOf(UserContext) > 0) + @ptrCast(*UserContext, @alignCast(@alignOf(UserContext), ctx)) + else + @ptrCast(*UserContext, ctx); + return try self.execute(context); + } + + fn wrap_parse(parser: *anyopaque, ctx: *anyopaque, args: [][:0]u8, env: std.process.EnvMap) anyerror!void { + const self = @ptrCast(*@This(), @alignCast(@alignOf(@This()), parser)); + const context = if (@alignOf(UserContext) > 0) + @ptrCast(*UserContext, @alignCast(@alignOf(UserContext), ctx)) + else + @ptrCast(*UserContext, ctx); + return try self.subparse(context, args, env); + } + + fn wrap_finish(parser: *anyopaque, ctx: *anyopaque) anyerror!void { + const self = @ptrCast(*@This(), @alignCast(@alignOf(@This()), parser)); + const context = if (@alignOf(UserContext) > 0) + @ptrCast(*UserContext, @alignCast(@alignOf(UserContext), ctx)) + else + @ptrCast(*UserContext, ctx); + return try self.finish(context); + } + + pub fn subparse(self: *@This(), context: *UserContext, args: [][:0]u8, env: std.process.EnvMap) anyerror!void { + const sliceto = try self.parse(args); + try self.read_environment(env); + // try self.convert_eager(context); + try self.convert(context); + + inline for (@typeInfo(@TypeOf(self.intermediate)).Struct.fields) |field| { + if (@field(self.intermediate, field.name) == null) { + std.debug.print("{s}: null,\n", .{field.name}); + } else { + std.debug.print("{s}: ", .{field.name}); + self.print_value(@field(self.intermediate, field.name).?, ""); + } + } + + if (self.subcommand) |verb| try verb.parse(args[sliceto..], env); + } + + pub fn finish(self: *@This(), context: *UserContext) anyerror!void { + // try self.convert(context); + try callback(context, self.output); + if (self.subcommand) |verb| try verb.finish(); + } + + 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); + defer env.deinit(); + + if (args.len < 1) return ParseError.EmptyArgs; + + self.progname = args[0]; + + try self.subparse(context, args[1..], env); + try self.finish(context); + } + + 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, + ) anyerror!usize { + // run pre-parse pass if we have any global parameters + // try self.preparse() + + var forced_ordinal = false; + var argit = ncmeta.SliceIterator(@TypeOf(args)).wrap(args); + + // there are a LOT of different parsing strategies that can be adopted to + // handle "incorrect" command lines. For example, a --long-style named + // argument could be parsed as an ordered argument if it doesn't match any + // of the specified tag names. However, if the user has not passed `--` + // then it's more likely the erroneous flag is a typo or some other + // erroneous input and should be treated as such. Similarly, handling the + // pair `--long-style --some-value`. if long_style takes one value, + // should --some-value be treated as the value, or should we assume the + // user forgot the value and is specifying a second tag? Getting too clever + // with context (e.g. checking if --some-value is a known tag name) + // probably also violates the principle of least astonishment, as if it + // doesn't match, it could very likely be a typo or other erroneous input. + // In this case we have an out, sort of, as --long-style=--some-value is + // unambiguous in purpose. However, this approach misses for short flags, + // unless we also support a -l=--some-value syntax, which I don't like and + // don't think is a common convention. In this case, I think it is + // reasonable to consume the value without getting fancy, + // e.g. -l --some-value produces 'long_style: "--some-value"'. Odds are, if + // the command line was specified incorrectly, the error will cascade + // through somewhere. + + // another consideration is how to deal with mixed --named and positional + // arguments. Theoretically, fixed quantity positional arguments can be + // unambiguously interspersed with named arguments, but that feels sloppy. + // If a positional argument needs to start with --, we have the -- argument + // to force positional parsing. + + argloop: while (argit.next()) |arg| { + if (!forced_ordinal and std.mem.eql(u8, arg, "--")) { + forced_ordinal = true; + continue :argloop; + } + + if (!forced_ordinal and arg.len > 1 and arg[0] == '-') { + if (arg.len > 2 and arg[1] == '-') { + try self.parse_long_tag(arg, &argit); + continue :argloop; + } else if (arg.len > 1) { + for (arg[1..], 1..) |short, idx| { + try self.parse_short_tag(short, arg.len - idx - 1, &argit); + } + continue :argloop; + } + + // if we've fallen through to here then we will be parsing ordinals + // exclusively from here on out. + forced_ordinal = true; + } + + if (try self.parse_ordinals(arg, &argit)) |verb| { + self.subcommand = verb; + // TODO: return slice of remaining or offset index + return argit.index; + } + } + + return 0; + } + + inline fn parse_long_tag( + self: *@This(), + arg: []const u8, + argit: *ncmeta.SliceIterator([][:0]u8), + ) ParseError!void { + inline for (comptime parameters) |param| { + const PType = @TypeOf(param); + // removing the comptime here causes the compiler to die + comptime if (PType.param_type != .Nominal or param.long_tag == null) continue; + const tag = param.long_tag.?; + + 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 ParseError.UnknownLongTagParameter; + } + + inline fn parse_short_tag( + self: *@This(), + arg: u8, + remaining: usize, + argit: *ncmeta.SliceIterator([][:0]u8), + ) ParseError!void { + inline for (comptime parameters) |param| { + const PType = @TypeOf(param); + // removing the comptime here causes the compiler to die + comptime if (PType.param_type != .Nominal or param.short_tag == null) continue; + const tag = param.short_tag.?; + + if (arg == tag[1]) { + if (comptime !PType.is_flag) + if (remaining > 0) + return ParseError.FusedShortTagValueMissing; + + try self.apply_param_values(param, argit, false); + return; + } + } + + return ParseError.UnknownShortTagParameter; + } + + inline fn parse_ordinals( + self: *@This(), + arg: []const u8, + argit: *ncmeta.SliceIterator([][:0]u8), + ) ParseError!?ParserInterface { + comptime var arg_index: u32 = 0; + inline for (comptime parameters) |param| { + comptime if (@TypeOf(param).param_type != .Ordinal) continue; + + if (self.consumed_args == arg_index) { + argit.rewind(); + if (comptime @TypeOf(param).G.multi) { + while (argit.peek()) |_| try self.apply_param_values(param, argit, false); + } else { + try self.apply_param_values(param, argit, false); + } + self.consumed_args += 1; + return null; + } + + arg_index += 1; + } + + return self.subcommands.get(arg) orelse ParseError.ExtraValue; + } + + inline fn push_intermediate_value( + self: *@This(), + comptime param: anytype, + // @TypeOf(param).G.IntermediateValue() should work but appears to trigger a + // compiler bug: expected pointer, found 'u1' + value: param.IntermediateValue(), + ) ParseError!void { + const gen = @TypeOf(param).G; + if (comptime gen.multi) { + if (@field(self.intermediate, param.name) == null) { + @field(self.intermediate, param.name) = gen.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 => return ParseError.ExtraValue, + 1 => try self.push_intermediate_value(param, argit.next() orelse return ParseError.MissingValue), + else => |total| { + 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.MissingValue; + list.append(next) catch return ParseError.UnexpectedFailure; + } + if (bounded and argit.next() != null) return ParseError.ExtraValue; + + try self.push_intermediate_value(param, list); + }, + }, + } + } + + 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 parameters) |param| { + if (comptime param.env_var) |env_var| blk: { + if (@field(self.intermediate, param.name) != null) break :blk; + const val = env.get(env_var) orelse break :blk; + if (comptime @TypeOf(param).G.value_count == .flag) { + try self.push_intermediate_value(param, val); + } else { + try self.apply_fused_values(param, val); + } + } + } + } + + fn convert(self: *@This(), context: *UserContext) NoclipError!void { + inline for (comptime parameters) |param| { + if (comptime param.eager) { + try self.convert_param(param, context); + } + } + + inline for (comptime parameters) |param| { + if (comptime !param.eager) { + try self.convert_param(param, context); + } + } + } + + fn convert_param(self: *@This(), comptime param: anytype, context: *UserContext) NoclipError!void { + if (@field(self.intermediate, param.name)) |intermediate| { + @field(self.output, param.name) = try param.converter(context, intermediate); + } else { + if (comptime param.required) { + return ParseError.RequiredParameterMissing; + } else if (comptime param.default) |def| { + // this has to be explicitly set because even though we set it as + // the field default, it gets clobbered because self.output is + // initialized as undefined. + @field(self.output, param.name) = def; + } else { + @field(self.output, param.name) = null; + return; + } + } + } + }; +}