restructure

the checklist of things to do is continuing to dwindle. hooray. last
big feature push is help text generation. Then improving error
reporting. Then writing some tests. Then writing documentation.

Ay carumba.
This commit is contained in:
torque 2023-03-30 17:00:49 -07:00
parent 21af82acea
commit 3acc412f2e
Signed by: torque
SSH Key Fingerprint: SHA256:nCrXefBNo6EbjNSQhv0nXmEg/VuNq3sMF5b8zETw3Tk
14 changed files with 1267 additions and 2285 deletions

View File

@ -1,27 +1,12 @@
const std = @import("std"); const std = @import("std");
pub fn build(b: *std.build.Builder) void { 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 target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{}); const optimize = b.standardOptimizeOption(.{});
const exe = b.addSharedLibrary(.{ demo(b, target, optimize);
.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);
const tests = b.step("test", "Run unit tests");
const lib_tests = b.addTest(.{ const lib_tests = b.addTest(.{
.name = "tests", .name = "tests",
.root_source_file = .{ .path = "source/noclip.zig" }, .root_source_file = .{ .path = "source/noclip.zig" },
@ -31,3 +16,19 @@ pub fn build(b: *std.build.Builder) void {
tests.dependOn(&lib_tests.step); 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);
}

View File

@ -1,97 +1,89 @@
const std = @import("std"); const std = @import("std");
const noclip = @import("noclip"); const noclip = @import("noclip");
const context: []const u8 = "hello friend"; const CommandBuilder = noclip.CommandBuilder;
const ContextType = @TypeOf(context);
const subcommand = blk: { const Choice = enum { first, second };
var cmd = noclip.Command(ContextType, .{ .name = "verb", .help = "this a sub command" });
cmd.add(cmd.defaultHelpFlag); const cli = cmd: {
cmd.add(cmd.StringOption{ .name = "meta", .short = "-m" }); var cmd = CommandBuilder(u32).init();
cmd.add(cmd.StringArgument{ .name = "argument" }); cmd.add_option(.{ .OutputType = struct { u8, u8 } }, .{
cmd.add(cmd.Argument(u32){ .name = "another", .default = 0 }); .name = "test",
break :blk cmd; .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: { const subcommand = cmd: {
var cmd = noclip.Command(ContextType, .{ var cmd = CommandBuilder(void).init();
.name = "main", cmd.add_flag(.{}, .{
.help = .name = "flag",
\\This is the main CLI entry point for the noclip demo .truthy = .{ .short_tag = "-f", .long_tag = "--flag" },
\\ .falsy = .{ .long_tag = "--no-flag" },
\\This program demonstrates the major features of noclip both in terms of API .env_var = "NOCLIP_SUBFLAG",
\\usage (in its source code) and argument parsing (in its execution).
,
}); });
cmd.add(cmd.defaultHelpFlag); cmd.add_argument(.{ .OutputType = []const u8 }, .{ .name = "argument" });
cmd.add(cmd.Flag{ .name = "flag", .truthy = .{ .short = "-f", .long = "--flag" }, .falsy = .{ .long = "--no-flag" } }); break :cmd cmd;
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;
}; };
fn printHandler(ctx: ContextType, input: []const u8) ![]const u8 { fn sub_handler(_: *void, result: subcommand.Output()) !void {
std.debug.print("ctx: {s}\n", .{ctx}); std.debug.print("subcommand: {s}\n", .{result.argument});
return input;
} }
pub fn subCallback(_: ContextType, result: subcommand.CommandResult()) !void { fn cli_handler(context: *u32, result: cli.Output()) !void {
std.debug.print( _ = context;
\\subcommand: {{
\\ .meta = {s}
\\ .argument = {s}
\\ .another = {d}
\\}}
\\
,
.{ result.meta, result.argument, result.another },
);
}
pub fn mainCommand(_: ContextType, result: command.CommandResult()) !void { std.debug.print("callback is working {any}\n", .{result.choice});
// std.debug.print("{any}", .{result}); std.debug.print("callback is working {d}\n", .{result.default});
std.debug.print(
\\arguments: {{
\\ .flag = {any}
\\ .input = {s}
\\ .output = {s}
\\ .number = {d}
\\}}
\\
,
.{ result.flag, result.input, result.output, result.number },
);
} }
pub fn main() !void { pub fn main() !void {
var parser = command.Parser(mainCommand);
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit(); defer arena.deinit();
const allocator = arena.allocator(); 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();
} }

View File

@ -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){};
}

387
source/command.zig Normal file
View File

@ -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),
};
}
};
}

View File

@ -1,13 +1,14 @@
const std = @import("std"); const std = @import("std");
const ParameterGenerics = @import("./doodle.zig").ParameterGenerics; const ConversionError = @import("./errors.zig").ConversionError;
const CommandError = @import("./doodle.zig").Errors;
const ValueCount = @import("./doodle.zig").ValueCount;
const ParseError = @import("./doodle.zig").ParseError;
const ncmeta = @import("./meta.zig"); 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 { 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) { 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(); const Intermediate = gen.IntermediateType();
return struct { 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 var output = std.ArrayList(gen.OutputType).initCapacity(input.allocator, input.items.len) catch
return ParseError.ConversionFailed; return ConversionError.ConversionFailed;
for (input.items) |item| { for (input.items) |item| {
output.appendAssumeCapacity(try converter(context, 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) { fn flag_converter(comptime gen: ParameterGenerics) ConverterSignature(gen) {
return struct { 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 // treat an empty string as falsy
if (input.len == 0) return false; 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) { fn string_converter(comptime gen: ParameterGenerics) ConverterSignature(gen) {
return struct { 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; return value;
} }
}.handler; }.handler;
@ -84,8 +85,8 @@ fn int_converter(comptime gen: ParameterGenerics) ConverterSignature(gen) {
const IntType = gen.OutputType; const IntType = gen.OutputType;
return struct { return struct {
pub fn handler(_: *gen.UserContext, value: []const u8) ParseError!IntType { pub fn handler(_: *gen.UserContext, value: []const u8) ConversionError!IntType {
return std.fmt.parseInt(IntType, value, 0) catch return ParseError.ConversionFailed; return std.fmt.parseInt(IntType, value, 0) catch return ConversionError.ConversionFailed;
} }
}.handler; }.handler;
} }
@ -96,8 +97,8 @@ fn struct_converter(comptime gen: ParameterGenerics) ConverterSignature(gen) {
const Intermediate = gen.IntermediateType(); const Intermediate = gen.IntermediateType();
return struct { return struct {
pub fn handler(context: *gen.UserContext, value: Intermediate) ParseError!StructType { pub fn handler(context: *gen.UserContext, value: Intermediate) ConversionError!StructType {
if (value.items.len != type_info.fields.len) return ParseError.ConversionFailed; if (value.items.len != type_info.fields.len) return ConversionError.ConversionFailed;
var result: StructType = undefined; var result: StructType = undefined;
inline for (comptime type_info.fields, 0..) |field, idx| { 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; const EnumType = gen.OutputType;
return struct { return struct {
pub fn handler(_: *gen.UserContext, value: []const u8) ParseError!EnumType { pub fn handler(_: *gen.UserContext, value: []const u8) ConversionError!EnumType {
return std.meta.stringToEnum(gen.ConvertedType(), value) orelse ParseError.ConversionFailed; return std.meta.stringToEnum(gen.ConvertedType(), value) orelse ConversionError.ConversionFailed;
} }
}.handler; }.handler;
} }

File diff suppressed because it is too large Load Diff

16
source/errors.zig Normal file
View File

@ -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;

View File

@ -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,
}
}

4
source/help.zig Normal file
View File

@ -0,0 +1,4 @@
fn HelpBuilder(comptime command: anytype) type {
_ = command;
}

View File

@ -117,8 +117,8 @@ pub const MutableTuple = struct {
types: []const type = &[0]type{}, types: []const type = &[0]type{},
pub fn add(comptime self: *@This(), comptime item: anytype) void { 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.pointers = self.pointers ++ &[_]*const anyopaque{@as(*const anyopaque, &item)};
self.types = &(@as([self.types.len]type, self.types[0..self.types.len].*) ++ [1]type{@TypeOf(item)}); self.types = self.types ++ &[_]type{@TypeOf(item)};
} }
pub fn retrieve(comptime self: @This(), comptime index: comptime_int) self.types[index] { pub fn retrieve(comptime self: @This(), comptime index: comptime_int) self.types[index] {

View File

@ -1,717 +1,9 @@
// Copyright (c) 2022 torque <torque@users.noreply.github.com> const command = @import("./command.zig");
const converters = @import("./converters.zig");
// Permission to use, copy, modify, and/or distribute this software for any const errors = @import("./errors.zig");
// purpose with or without fee is hereby granted, provided that the above const help = @import("./help.zig");
// copyright notice and this permission notice appear in all copies. const ncmeta = @import("./meta.zig");
const parameters = @import("./parameters.zig");
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH const parser = @import("./parser.zig");
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, pub const CommandBuilder = command.CommandBuilder;
// 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, &param.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;
}

329
source/parameters.zig Normal file
View File

@ -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,
};
}
}

View File

@ -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,
};

417
source/parser.zig Normal file
View File

@ -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;
}
}
}
};
}