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