NOCLIP/source/noclip.zig
torque b1bac01257
all: start organizing into components and add user context support
The user can provide a context type and corresponding value that will
get passed into any executed callbacks. This allows for complex
behavior through side effects and provides a mechanism by which the
user can pass an allocator into argument handlers, etc.

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

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

However, this seems like it will be usable once I get around to writing
the help text generation.
2022-11-27 01:31:24 -08:00

623 lines
25 KiB
Zig

// Copyright (c) 2022 torque <torque@users.noreply.github.com>
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
const std = @import("std");
const StructField = std.builtin.Type.StructField;
pub const meta = @import("./meta.zig");
pub const handlers = @import("./handlers.zig");
const Brand = enum {
Option,
Flag,
Argument,
Command,
};
pub const OptionError = error{
BadShortOption,
BadLongOption,
UnknownOption,
MissingOption,
MissingArgument,
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
pub fn CommandParser(
comptime commandData: CommandData,
comptime spec: anytype,
comptime UserContext: type,
comptime callback: *const fn (UserContext, CommandResult(spec, UserContext)) anyerror!void,
) type {
comptime var argCount = 0;
comptime for (spec) |param| {
switch (@TypeOf(param).brand) {
.Argument => argCount += 1,
.Option, .Flag, .Command => continue,
}
};
const ResultType = CommandResult(spec, UserContext);
const RequiredType = RequiredTracker(spec);
const ParseState = enum { Mixed, ForcedArgs };
return struct {
pub const brand: Brand = .Command;
pub const ContextType = UserContext;
// this should be copied at compile time
var data: CommandData = commandData;
/// parse command line arguments from an iterator
pub fn execute(self: @This(), alloc: std.mem.Allocator, comptime argit_type: type, argit: *argit_type, context: UserContext) !void {
try self.attachSubcommands(alloc);
var result: ResultType = createCommandresult();
var required: RequiredType = .{};
var parseState: ParseState = .Mixed;
try extractEnvVars(alloc, &result, &required, context);
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 => {
// have to force lower the handler to runtime
// var handler = param.handler.?;
if (param.long) |flag| {
if (std.mem.eql(u8, flag, arg)) {
if (comptime param.required()) {
@field(required, param.name) = true;
}
const val = 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.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, data);
}
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..]) |shorty, idx| {
specloop: inline for (spec) |param| {
switch (@TypeOf(param).brand) {
.Option => {
// var handler = param.handler.?;
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, data);
}
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 => {
if (std.mem.eql(u8, @TypeOf(param).data.name, arg)) {
// we're calling a subcommand
try checkErrors(seenArgs, required);
try callback(context, result);
return param.execute(alloc, argit_type, argit, context);
}
},
.Argument => {
if (seenArgs == idx) {
if (comptime param.required()) {
@field(required, param.name) = true;
}
// var handler = param.handler;
@field(result, param.name) = try param.handler.?(context, arg);
continue :argloop;
}
idx += 1;
},
else => continue,
}
}
}
}
try checkErrors(seenArgs, required);
try callback(context, result);
}
inline fn checkErrors(seenArgs: u32, required: RequiredType) OptionError!void {
if (seenArgs < argCount) {
return OptionError.MissingArgument;
} else if (seenArgs > argCount) {
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(*CommandData).init(alloc);
}
inline for (spec) |param| {
switch (@TypeOf(param).brand) {
.Command => {
try data.subcommands.?.append(&@TypeOf(param).data);
},
else => continue,
}
}
}
fn scryTruthiness(alloc: std.mem.Allocator, input: []const u8) !bool {
// empty string is falsy.
if (input.len == 0) return false;
if (input.len <= 5) {
const comp = try std.ascii.allocLowerString(alloc, input);
defer alloc.free(comp);
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 (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) = try scryTruthiness(alloc, 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,
.field_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,
.field_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;
}