NOCLIP/source/doodle.zig
torque 0fbbf34156
parser: a lot of work on multiple-value options
Also work on a generic runtime parser interface for attaching
subcommands. This will allow subcommands to live in a mapping or
something at runtime which will simplify their use.
2023-03-30 00:29:46 -07:00

987 lines
38 KiB
Zig

const std = @import("std");
const StructField = std.builtin.Type.StructField;
const converters = @import("./converters.zig");
const ncmeta = @import("./meta.zig");
const ConverterSignature = converters.ConverterSignature;
const ParameterType = enum {
Nominal,
Ordinal,
Executable,
};
const Errors = error{
BadConfiguration,
MissingTag,
ArgumentWithTags,
ArgumentWithEnvVar,
MissingDefaultConverter,
};
const ParseError = error{
UnexpectedFailure,
EmptyArgs,
ValueMissing,
UnexpectedValue,
FusedShortTagValueMissing,
UnknownLongTagParameter,
UnknownShortTagParameter,
};
// in theory, we could also have a flexible value count, which could be followed by
// any number of fixed args and be well-defined. `mv` is a classic example
// of this pattern. But putting that logic in the parser seems to add a lot of
// complexity for little gain. The `mv` use case can be much more easily handled
// with a greedy value and then splitting in the value handler.
const ValueCount = union(enum) {
fixed: u32,
greedy: void,
};
const FlagBias = enum {
falsy,
truthy,
unbiased,
pub fn string(comptime self: @This()) []const u8 {
return switch (self) {
.truthy => "true",
.falsy => "false",
else => @compileError("flag tag with unbiased bias?"),
};
}
};
const OptionResult = union(enum) {
Value: type,
flag: FlagBias,
};
pub const ParameterGenerics = struct {
ContextType: type = void,
result: OptionResult = .{ .Value = []const u8 },
param_type: ParameterType,
pub fn no_context(comptime self: @This()) bool {
return self.ContextType == void;
}
pub fn is_flag(comptime self: @This()) bool {
return self.result == .flag;
}
pub fn ResultType(comptime self: @This()) type {
return switch (self.result) {
.Value => |res| res,
.flag => bool,
};
}
};
const ValuedGenericsBasis = struct { ContextType: type = void, Result: type };
const FlagGenericsBasis = struct { ContextType: type = void, flag_bias: FlagBias = .truthy };
fn tag_generics(comptime basis: ValuedGenericsBasis) ParameterGenerics {
return ParameterGenerics{
.ContextType = basis.ContextType,
.result = .{ .Value = basis.Result },
.param_type = .Nominal,
};
}
fn flag_generics(comptime basis: FlagGenericsBasis) ParameterGenerics {
return ParameterGenerics{
.ContextType = basis.ContextType,
.result = .{ .flag = basis.flag_bias },
.param_type = .Nominal,
};
}
fn arg_generics(comptime basis: ValuedGenericsBasis) ParameterGenerics {
return ParameterGenerics{
.ContextType = basis.ContextType,
.result = .{ .Value = basis.Result },
.param_type = .Ordinal,
};
}
fn OptionConfig(comptime generics: ParameterGenerics) type {
return struct {
name: []const u8,
short_tag: ?[]const u8 = null,
long_tag: ?[]const u8 = null,
env_var: ?[]const u8 = null,
value_count: ValueCount = if (generics.is_flag()) .{ .fixed = 0 } else .{ .fixed = 1 },
default: ?generics.ResultType() = null,
converter: ?ConverterSignature(generics) = null,
description: []const u8 = "", // description for output in help text
eager: bool = false,
required: bool = generics.param_type == .Ordinal,
global: bool = false,
multi: bool = false,
exposed: bool = true,
secret: bool = false,
nice_type_name: []const u8 = @typeName(generics.ResultType()),
};
}
fn OptionType(comptime generics: ParameterGenerics) type {
return struct {
pub const param_type: ParameterType = generics.param_type;
pub const is_flag: bool = generics.is_flag();
pub const flag_bias: FlagBias = if (is_flag) generics.result.flag else .unbiased;
name: []const u8,
short_tag: ?[]const u8,
long_tag: ?[]const u8,
env_var: ?[]const u8,
/// description for output in help text
description: []const u8,
default: ?generics.ResultType(),
converter: ConverterSignature(generics),
/// number of values this option wants to consume
value_count: ValueCount,
/// the option converter will be run eagerly, before full command line
/// validation.
eager: bool,
/// the option cannot be omitted from the command line.
required: bool,
/// this option is parsed in a pre-parsing pass that consumes it. It
/// may be present anywhere on the command line. A different way to
/// solve this problem is by using an environment variable. It must be
/// a tagged option.
global: bool,
/// allow this named parameter to be passed multiple times.
/// values will be appended when it is encountered. If false, only the
/// final encountered instance will be used.
multi: bool,
/// if false, do not expose the resulting value in the output type.
/// the converter must have side effects for this option to do anything.
exposed: bool,
/// do not print help for this parameter
secret: bool,
nice_type_name: []const u8, // friendly type name (string better than []const u8)
pub fn ResultType(comptime self: @This()) type {
// is this the correct way to collapse this?
return comptime if (self.multi)
std.ArrayList(self._RType())
else
self._RType();
}
inline fn _RType(comptime self: @This()) type {
comptime switch (self.value_count) {
.fixed => |count| {
return switch (count) {
0, 1 => generics.ResultType(),
// TODO: use an ArrayList instead? it generalizes a bit better
// (i.e. can use the same codepath for multi-fixed and greedy)
else => [count]generics.ResultType(),
};
},
.greedy => return std.ArrayList(generics.ResultType()),
};
}
};
}
fn check_short(comptime short_tag: ?[]const u8) void {
if (short_tag) |short| {
if (short.len != 2 or short[0] != '-') @compileError("bad short tag" ++ short);
}
}
fn check_long(comptime long_tag: ?[]const u8) void {
if (long_tag) |long| {
if (long.len < 3 or long[0] != '-' or long[1] != '-') @compileError("bad long tag" ++ long);
}
}
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 {
@compileLog(opts);
@compileError("implement me");
};
return OptionType(generics){
.name = opts.name,
.short_tag = opts.short_tag,
.long_tag = opts.long_tag,
.env_var = opts.env_var,
.description = opts.description,
.default = opts.default,
.converter = converter,
.value_count = opts.value_count,
.eager = opts.eager,
.required = opts.required,
.multi = opts.multi,
.exposed = opts.exposed,
.global = opts.global,
.secret = opts.secret,
.nice_type_name = opts.nice_type_name,
};
}
fn make_argument(comptime generics: ParameterGenerics, comptime opts: OptionConfig(generics)) OptionType(generics) {
// TODO: it would technically be possible to support specification of
// ordered arguments through environmental variables, but it doesn't really
// make a lot of sense. The algorithm would consume the env var greedily
if (opts.short_tag != null or opts.long_tag != null or opts.env_var != null) {
@compileLog(opts);
@compileError("argument " ++ opts.name ++ " must not have a long or short tag or an env var");
}
const converter = opts.converter orelse converters.default_converter(generics) orelse {
@compileLog(opts);
@compileError("implement me");
};
if (opts.multi == true) @compileError("argument " ++ opts.name ++ " cannot be multi");
return OptionType(generics){
.name = opts.name,
.short_tag = opts.short_tag,
.long_tag = opts.long_tag,
.env_var = opts.env_var,
.description = opts.description,
.default = opts.default,
.converter = converter,
.value_count = opts.value_count,
.eager = opts.eager,
.required = opts.required,
.multi = opts.multi,
.global = opts.global,
.exposed = opts.exposed,
.secret = opts.secret,
.nice_type_name = opts.nice_type_name,
};
}
const ShortLongPair = struct {
short_tag: ?[]const u8 = null,
long_tag: ?[]const u8 = null,
};
fn FlagBuilderArgs(comptime ContextType: type) type {
return struct {
name: []const u8,
truthy: ?ShortLongPair = null,
falsy: ?ShortLongPair = null,
env_var: ?[]const u8 = null,
description: []const u8 = "",
default: ?bool = null,
converter: ?ConverterSignature(flag_generics(.{ .ContextType = ContextType })) = null,
eager: bool = false,
required: bool = false,
global: bool = false,
multi: bool = false,
exposed: bool = true,
secret: bool = false,
};
}
fn CommandBuilder(comptime ContextType: type) type {
return struct {
param_spec: ncmeta.MutableTuple = .{},
pub const UserContextType = ContextType;
pub fn add_argument(
comptime self: *@This(),
comptime Result: type,
comptime args: OptionConfig(arg_generics(.{ .ContextType = ContextType, .Result = Result })),
) void {
self.param_spec.add(make_argument(
arg_generics(.{ .ContextType = ContextType, .Result = Result }),
args,
));
}
pub fn add_option(
comptime self: *@This(),
comptime Result: type,
comptime args: OptionConfig(tag_generics(.{ .ContextType = ContextType, .Result = Result })),
) void {
const generics = tag_generics(.{ .ContextType = ContextType, .Result = Result });
if (comptime args.value_count == .fixed and args.value_count.fixed == 0) {
@compileError(
"please use add_flag rather than add_option to " ++
"create a 0-argument option",
);
}
self.param_spec.add(make_option(generics, args));
}
pub fn set_help_flag(
comptime self: *@This(),
comptime args: OptionConfig(flag_generics(.{ .ContextType = ContextType, .flag_bias = .truthy })),
) void {
_ = self;
_ = args;
}
pub fn add_flag(
comptime self: *@This(),
comptime build_args: FlagBuilderArgs(ContextType),
) void {
if (build_args.truthy == null and build_args.falsy == null and build_args.env_var == null) {
@compileError(
"flag " ++
build_args.name ++
" must have at least one of truthy flags, falsy flags, or env_var flags",
);
}
if (build_args.truthy) |truthy_pair| {
if (truthy_pair.short_tag == null and truthy_pair.long_tag == null) {
@compileError(
"flag " ++
build_args.name ++
" truthy pair must have at least short or long tags set",
);
}
const generics = flag_generics(.{ .ContextType = ContextType, .flag_bias = .truthy });
const args = OptionConfig(generics){
.name = build_args.name,
.short_tag = truthy_pair.short_tag,
.long_tag = truthy_pair.long_tag,
.env_var = null,
.description = build_args.description,
.value_count = .{ .fixed = 0 },
.default = build_args.default,
.converter = build_args.converter,
.eager = build_args.eager,
.required = build_args.required,
.global = build_args.global,
.multi = build_args.multi,
.exposed = build_args.exposed,
.secret = build_args.secret,
};
self.param_spec.add(make_option(generics, args));
}
if (build_args.falsy) |falsy_pair| {
if (falsy_pair.short_tag == null and falsy_pair.long_tag == null) {
@compileError(
"flag " ++
build_args.name ++
" falsy pair must have at least short or long tags set",
);
}
const generics = flag_generics(.{ .ContextType = ContextType, .flag_bias = .falsy });
const args = OptionConfig(generics){
.name = build_args.name,
.short_tag = falsy_pair.short_tag,
.long_tag = falsy_pair.long_tag,
.env_var = null,
.description = build_args.description,
.value_count = .{ .fixed = 0 },
.default = build_args.default,
.converter = build_args.converter,
.eager = build_args.eager,
.required = build_args.required,
.global = build_args.global,
.multi = build_args.multi,
.exposed = build_args.exposed,
.secret = build_args.secret,
};
self.param_spec.add(make_option(generics, args));
}
if (build_args.env_var) |env_var| {
const generics = flag_generics(.{ .ContextType = ContextType, .flag_bias = .unbiased });
const args = OptionConfig(generics){
.name = build_args.name,
.short_tag = null,
.long_tag = null,
.env_var = env_var,
.description = build_args.description,
.value_count = .{ .fixed = 0 },
.default = build_args.default,
.converter = build_args.converter,
.eager = build_args.eager,
.required = build_args.required,
.global = build_args.global,
.multi = build_args.multi,
.secret = build_args.secret,
.exposed = build_args.exposed,
};
self.param_spec.add(make_option(generics, args));
}
}
fn generate(comptime self: @This()) self.param_spec.TupleType() {
return self.param_spec.realTuple();
}
pub fn CallbackSignature(comptime self: @This()) type {
return *const fn (ContextType, self.Output()) anyerror!void;
}
pub fn Output(comptime self: @This()) type {
comptime {
const spec = self.generate();
var fields: []const StructField = &[0]StructField{};
var flag_skip = 0;
paramloop: for (spec, 0..) |param, idx| {
if (!param.exposed) continue :paramloop;
while (flag_skip > 0) {
flag_skip -= 1;
continue :paramloop;
}
const PType = @TypeOf(param);
if (PType.is_flag) {
var peek = idx + 1;
var bais_seen: [ncmeta.enum_length(FlagBias)]bool = [_]bool{false} ** ncmeta.enum_length(FlagBias);
bais_seen[@enumToInt(PType.flag_bias)] = true;
while (peek < spec.len) : (peek += 1) {
const peek_param = spec[peek];
const PeekType = @TypeOf(peek_param);
if (PeekType.is_flag and std.mem.eql(u8, param.name, peek_param.name)) {
if (bais_seen[@enumToInt(PeekType.flag_bias)] == true) {
@compileError("redundant flag!!!! " ++ param.name);
} else {
bais_seen[@enumToInt(PeekType.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)
param.ResultType()
else
?param.ResultType();
// the wacky comptime slice extension hack
fields = &(@as([fields.len]StructField, fields[0..fields.len].*) ++ [1]StructField{.{
.name = param.name,
.type = FieldType,
.default_value = @ptrCast(?*const anyopaque, &param.default),
.is_comptime = false,
.alignment = @alignOf(FieldType),
}});
}
return @Type(.{ .Struct = .{
.layout = .Auto,
.fields = fields,
.decls = &.{},
.is_tuple = false,
} });
}
}
pub fn Intermediate(comptime self: @This()) type {
comptime {
const spec = self.generate();
var fields: []const StructField = &[0]StructField{};
var flag_skip = 0;
paramloop: for (spec, 0..) |param, idx| {
while (flag_skip > 0) {
flag_skip -= 1;
continue :paramloop;
}
const PType = @TypeOf(param);
if (PType.is_flag) {
var peek = idx + 1;
var bais_seen: [ncmeta.enum_length(FlagBias)]bool = [_]bool{false} ** ncmeta.enum_length(FlagBias);
bais_seen[@enumToInt(PType.flag_bias)] = true;
while (peek < spec.len) : (peek += 1) {
const peek_param = spec[peek];
const PeekType = @TypeOf(peek_param);
if (PeekType.is_flag and std.mem.eql(u8, param.name, peek_param.name)) {
if (bais_seen[@enumToInt(PeekType.flag_bias)] == true) {
@compileError("redundant flag!!!! " ++ param.name);
} else {
bais_seen[@enumToInt(PeekType.flag_bias)] = true;
}
flag_skip += 1;
} else {
break;
}
}
}
// This needs to be reconciled with options that take many
// arguments. We could make all of these be ArrayLists of string
// slices instead... but that makes the parsing code much more allocation heavy.
// The problem is essentially that `--long=multi,value` and `--long multi value`
// evaluate to a different number of strings for the same number of arguments.
const FieldType = switch (param.value_count) {
.fixed => |val| switch (val) {
0, 1 => []const u8,
else => std.ArrayList([]const u8),
},
else => std.ArrayList([]const u8),
};
fields = &(@as([fields.len]StructField, fields[0..fields.len].*) ++ [1]StructField{.{
.name = param.name,
.type = ?FieldType,
.default_value = @ptrCast(?*const anyopaque, &@as(?[]const u8, null)),
.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 };
}
};
}
fn push_unparsed_multi(comptime T: type, comptime field: []const u8, intermediate: *T, value: []const u8, alloc: std.mem.Allocator) !void {
if (@field(intermediate, field) == null) {
@field(intermediate, field) = std.ArrayList([]const u8).init(alloc);
}
try @field(intermediate, field).?.append(value);
}
fn push_unparsed_value(comptime T: type, comptime param: anytype, intermediate: *T, value: []const u8, alloc: std.mem.Allocator) ParseError!void {
switch (comptime param.value_count) {
.fixed => |val| switch (val) {
0, 1 => @field(intermediate, param.name) = value,
else => push_unparsed_multi(T, param.name, intermediate, value, alloc) catch return ParseError.UnexpectedFailure,
},
else => push_unparsed_multi(T, param.name, intermediate, value, alloc) catch return ParseError.UnexpectedFailure,
}
}
fn ParserInterface(comptime ContextType: type) type {
return struct {
ctx: *anyopaque,
methods: *const Interface,
const Interface = struct {
execute: *const fn (ctx: *anyopaque, context: ContextType) anyerror!void,
};
pub fn execute(self: @This(), context: ContextType) anyerror!void {
return try self.methods.execute(self.ctx, context);
}
};
}
// the parser is generated by the bind method of the CommandBuilder, so we can
// be extremely type-sloppy here, which simplifies the signature.
fn Parser(comptime command: anytype, comptime callback: anytype) type {
return struct {
const ContextType = @TypeOf(command).UserContextType;
const Intermediate = command.Intermediate();
const Output = command.Output();
intermediate: Intermediate = .{},
output: Output = undefined,
consumed_args: u32 = 0,
progname: ?[]const u8 = null,
has_global_tags: bool = false,
allocator: std.mem.Allocator,
// pub fn add_subcommand(self: *@This(), verb: []const u8, parser: anytype) void {
// self.subcommands
// }
pub fn interface(self: *@This()) ParserInterface(ContextType) {
return .{ .ctx = self, .methods = &.{ .execute = wrap_execute } };
}
fn wrap_execute(ctx: *anyopaque, context: ContextType) anyerror!void {
const self = @ptrCast(*@This(), @alignCast(@alignOf(@This()), ctx));
return try self.execute(context);
}
pub fn execute(self: *@This(), context: ContextType) 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.parse(args[1..]);
// run eager conversions
// try self.convert_eager()
// run normal conversions
// try self.convert()
// execute callback:
try callback(context, self.output);
inline for (@typeInfo(@TypeOf(self.intermediate)).Struct.fields) |field| {
// @compileLog(@typeName(field.type));
if (comptime std.mem.startsWith(u8, @typeName(field.type), "?array_list.ArrayList")) {
if (@field(self.intermediate, field.name)) |list| {
std.debug.print("{s}: [\n", .{field.name});
for (list.items) |item| std.debug.print(" {s},\n", .{item});
std.debug.print("]\n", .{});
} else {
std.debug.print("{s}: null\n", .{field.name});
}
} else {
std.debug.print("{s}: {?s}\n", .{ field.name, @field(self.intermediate, field.name) });
}
}
}
pub fn parse(
self: *@This(),
args: [][:0]u8,
) anyerror!void {
// actually: don't consider env variables until performing conversions. This
// is the most reasonable way to treat the environment as a
// separate "namespace" for e.g. multi options. we only want to use
// environment values if there is nothing specified on the CLI, which cannot
// be determined until the CLI parsing is complete.
// try self.read_environment(env);
// 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;
}
try self.parse_ordinals(arg, &argit);
}
}
inline fn parse_long_tag(
self: *@This(),
arg: []const u8,
argit: *ncmeta.SliceIterator([][:0]u8),
) ParseError!void {
inline for (comptime command.generate()) |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 (comptime PType.is_flag) {
if (std.mem.eql(u8, arg, tag)) {
try self.apply_param_values(param, comptime PType.flag_bias.string(), argit);
return;
}
} else {
if (std.mem.startsWith(u8, arg, tag)) match: {
// TODO: in case of --long=value we should split value
// on comma, so e.g. --long=one,two which is kinda docker-style.
// This adds complexity. Note that --long=one,two will also
// parse as a single value because we take a different
// codepath. In that case presumably the converter will choke if
// it needs to. Ideally the multi-value stuff would all be
// shoved into the converter layer, but we can't do that due to
// needing to consume multiple argv values in some cases. This
// could be an opportunity to become opinionated about CLI flag
// styles, but I will not do that for the time being.
if (arg.len == tag.len) {
const next = argit.next() orelse return ParseError.ValueMissing;
try self.apply_param_values(param, next, argit);
} else if (arg[tag.len] == '=') {
try self.apply_fused_values(param, arg[tag.len + 1 ..]);
} else break :match;
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 command.generate()) |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 (comptime PType.is_flag) {
if (arg == tag[1]) {
try self.apply_param_values(param, comptime PType.flag_bias.string(), argit);
return;
}
} else {
if (arg == tag[1]) {
if (remaining > 0) return ParseError.FusedShortTagValueMissing;
const next = argit.next() orelse return ParseError.ValueMissing;
try self.apply_param_values(param, next, argit);
return;
}
}
}
return ParseError.UnknownShortTagParameter;
}
inline fn parse_ordinals(
self: *@This(),
arg: []const u8,
argit: *ncmeta.SliceIterator([][:0]u8),
) ParseError!void {
comptime var arg_index: u32 = 0;
inline for (comptime command.generate()) |param| {
comptime if (@TypeOf(param).param_type != .Ordinal) continue;
if (self.consumed_args == arg_index) {
try self.apply_param_values(param, arg, argit);
self.consumed_args += 1;
return;
}
arg_index += 1;
}
// look for subcommands now
}
inline fn apply_param_values(self: *@This(), comptime param: anytype, value: []const u8, argit: *ncmeta.SliceIterator([][:0]u8)) ParseError!void {
try push_unparsed_value(Intermediate, param, &self.intermediate, value, self.allocator);
switch (comptime param.value_count) {
.fixed => |count| switch (count) {
0, 1 => return,
else => |total| {
var consumed: u32 = 1;
while (consumed < total) : (consumed += 1) {
const next = argit.next() orelse return ParseError.ValueMissing;
try push_unparsed_value(
Intermediate,
param,
&self.intermediate,
next,
self.allocator,
);
}
},
},
.greedy => {
while (argit.next()) |next| {
try push_unparsed_value(
Intermediate,
param,
&self.intermediate,
next,
self.allocator,
);
}
},
}
}
inline fn apply_fused_values(self: *@This(), comptime param: anytype, value: []const u8) ParseError!void {
switch (comptime param.value_count) {
.fixed => |count| switch (count) {
0 => return ParseError.UnexpectedValue,
1 => try push_unparsed_value(Intermediate, param, &self.intermediate, value, self.allocator),
else => |total| {
var seen: u32 = 0;
var iterator = std.mem.split(u8, value, ",");
while (iterator.next()) |next| {
try push_unparsed_value(Intermediate, param, &self.intermediate, next, self.allocator);
seen += 1;
}
if (seen < total) return ParseError.ValueMissing else if (seen > total) return ParseError.UnexpectedValue;
},
},
.greedy => {
// huh. this is just an unchecked version of the fixed-many case.
var iterator = std.mem.split(u8, value, ",");
while (iterator.next()) |next| {
try push_unparsed_value(Intermediate, param, &self.intermediate, next, self.allocator);
}
},
}
}
fn read_environment(self: *@This(), env: std.process.EnvMap) !void {
inline for (comptime command.generate()) |param| {
if (comptime param.env_var) |env_var| {
const val = env.get(env_var) orelse return;
push_unparsed_value(
Intermediate,
param,
&self.intermediate,
val,
self.allocator,
) catch return ParseError.UnexpectedFailure;
return;
}
}
}
};
}
fn HelpBuilder(comptime command: anytype) type {
_ = command;
}
pub fn command_builder(comptime ContextType: type) CommandBuilder(ContextType) {
return CommandBuilder(ContextType){};
}
const Choice = enum { first, second };
const cli = cmd: {
var cmd = command_builder(void);
cmd.add_option(u8, .{
.name = "test",
.short_tag = "-t",
.long_tag = "--test",
.env_var = "NOCLIP_TEST",
.value_count = .{ .fixed = 2 },
});
cmd.add_option(Choice, .{
.name = "choice",
.short_tag = "-c",
.long_tag = "--choice",
.env_var = "NOCLIP_CHOICE",
});
cmd.add_flag(.{
.name = "flag",
.truthy = .{ .short_tag = "-f", .long_tag = "--flag" },
.falsy = .{ .long_tag = "--no-flag" },
.env_var = "NOCLIP_FLAG",
});
cmd.add_argument([]const u8, .{
.name = "arg",
// .value_count = .{ .fixed = 3 },
.value_count = .greedy,
});
break :cmd cmd;
};
fn cli_handler(_: void, result: cli.Output()) !void {
_ = result;
std.debug.print("callback is working\n", .{});
}
pub fn main() !void {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const allocator = arena.allocator();
var parser = cli.bind(cli_handler, allocator);
const iface = parser.interface();
try iface.execute({});
}