2023-03-25 16:08:59 -07:00
|
|
|
const std = @import("std");
|
|
|
|
const StructField = std.builtin.Type.StructField;
|
|
|
|
|
|
|
|
const converters = @import("./converters.zig");
|
|
|
|
const ncmeta = @import("./meta.zig");
|
|
|
|
|
|
|
|
const ConverterSignature = converters.ConverterSignature;
|
|
|
|
|
2023-03-29 23:10:42 -07:00
|
|
|
pub const ParseError = error{
|
2023-03-28 01:07:02 -07:00
|
|
|
UnexpectedFailure,
|
|
|
|
EmptyArgs,
|
2023-03-28 23:35:54 -07:00
|
|
|
MissingValue,
|
|
|
|
ExtraValue,
|
2023-03-25 16:08:59 -07:00
|
|
|
FusedShortTagValueMissing,
|
|
|
|
UnknownLongTagParameter,
|
|
|
|
UnknownShortTagParameter,
|
2023-03-29 23:10:42 -07:00
|
|
|
RequiredMissing,
|
|
|
|
ConversionFailed,
|
2023-03-25 16:08:59 -07:00
|
|
|
};
|
|
|
|
|
2023-03-28 23:35:54 -07:00
|
|
|
const ParameterType = enum {
|
|
|
|
Nominal,
|
|
|
|
Ordinal,
|
|
|
|
Executable,
|
|
|
|
};
|
|
|
|
|
2023-03-28 01:07:02 -07:00
|
|
|
// 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
|
2023-03-29 23:10:42 -07:00
|
|
|
// with a multi value and then splitting in the value handler.
|
2023-03-28 01:07:02 -07:00
|
|
|
const ValueCount = union(enum) {
|
2023-03-28 23:35:54 -07:00
|
|
|
flag: void,
|
|
|
|
count: void,
|
2023-03-28 01:07:02 -07:00
|
|
|
fixed: u32,
|
|
|
|
};
|
|
|
|
|
2023-03-25 16:08:59 -07:00
|
|
|
const FlagBias = enum {
|
|
|
|
falsy,
|
|
|
|
truthy,
|
|
|
|
unbiased,
|
|
|
|
|
2023-03-28 01:07:02 -07:00
|
|
|
pub fn string(comptime self: @This()) []const u8 {
|
2023-03-28 23:35:54 -07:00
|
|
|
return switch (comptime self) {
|
2023-03-25 16:08:59 -07:00
|
|
|
.truthy => "true",
|
2023-03-28 01:07:02 -07:00
|
|
|
.falsy => "false",
|
|
|
|
else => @compileError("flag tag with unbiased bias?"),
|
2023-03-25 16:08:59 -07:00
|
|
|
};
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
pub const ParameterGenerics = struct {
|
2023-03-28 23:35:54 -07:00
|
|
|
UserContext: type = void,
|
|
|
|
OutputType: type = void,
|
2023-03-25 16:08:59 -07:00
|
|
|
param_type: ParameterType,
|
2023-03-28 23:35:54 -07:00
|
|
|
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,
|
2023-03-29 23:10:42 -07:00
|
|
|
// 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 };
|
|
|
|
}
|
2023-03-28 23:35:54 -07:00
|
|
|
|
|
|
|
pub fn has_context(comptime self: @This()) bool {
|
|
|
|
return comptime self.UserContext != void;
|
2023-03-25 16:08:59 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn is_flag(comptime self: @This()) bool {
|
2023-03-28 23:35:54 -07:00
|
|
|
return comptime switch (self.value_count) {
|
|
|
|
.flag, .count => true,
|
2023-03-29 23:10:42 -07:00
|
|
|
.fixed => false,
|
2023-03-28 23:35:54 -07:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
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();
|
2023-03-25 16:08:59 -07:00
|
|
|
}
|
|
|
|
|
2023-03-28 23:35:54 -07:00
|
|
|
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) {
|
2023-03-25 16:08:59 -07:00
|
|
|
.flag => bool,
|
2023-03-28 23:35:54 -07:00
|
|
|
.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,
|
|
|
|
},
|
2023-03-25 16:08:59 -07:00
|
|
|
};
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2023-03-28 23:35:54 -07:00
|
|
|
fn OptionConfig(comptime generics: ParameterGenerics) type {
|
|
|
|
return struct {
|
|
|
|
name: []const u8,
|
2023-03-25 16:08:59 -07:00
|
|
|
|
2023-03-28 23:35:54 -07:00
|
|
|
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,
|
2023-03-25 16:08:59 -07:00
|
|
|
|
2023-03-28 23:35:54 -07:00
|
|
|
exposed: bool = true,
|
|
|
|
secret: bool = false,
|
|
|
|
nice_type_name: []const u8 = @typeName(generics.OutputType),
|
|
|
|
flag_bias: FlagBias = .unbiased,
|
2023-03-25 16:08:59 -07:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2023-03-28 23:35:54 -07:00
|
|
|
fn FlagConfig(comptime generics: ParameterGenerics) type {
|
|
|
|
const ShortLongPair = struct {
|
|
|
|
short_tag: ?[]const u8 = null,
|
|
|
|
long_tag: ?[]const u8 = null,
|
2023-03-25 16:08:59 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
return struct {
|
|
|
|
name: []const u8,
|
|
|
|
|
2023-03-28 23:35:54 -07:00
|
|
|
truthy: ?ShortLongPair = null,
|
|
|
|
falsy: ?ShortLongPair = null,
|
2023-03-25 16:08:59 -07:00
|
|
|
env_var: ?[]const u8 = null,
|
2023-03-28 23:35:54 -07:00
|
|
|
description: []const u8 = "",
|
2023-03-25 16:08:59 -07:00
|
|
|
|
2023-03-28 23:35:54 -07:00
|
|
|
default: ?bool = null,
|
2023-03-25 16:08:59 -07:00
|
|
|
converter: ?ConverterSignature(generics) = null,
|
2023-03-28 01:07:02 -07:00
|
|
|
|
2023-03-25 16:08:59 -07:00
|
|
|
eager: bool = false,
|
2023-03-28 23:35:54 -07:00
|
|
|
required: bool = false,
|
2023-03-28 01:07:02 -07:00
|
|
|
global: bool = false,
|
2023-03-28 23:35:54 -07:00
|
|
|
|
2023-03-25 16:08:59 -07:00
|
|
|
exposed: bool = true,
|
|
|
|
secret: bool = false,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
fn OptionType(comptime generics: ParameterGenerics) type {
|
|
|
|
return struct {
|
2023-03-28 23:35:54 -07:00
|
|
|
pub const G: ParameterGenerics = generics;
|
2023-03-25 16:08:59 -07:00
|
|
|
pub const param_type: ParameterType = generics.param_type;
|
|
|
|
pub const is_flag: bool = generics.is_flag();
|
2023-03-28 23:35:54 -07:00
|
|
|
pub const value_count: ValueCount = generics.value_count;
|
|
|
|
pub const multi: bool = generics.multi;
|
2023-03-25 16:08:59 -07:00
|
|
|
|
|
|
|
name: []const u8,
|
|
|
|
short_tag: ?[]const u8,
|
|
|
|
long_tag: ?[]const u8,
|
|
|
|
env_var: ?[]const u8,
|
2023-03-28 01:07:02 -07:00
|
|
|
/// description for output in help text
|
|
|
|
description: []const u8,
|
|
|
|
|
2023-03-28 23:35:54 -07:00
|
|
|
default: ?generics.OutputType,
|
|
|
|
converter: ConverterSignature(generics),
|
2023-03-28 01:07:02 -07:00
|
|
|
|
|
|
|
/// the option converter will be run eagerly, before full command line
|
|
|
|
/// validation.
|
2023-03-25 16:08:59 -07:00
|
|
|
eager: bool,
|
2023-03-28 01:07:02 -07:00
|
|
|
/// the option cannot be omitted from the command line.
|
2023-03-25 16:08:59 -07:00
|
|
|
required: bool,
|
2023-03-28 01:07:02 -07:00
|
|
|
/// 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,
|
2023-03-28 23:35:54 -07:00
|
|
|
|
2023-03-28 01:07:02 -07:00
|
|
|
/// 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,
|
|
|
|
|
2023-03-28 23:35:54 -07:00
|
|
|
/// 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,
|
2023-03-28 01:07:02 -07:00
|
|
|
|
2023-03-28 23:35:54 -07:00
|
|
|
pub fn IntermediateValue(comptime _: @This()) type {
|
|
|
|
return generics.IntermediateValue();
|
2023-03-28 01:07:02 -07:00
|
|
|
}
|
2023-03-25 16:08:59 -07:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
fn check_short(comptime short_tag: ?[]const u8) void {
|
2023-03-28 23:35:54 -07:00
|
|
|
const short = comptime short_tag orelse return;
|
|
|
|
if (short.len != 2 or short[0] != '-') @compileError("bad short tag" ++ short);
|
2023-03-25 16:08:59 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
fn check_long(comptime long_tag: ?[]const u8) void {
|
2023-03-28 23:35:54 -07:00
|
|
|
const long = comptime long_tag orelse return;
|
|
|
|
if (long.len < 3 or long[0] != '-' or long[1] != '-') @compileError("bad long tag" ++ long);
|
2023-03-25 16:08:59 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
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.
|
2023-03-28 23:35:54 -07:00
|
|
|
const converter = opts.converter orelse
|
2023-03-29 23:10:42 -07:00
|
|
|
(converters.default_converter(generics) orelse @compileError(
|
|
|
|
"no converter provided for " ++
|
|
|
|
opts.name ++
|
|
|
|
"and no default exists",
|
|
|
|
));
|
2023-03-25 16:08:59 -07:00
|
|
|
|
|
|
|
return OptionType(generics){
|
|
|
|
.name = opts.name,
|
2023-03-28 23:35:54 -07:00
|
|
|
//
|
2023-03-25 16:08:59 -07:00
|
|
|
.short_tag = opts.short_tag,
|
|
|
|
.long_tag = opts.long_tag,
|
|
|
|
.env_var = opts.env_var,
|
2023-03-28 23:35:54 -07:00
|
|
|
//
|
2023-03-28 01:07:02 -07:00
|
|
|
.description = opts.description,
|
2023-03-25 16:08:59 -07:00
|
|
|
.default = opts.default,
|
|
|
|
.converter = converter,
|
2023-03-28 23:35:54 -07:00
|
|
|
//
|
2023-03-25 16:08:59 -07:00
|
|
|
.eager = opts.eager,
|
|
|
|
.required = opts.required,
|
2023-03-28 01:07:02 -07:00
|
|
|
.global = opts.global,
|
2023-03-28 23:35:54 -07:00
|
|
|
//
|
|
|
|
.exposed = opts.exposed,
|
2023-03-25 16:08:59 -07:00
|
|
|
.secret = opts.secret,
|
2023-03-28 01:07:02 -07:00
|
|
|
.nice_type_name = opts.nice_type_name,
|
2023-03-28 23:35:54 -07:00
|
|
|
.flag_bias = opts.flag_bias,
|
2023-03-25 16:08:59 -07:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2023-03-28 23:35:54 -07:00
|
|
|
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");
|
|
|
|
}
|
|
|
|
|
2023-03-29 23:10:42 -07:00
|
|
|
if (opts.global) {
|
|
|
|
@compileError("argument " ++ opts.name ++ " cannot be global");
|
|
|
|
}
|
2023-03-28 23:35:54 -07:00
|
|
|
|
2023-03-29 23:10:42 -07:00
|
|
|
const converter = opts.converter orelse
|
|
|
|
(converters.default_converter(generics) orelse @compileError(
|
|
|
|
"no converter provided for " ++
|
|
|
|
opts.name ++
|
|
|
|
"and no default exists",
|
|
|
|
));
|
2023-03-28 23:35:54 -07:00
|
|
|
|
|
|
|
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,
|
|
|
|
};
|
2023-03-25 16:08:59 -07:00
|
|
|
}
|
2023-03-28 23:35:54 -07:00
|
|
|
}
|
2023-03-25 16:08:59 -07:00
|
|
|
|
2023-03-28 23:35:54 -07:00
|
|
|
fn BuilderGenerics(comptime UserContext: type) type {
|
|
|
|
return struct {
|
|
|
|
OutputType: type = void,
|
|
|
|
value_count: ValueCount = .{ .fixed = 1 },
|
|
|
|
multi: bool = false,
|
2023-03-25 16:08:59 -07:00
|
|
|
|
2023-03-28 23:35:54 -07:00
|
|
|
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,
|
2023-03-29 23:10:42 -07:00
|
|
|
.value_count = ParameterGenerics.fixed_value_count(self.OutputType, self.value_count),
|
|
|
|
.multi = self.multi,
|
2023-03-28 23:35:54 -07:00
|
|
|
};
|
|
|
|
}
|
2023-03-28 01:07:02 -07:00
|
|
|
|
2023-03-28 23:35:54 -07:00
|
|
|
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");
|
2023-03-25 16:08:59 -07:00
|
|
|
|
2023-03-28 23:35:54 -07:00
|
|
|
return ParameterGenerics{
|
|
|
|
.UserContext = UserContext,
|
|
|
|
.OutputType = self.OutputType,
|
|
|
|
.param_type = .Nominal,
|
2023-03-29 23:10:42 -07:00
|
|
|
.value_count = ParameterGenerics.fixed_value_count(self.OutputType, self.value_count),
|
2023-03-28 23:35:54 -07:00
|
|
|
.multi = self.multi,
|
|
|
|
};
|
|
|
|
}
|
2023-03-25 16:08:59 -07:00
|
|
|
|
2023-03-28 23:35:54 -07:00
|
|
|
pub fn count_gen(comptime _: @This()) ParameterGenerics {
|
|
|
|
return ParameterGenerics{
|
|
|
|
.UserContext = UserContext,
|
|
|
|
.OutputType = usize,
|
|
|
|
.param_type = .Nominal,
|
|
|
|
.value_count = .count,
|
|
|
|
.multi = true,
|
|
|
|
};
|
|
|
|
}
|
2023-03-25 16:08:59 -07:00
|
|
|
|
2023-03-28 23:35:54 -07:00
|
|
|
pub fn flag_gen(comptime self: @This()) ParameterGenerics {
|
|
|
|
return ParameterGenerics{
|
|
|
|
.UserContext = UserContext,
|
|
|
|
.OutputType = bool,
|
|
|
|
.param_type = .Nominal,
|
|
|
|
.value_count = .flag,
|
|
|
|
.multi = self.multi,
|
|
|
|
};
|
|
|
|
}
|
2023-03-25 16:08:59 -07:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2023-03-28 23:35:54 -07:00
|
|
|
fn CommandBuilder(comptime UserContext: type) type {
|
2023-03-25 16:08:59 -07:00
|
|
|
return struct {
|
|
|
|
param_spec: ncmeta.MutableTuple = .{},
|
|
|
|
|
2023-03-28 23:35:54 -07:00
|
|
|
pub const UserContextType = UserContext;
|
2023-03-25 16:08:59 -07:00
|
|
|
|
|
|
|
pub fn add_argument(
|
|
|
|
comptime self: *@This(),
|
2023-03-28 23:35:54 -07:00
|
|
|
comptime bgen: BuilderGenerics(UserContext),
|
|
|
|
comptime config: OptionConfig(bgen.arg_gen()),
|
2023-03-25 16:08:59 -07:00
|
|
|
) void {
|
2023-03-28 23:35:54 -07:00
|
|
|
self.param_spec.add(make_argument(bgen.arg_gen(), config));
|
2023-03-25 16:08:59 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn add_option(
|
|
|
|
comptime self: *@This(),
|
2023-03-28 23:35:54 -07:00
|
|
|
comptime bgen: BuilderGenerics(UserContext),
|
|
|
|
comptime config: OptionConfig(bgen.opt_gen()),
|
2023-03-25 16:08:59 -07:00
|
|
|
) void {
|
2023-03-28 23:35:54 -07:00
|
|
|
if (comptime bgen.value_count == .fixed and bgen.value_count.fixed == 0) {
|
2023-03-28 01:07:02 -07:00
|
|
|
@compileError(
|
|
|
|
"please use add_flag rather than add_option to " ++
|
|
|
|
"create a 0-argument option",
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-03-28 23:35:54 -07:00
|
|
|
self.param_spec.add(make_option(bgen.opt_gen(), config));
|
2023-03-28 01:07:02 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn set_help_flag(
|
|
|
|
comptime self: *@This(),
|
2023-03-28 23:35:54 -07:00
|
|
|
comptime bgen: BuilderGenerics(UserContext),
|
|
|
|
comptime config: FlagConfig(bgen.flag_gen()),
|
2023-03-28 01:07:02 -07:00
|
|
|
) void {
|
|
|
|
_ = self;
|
2023-03-28 23:35:54 -07:00
|
|
|
_ = config;
|
2023-03-25 16:08:59 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn add_flag(
|
|
|
|
comptime self: *@This(),
|
2023-03-28 23:35:54 -07:00
|
|
|
comptime bgen: BuilderGenerics(UserContext),
|
|
|
|
comptime config: FlagConfig(bgen.flag_gen()),
|
2023-03-25 16:08:59 -07:00
|
|
|
) void {
|
2023-03-28 23:35:54 -07:00
|
|
|
comptime {
|
|
|
|
if (config.truthy == null and config.falsy == null and config.env_var == null) {
|
2023-03-25 16:08:59 -07:00
|
|
|
@compileError(
|
|
|
|
"flag " ++
|
2023-03-28 23:35:54 -07:00
|
|
|
config.name ++
|
|
|
|
" must have at least one of truthy flags, falsy flags, or env_var flags",
|
2023-03-25 16:08:59 -07:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-03-28 23:35:54 -07:00
|
|
|
const generics = bgen.flag_gen();
|
|
|
|
var args = OptionConfig(generics){
|
|
|
|
.name = config.name,
|
|
|
|
//
|
|
|
|
.short_tag = null,
|
|
|
|
.long_tag = null,
|
2023-03-25 16:08:59 -07:00
|
|
|
.env_var = null,
|
2023-03-28 23:35:54 -07:00
|
|
|
//
|
|
|
|
.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",
|
2023-03-25 16:08:59 -07:00
|
|
|
};
|
|
|
|
|
2023-03-28 23:35:54 -07:00
|
|
|
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",
|
|
|
|
);
|
|
|
|
}
|
2023-03-25 16:08:59 -07:00
|
|
|
|
2023-03-28 23:35:54 -07:00
|
|
|
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));
|
2023-03-25 16:08:59 -07:00
|
|
|
}
|
|
|
|
|
2023-03-28 23:35:54 -07:00
|
|
|
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",
|
|
|
|
);
|
|
|
|
}
|
2023-03-25 16:08:59 -07:00
|
|
|
|
2023-03-28 23:35:54 -07:00
|
|
|
args.short_tag = falsy_pair.short_tag;
|
|
|
|
args.long_tag = falsy_pair.long_tag;
|
|
|
|
args.flag_bias = .falsy;
|
2023-03-25 16:08:59 -07:00
|
|
|
|
2023-03-28 23:35:54 -07:00
|
|
|
self.param_spec.add(make_option(generics, args));
|
|
|
|
}
|
|
|
|
|
|
|
|
if (config.env_var) |env_var| {
|
2023-03-29 23:10:42 -07:00
|
|
|
// @compileLog(env_var);
|
2023-03-28 23:35:54 -07:00
|
|
|
args.short_tag = null;
|
|
|
|
args.long_tag = null;
|
|
|
|
args.env_var = env_var;
|
|
|
|
args.flag_bias = .unbiased;
|
2023-03-25 16:08:59 -07:00
|
|
|
|
2023-03-28 23:35:54 -07:00
|
|
|
self.param_spec.add(make_option(generics, args));
|
|
|
|
}
|
2023-03-25 16:08:59 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn generate(comptime self: @This()) self.param_spec.TupleType() {
|
|
|
|
return self.param_spec.realTuple();
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn CallbackSignature(comptime self: @This()) type {
|
2023-03-28 23:35:54 -07:00
|
|
|
return *const fn (*UserContext, self.Output()) anyerror!void;
|
2023-03-25 16:08:59 -07:00
|
|
|
}
|
|
|
|
|
2023-03-28 01:07:02 -07:00
|
|
|
pub fn Output(comptime self: @This()) type {
|
2023-03-25 16:08:59 -07:00
|
|
|
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;
|
2023-03-28 23:35:54 -07:00
|
|
|
var bias_seen: [ncmeta.enum_length(FlagBias)]bool = [_]bool{false} ** ncmeta.enum_length(FlagBias);
|
|
|
|
bias_seen[@enumToInt(param.flag_bias)] = true;
|
2023-03-25 16:08:59 -07:00
|
|
|
while (peek < spec.len) : (peek += 1) {
|
|
|
|
const peek_param = spec[peek];
|
|
|
|
|
2023-03-28 23:35:54 -07:00
|
|
|
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) {
|
2023-03-28 01:07:02 -07:00
|
|
|
@compileError("redundant flag!!!! " ++ param.name);
|
2023-03-25 16:08:59 -07:00
|
|
|
} else {
|
2023-03-28 23:35:54 -07:00
|
|
|
bias_seen[@enumToInt(peek_param.flag_bias)] = true;
|
2023-03-25 16:08:59 -07:00
|
|
|
}
|
|
|
|
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)
|
2023-03-28 23:35:54 -07:00
|
|
|
PType.G.ConvertedType()
|
2023-03-25 16:08:59 -07:00
|
|
|
else
|
2023-03-28 23:35:54 -07:00
|
|
|
?PType.G.ConvertedType();
|
2023-03-25 16:08:59 -07:00
|
|
|
|
|
|
|
// the wacky comptime slice extension hack
|
|
|
|
fields = &(@as([fields.len]StructField, fields[0..fields.len].*) ++ [1]StructField{.{
|
|
|
|
.name = param.name,
|
|
|
|
.type = FieldType,
|
|
|
|
.default_value = @ptrCast(?*const anyopaque, ¶m.default),
|
|
|
|
.is_comptime = false,
|
|
|
|
.alignment = @alignOf(FieldType),
|
|
|
|
}});
|
|
|
|
}
|
|
|
|
|
|
|
|
return @Type(.{ .Struct = .{
|
|
|
|
.layout = .Auto,
|
|
|
|
.fields = fields,
|
|
|
|
.decls = &.{},
|
|
|
|
.is_tuple = false,
|
|
|
|
} });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn Intermediate(comptime self: @This()) type {
|
|
|
|
comptime {
|
|
|
|
const spec = self.generate();
|
|
|
|
var fields: []const StructField = &[0]StructField{};
|
|
|
|
var flag_skip = 0;
|
|
|
|
|
|
|
|
paramloop: for (spec, 0..) |param, idx| {
|
|
|
|
while (flag_skip > 0) {
|
|
|
|
flag_skip -= 1;
|
|
|
|
continue :paramloop;
|
|
|
|
}
|
|
|
|
|
|
|
|
const PType = @TypeOf(param);
|
|
|
|
if (PType.is_flag) {
|
|
|
|
var peek = idx + 1;
|
2023-03-28 23:35:54 -07:00
|
|
|
var bias_seen: [ncmeta.enum_length(FlagBias)]bool = [_]bool{false} ** ncmeta.enum_length(FlagBias);
|
|
|
|
bias_seen[@enumToInt(param.flag_bias)] = true;
|
2023-03-25 16:08:59 -07:00
|
|
|
while (peek < spec.len) : (peek += 1) {
|
|
|
|
const peek_param = spec[peek];
|
|
|
|
|
2023-03-28 23:35:54 -07:00
|
|
|
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) {
|
2023-03-28 01:07:02 -07:00
|
|
|
@compileError("redundant flag!!!! " ++ param.name);
|
2023-03-25 16:08:59 -07:00
|
|
|
} else {
|
2023-03-28 23:35:54 -07:00
|
|
|
bias_seen[@enumToInt(peek_param.flag_bias)] = true;
|
2023-03-25 16:08:59 -07:00
|
|
|
}
|
|
|
|
flag_skip += 1;
|
|
|
|
} else {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-28 23:35:54 -07:00
|
|
|
const FieldType = if (PType.value_count == .count)
|
|
|
|
PType.G.IntermediateType()
|
|
|
|
else
|
|
|
|
?PType.G.IntermediateType();
|
2023-03-28 01:07:02 -07:00
|
|
|
|
2023-03-25 16:08:59 -07:00
|
|
|
fields = &(@as([fields.len]StructField, fields[0..fields.len].*) ++ [1]StructField{.{
|
|
|
|
.name = param.name,
|
2023-03-28 23:35:54 -07:00
|
|
|
.type = FieldType,
|
|
|
|
.default_value = @ptrCast(
|
|
|
|
?*const anyopaque,
|
|
|
|
&@as(
|
|
|
|
FieldType,
|
|
|
|
if (PType.value_count == .count) 0 else null,
|
|
|
|
),
|
|
|
|
),
|
2023-03-25 16:08:59 -07:00
|
|
|
.is_comptime = false,
|
|
|
|
.alignment = @alignOf(?[]const u8),
|
|
|
|
}});
|
|
|
|
}
|
|
|
|
|
|
|
|
return @Type(.{ .Struct = .{
|
|
|
|
.layout = .Auto,
|
|
|
|
.fields = fields,
|
|
|
|
.decls = &.{},
|
|
|
|
.is_tuple = false,
|
|
|
|
} });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-28 01:07:02 -07:00
|
|
|
pub fn bind(
|
|
|
|
comptime self: @This(),
|
|
|
|
comptime callback: self.CallbackSignature(),
|
|
|
|
allocator: std.mem.Allocator,
|
|
|
|
) Parser(self, callback) {
|
2023-03-30 00:19:00 -07:00
|
|
|
return Parser(self, callback){
|
|
|
|
.allocator = allocator,
|
|
|
|
.subcommands = std.hash_map.StringHashMap(ParserInterface).init(allocator),
|
|
|
|
};
|
2023-03-28 01:07:02 -07:00
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2023-03-30 00:19:00 -07:00
|
|
|
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,
|
2023-03-28 23:35:54 -07:00
|
|
|
};
|
2023-03-28 01:07:02 -07:00
|
|
|
|
2023-03-30 00:19:00 -07:00
|
|
|
parser: *anyopaque,
|
|
|
|
context: *anyopaque,
|
|
|
|
methods: *const Vtable,
|
2023-03-28 01:07:02 -07:00
|
|
|
|
2023-03-30 00:19:00 -07:00
|
|
|
pub fn execute(self: @This()) anyerror!void {
|
|
|
|
return try self.methods.execute(self.parser, self.context);
|
|
|
|
}
|
2023-03-28 01:07:02 -07:00
|
|
|
|
2023-03-30 00:19:00 -07:00
|
|
|
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);
|
|
|
|
}
|
2023-03-28 23:35:54 -07:00
|
|
|
|
2023-03-30 00:19:00 -07:00
|
|
|
pub fn finish(self: @This()) anyerror!void {
|
|
|
|
return try self.methods.finish(self.parser, self.context);
|
|
|
|
}
|
|
|
|
};
|
2023-03-28 01:07:02 -07:00
|
|
|
|
2023-03-28 23:35:54 -07:00
|
|
|
fn InterfaceGen(comptime ParserType: type, comptime UserContext: type) type {
|
|
|
|
return if (@typeInfo(UserContext) == .Void) struct {
|
2023-03-30 00:19:00 -07:00
|
|
|
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,
|
|
|
|
},
|
|
|
|
};
|
2023-03-28 23:35:54 -07:00
|
|
|
}
|
|
|
|
} else struct {
|
2023-03-30 00:19:00 -07:00
|
|
|
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,
|
|
|
|
},
|
|
|
|
};
|
2023-03-25 16:08:59 -07:00
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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 {
|
2023-03-28 23:35:54 -07:00
|
|
|
const UserContext = @TypeOf(command).UserContextType;
|
|
|
|
const Intermediate = command.Intermediate();
|
|
|
|
const Output = command.Output();
|
2023-03-29 23:10:42 -07:00
|
|
|
const parameters = command.generate();
|
2023-03-25 16:08:59 -07:00
|
|
|
|
2023-03-28 23:35:54 -07:00
|
|
|
return struct {
|
2023-03-28 01:07:02 -07:00
|
|
|
intermediate: Intermediate = .{},
|
|
|
|
output: Output = undefined,
|
2023-03-25 16:08:59 -07:00
|
|
|
consumed_args: u32 = 0,
|
2023-03-28 01:07:02 -07:00
|
|
|
progname: ?[]const u8 = null,
|
|
|
|
has_global_tags: bool = false,
|
|
|
|
allocator: std.mem.Allocator,
|
2023-03-30 00:19:00 -07:00
|
|
|
subcommands: std.hash_map.StringHashMap(ParserInterface),
|
|
|
|
subcommand: ?ParserInterface = null,
|
2023-03-25 16:08:59 -07:00
|
|
|
|
2023-03-30 00:19:00 -07:00
|
|
|
pub fn add_subcommand(self: *@This(), verb: []const u8, parser: ParserInterface) !void {
|
|
|
|
try self.subcommands.put(verb, parser);
|
|
|
|
}
|
2023-03-25 16:08:59 -07:00
|
|
|
|
2023-03-28 23:35:54 -07:00
|
|
|
// 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);
|
2023-03-28 01:07:02 -07:00
|
|
|
|
2023-03-30 00:19:00 -07:00
|
|
|
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);
|
2023-03-28 01:07:02 -07:00
|
|
|
return try self.execute(context);
|
|
|
|
}
|
|
|
|
|
2023-03-30 00:19:00 -07:00
|
|
|
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);
|
|
|
|
}
|
2023-03-28 01:07:02 -07:00
|
|
|
|
2023-03-30 00:19:00 -07:00
|
|
|
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);
|
|
|
|
}
|
2023-03-28 01:07:02 -07:00
|
|
|
|
2023-03-30 00:19:00 -07:00
|
|
|
pub fn subparse(self: *@This(), context: *UserContext, args: [][:0]u8, env: std.process.EnvMap) anyerror!void {
|
|
|
|
const sliceto = try self.parse(args);
|
2023-03-29 23:10:42 -07:00
|
|
|
try self.read_environment(env);
|
|
|
|
try self.convert(context);
|
2023-03-28 01:07:02 -07:00
|
|
|
|
|
|
|
inline for (@typeInfo(@TypeOf(self.intermediate)).Struct.fields) |field| {
|
2023-03-28 23:35:54 -07:00
|
|
|
if (@field(self.intermediate, field.name) == null) {
|
|
|
|
std.debug.print("{s}: null,\n", .{field.name});
|
2023-03-28 01:07:02 -07:00
|
|
|
} else {
|
2023-03-28 23:35:54 -07:00
|
|
|
std.debug.print("{s}: ", .{field.name});
|
|
|
|
self.print_value(@field(self.intermediate, field.name).?, "");
|
|
|
|
}
|
|
|
|
}
|
2023-03-30 00:19:00 -07:00
|
|
|
|
|
|
|
if (self.subcommand) |verb| try verb.parse(args[sliceto..], env);
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn finish(self: *@This(), context: *UserContext) anyerror!void {
|
|
|
|
try callback(context, self.output);
|
|
|
|
if (self.subcommand) |verb| try verb.finish();
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn execute(self: *@This(), context: *UserContext) anyerror!void {
|
|
|
|
const args = try std.process.argsAlloc(self.allocator);
|
|
|
|
defer std.process.argsFree(self.allocator, args);
|
|
|
|
var env = try std.process.getEnvMap(self.allocator);
|
|
|
|
defer env.deinit();
|
|
|
|
|
|
|
|
if (args.len < 1) return ParseError.EmptyArgs;
|
|
|
|
|
|
|
|
self.progname = args[0];
|
|
|
|
|
|
|
|
try self.subparse(context, args[1..], env);
|
|
|
|
try self.finish(context);
|
2023-03-28 23:35:54 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
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 ++ " ");
|
2023-03-28 01:07:02 -07:00
|
|
|
}
|
2023-03-28 23:35:54 -07:00
|
|
|
std.debug.print("{s}]\n", .{indent});
|
|
|
|
} else {
|
|
|
|
std.debug.print("{s}{s}\n", .{ indent, value });
|
2023-03-28 01:07:02 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-25 16:08:59 -07:00
|
|
|
pub fn parse(
|
|
|
|
self: *@This(),
|
2023-03-28 01:07:02 -07:00
|
|
|
args: [][:0]u8,
|
2023-03-30 00:19:00 -07:00
|
|
|
) anyerror!usize {
|
2023-03-28 01:07:02 -07:00
|
|
|
// 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.
|
2023-03-25 16:08:59 -07:00
|
|
|
|
|
|
|
argloop: while (argit.next()) |arg| {
|
2023-03-28 01:07:02 -07:00
|
|
|
if (!forced_ordinal and std.mem.eql(u8, arg, "--")) {
|
|
|
|
forced_ordinal = true;
|
2023-03-25 16:08:59 -07:00
|
|
|
continue :argloop;
|
|
|
|
}
|
|
|
|
|
2023-03-28 01:07:02 -07:00
|
|
|
if (!forced_ordinal and arg.len > 1 and arg[0] == '-') {
|
2023-03-25 16:08:59 -07:00
|
|
|
if (arg.len > 2 and arg[1] == '-') {
|
2023-03-28 01:07:02 -07:00
|
|
|
try self.parse_long_tag(arg, &argit);
|
2023-03-25 16:08:59 -07:00
|
|
|
continue :argloop;
|
|
|
|
} else if (arg.len > 1) {
|
|
|
|
for (arg[1..], 1..) |short, idx| {
|
2023-03-28 01:07:02 -07:00
|
|
|
try self.parse_short_tag(short, arg.len - idx - 1, &argit);
|
2023-03-25 16:08:59 -07:00
|
|
|
}
|
|
|
|
continue :argloop;
|
|
|
|
}
|
2023-03-28 01:07:02 -07:00
|
|
|
|
|
|
|
// if we've fallen through to here then we will be parsing ordinals
|
|
|
|
// exclusively from here on out.
|
|
|
|
forced_ordinal = true;
|
2023-03-25 16:08:59 -07:00
|
|
|
}
|
|
|
|
|
2023-03-30 00:19:00 -07:00
|
|
|
if (try self.parse_ordinals(arg, &argit)) |verb| {
|
|
|
|
self.subcommand = verb;
|
|
|
|
// TODO: return slice of remaining or offset index
|
|
|
|
return argit.index;
|
|
|
|
}
|
2023-03-25 16:08:59 -07:00
|
|
|
}
|
2023-03-30 00:19:00 -07:00
|
|
|
|
|
|
|
return 0;
|
2023-03-25 16:08:59 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
inline fn parse_long_tag(
|
|
|
|
self: *@This(),
|
|
|
|
arg: []const u8,
|
2023-03-28 01:07:02 -07:00
|
|
|
argit: *ncmeta.SliceIterator([][:0]u8),
|
2023-03-25 16:08:59 -07:00
|
|
|
) ParseError!void {
|
2023-03-29 23:10:42 -07:00
|
|
|
inline for (comptime parameters) |param| {
|
2023-03-25 16:08:59 -07:00
|
|
|
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.?;
|
|
|
|
|
2023-03-28 23:35:54 -07:00
|
|
|
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;
|
2023-03-25 16:08:59 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return ParseError.UnknownLongTagParameter;
|
|
|
|
}
|
|
|
|
|
|
|
|
inline fn parse_short_tag(
|
|
|
|
self: *@This(),
|
|
|
|
arg: u8,
|
|
|
|
remaining: usize,
|
2023-03-28 01:07:02 -07:00
|
|
|
argit: *ncmeta.SliceIterator([][:0]u8),
|
2023-03-25 16:08:59 -07:00
|
|
|
) ParseError!void {
|
2023-03-29 23:10:42 -07:00
|
|
|
inline for (comptime parameters) |param| {
|
2023-03-25 16:08:59 -07:00
|
|
|
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.?;
|
|
|
|
|
2023-03-28 23:35:54 -07:00
|
|
|
if (arg == tag[1]) {
|
|
|
|
if (comptime !PType.is_flag)
|
|
|
|
if (remaining > 0)
|
|
|
|
return ParseError.FusedShortTagValueMissing;
|
|
|
|
|
|
|
|
try self.apply_param_values(param, argit, false);
|
|
|
|
return;
|
2023-03-25 16:08:59 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return ParseError.UnknownShortTagParameter;
|
|
|
|
}
|
|
|
|
|
2023-03-28 01:07:02 -07:00
|
|
|
inline fn parse_ordinals(
|
|
|
|
self: *@This(),
|
|
|
|
arg: []const u8,
|
|
|
|
argit: *ncmeta.SliceIterator([][:0]u8),
|
2023-03-30 00:19:00 -07:00
|
|
|
) ParseError!?ParserInterface {
|
2023-03-25 16:08:59 -07:00
|
|
|
comptime var arg_index: u32 = 0;
|
2023-03-29 23:10:42 -07:00
|
|
|
inline for (comptime parameters) |param| {
|
2023-03-28 01:07:02 -07:00
|
|
|
comptime if (@TypeOf(param).param_type != .Ordinal) continue;
|
2023-03-25 16:08:59 -07:00
|
|
|
|
|
|
|
if (self.consumed_args == arg_index) {
|
2023-03-28 23:35:54 -07:00
|
|
|
argit.rewind();
|
2023-03-29 23:10:42 -07:00
|
|
|
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);
|
|
|
|
}
|
2023-03-25 16:08:59 -07:00
|
|
|
self.consumed_args += 1;
|
2023-03-30 00:19:00 -07:00
|
|
|
return null;
|
2023-03-25 16:08:59 -07:00
|
|
|
}
|
2023-03-28 01:07:02 -07:00
|
|
|
|
2023-03-25 16:08:59 -07:00
|
|
|
arg_index += 1;
|
|
|
|
}
|
2023-03-28 01:07:02 -07:00
|
|
|
|
2023-03-30 00:19:00 -07:00
|
|
|
return self.subcommands.get(arg) orelse ParseError.ExtraValue;
|
2023-03-28 01:07:02 -07:00
|
|
|
}
|
|
|
|
|
2023-03-28 23:35:54 -07:00
|
|
|
inline fn push_intermediate_value(
|
|
|
|
self: *@This(),
|
|
|
|
comptime param: anytype,
|
2023-03-29 23:10:42 -07:00
|
|
|
// @TypeOf(param).G.IntermediateValue() should work but appears to trigger a
|
|
|
|
// compiler bug: expected pointer, found 'u1'
|
2023-03-28 23:35:54 -07:00
|
|
|
value: param.IntermediateValue(),
|
|
|
|
) ParseError!void {
|
2023-03-29 23:10:42 -07:00
|
|
|
const gen = @TypeOf(param).G;
|
|
|
|
if (comptime gen.multi) {
|
2023-03-28 23:35:54 -07:00
|
|
|
if (@field(self.intermediate, param.name) == null) {
|
2023-03-29 23:10:42 -07:00
|
|
|
@field(self.intermediate, param.name) = gen.IntermediateType().init(self.allocator);
|
2023-03-28 23:35:54 -07:00
|
|
|
}
|
|
|
|
@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;
|
2023-03-28 01:07:02 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-28 23:35:54 -07:00
|
|
|
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,
|
2023-03-28 01:07:02 -07:00
|
|
|
.fixed => |count| switch (count) {
|
2023-03-28 23:35:54 -07:00
|
|
|
0 => return ParseError.ExtraValue,
|
|
|
|
1 => try self.push_intermediate_value(param, argit.next() orelse return ParseError.MissingValue),
|
2023-03-28 01:07:02 -07:00
|
|
|
else => |total| {
|
2023-03-28 23:35:54 -07:00
|
|
|
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;
|
2023-03-28 01:07:02 -07:00
|
|
|
}
|
2023-03-28 23:35:54 -07:00
|
|
|
if (bounded and argit.next() != null) return ParseError.ExtraValue;
|
|
|
|
|
|
|
|
try self.push_intermediate_value(param, list);
|
2023-03-28 01:07:02 -07:00
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
2023-03-25 16:08:59 -07:00
|
|
|
}
|
|
|
|
|
2023-03-28 23:35:54 -07:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2023-03-25 16:08:59 -07:00
|
|
|
fn read_environment(self: *@This(), env: std.process.EnvMap) !void {
|
2023-03-29 23:10:42 -07:00
|
|
|
inline for (comptime parameters) |param| {
|
|
|
|
if (comptime param.env_var) |env_var| blk: {
|
|
|
|
if (@field(self.intermediate, param.name) != null) break :blk;
|
|
|
|
const val = env.get(env_var) orelse break :blk;
|
|
|
|
if (comptime @TypeOf(param).G.value_count == .flag) {
|
|
|
|
try self.push_intermediate_value(param, val);
|
|
|
|
} else {
|
|
|
|
try self.apply_fused_values(param, val);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn convert(self: *@This(), context: *UserContext) ParseError!void {
|
|
|
|
inline for (comptime parameters) |param| {
|
|
|
|
if (comptime param.eager) {
|
|
|
|
try self.convert_param(param, context);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
inline for (comptime parameters) |param| {
|
|
|
|
if (comptime !param.eager) {
|
|
|
|
try self.convert_param(param, context);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn convert_param(self: *@This(), comptime param: anytype, context: *UserContext) ParseError!void {
|
|
|
|
if (@field(self.intermediate, param.name)) |intermediate| {
|
|
|
|
@field(self.output, param.name) = try param.converter(context, intermediate);
|
|
|
|
} else {
|
|
|
|
if (comptime param.required) {
|
|
|
|
return ParseError.RequiredMissing;
|
|
|
|
} else {
|
|
|
|
@field(self.output, param.name) = null;
|
2023-03-28 01:07:02 -07:00
|
|
|
return;
|
2023-03-25 16:08:59 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
fn HelpBuilder(comptime command: anytype) type {
|
|
|
|
_ = command;
|
|
|
|
}
|
|
|
|
|
2023-03-28 23:35:54 -07:00
|
|
|
pub fn command_builder(comptime UserContext: type) CommandBuilder(UserContext) {
|
|
|
|
return CommandBuilder(UserContext){};
|
2023-03-25 16:08:59 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
const Choice = enum { first, second };
|
|
|
|
|
|
|
|
const cli = cmd: {
|
2023-03-28 23:35:54 -07:00
|
|
|
var cmd = command_builder(u32);
|
2023-03-29 23:10:42 -07:00
|
|
|
cmd.add_option(.{ .OutputType = struct { u8, u8 } }, .{
|
2023-03-25 16:08:59 -07:00
|
|
|
.name = "test",
|
|
|
|
.short_tag = "-t",
|
|
|
|
.long_tag = "--test",
|
|
|
|
.env_var = "NOCLIP_TEST",
|
2023-03-28 01:07:02 -07:00
|
|
|
});
|
2023-03-28 23:35:54 -07:00
|
|
|
cmd.add_option(.{ .OutputType = Choice }, .{
|
2023-03-28 01:07:02 -07:00
|
|
|
.name = "choice",
|
|
|
|
.short_tag = "-c",
|
|
|
|
.long_tag = "--choice",
|
|
|
|
.env_var = "NOCLIP_CHOICE",
|
2023-03-25 16:08:59 -07:00
|
|
|
});
|
2023-03-29 23:10:42 -07:00
|
|
|
cmd.add_option(.{ .OutputType = u8, .multi = true }, .{
|
|
|
|
.name = "multi",
|
|
|
|
.short_tag = "-m",
|
|
|
|
.long_tag = "--multi",
|
|
|
|
.env_var = "NOCLIP_MULTI",
|
|
|
|
});
|
2023-03-28 23:35:54 -07:00
|
|
|
cmd.add_flag(.{}, .{
|
2023-03-25 16:08:59 -07:00
|
|
|
.name = "flag",
|
|
|
|
.truthy = .{ .short_tag = "-f", .long_tag = "--flag" },
|
|
|
|
.falsy = .{ .long_tag = "--no-flag" },
|
|
|
|
.env_var = "NOCLIP_FLAG",
|
|
|
|
});
|
2023-03-29 23:10:42 -07:00
|
|
|
cmd.add_flag(.{ .multi = true }, .{
|
|
|
|
.name = "multiflag",
|
|
|
|
.truthy = .{ .short_tag = "-M" },
|
|
|
|
.env_var = "NOCLIP_MULTIFLAG",
|
|
|
|
});
|
2023-03-30 00:19:00 -07:00
|
|
|
cmd.add_argument(.{ .OutputType = []const u8 }, .{
|
2023-03-28 23:35:54 -07:00
|
|
|
.name = "arg",
|
2023-03-28 01:07:02 -07:00
|
|
|
});
|
2023-03-25 16:08:59 -07:00
|
|
|
|
|
|
|
break :cmd cmd;
|
|
|
|
};
|
|
|
|
|
2023-03-30 00:19:00 -07:00
|
|
|
const subcommand = cmd: {
|
|
|
|
var cmd = command_builder(void);
|
|
|
|
cmd.add_flag(.{}, .{
|
|
|
|
.name = "flag",
|
|
|
|
.truthy = .{ .short_tag = "-f", .long_tag = "--flag" },
|
|
|
|
.falsy = .{ .long_tag = "--no-flag" },
|
|
|
|
.env_var = "NOCLIP_SUBFLAG",
|
|
|
|
});
|
|
|
|
cmd.add_argument(.{ .OutputType = []const u8 }, .{ .name = "argument" });
|
|
|
|
break :cmd cmd;
|
|
|
|
};
|
|
|
|
|
|
|
|
fn sub_handler(_: *void, result: subcommand.Output()) !void {
|
|
|
|
std.debug.print("subcommand: {s}\n", .{result.argument});
|
|
|
|
}
|
|
|
|
|
2023-03-28 23:35:54 -07:00
|
|
|
fn cli_handler(context: *u32, result: cli.Output()) !void {
|
2023-03-29 23:10:42 -07:00
|
|
|
_ = context;
|
2023-03-28 01:07:02 -07:00
|
|
|
|
2023-03-30 00:19:00 -07:00
|
|
|
// std.debug.print("callback is working {any}\n", .{result.multi.?.items});
|
|
|
|
// std.debug.print("callback is working {any}\n", .{result.multiflag.?.items});
|
2023-03-29 23:10:42 -07:00
|
|
|
std.debug.print("callback is working {any}\n", .{result.choice});
|
2023-03-25 16:08:59 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn main() !void {
|
|
|
|
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
|
|
|
|
defer arena.deinit();
|
|
|
|
const allocator = arena.allocator();
|
|
|
|
|
2023-03-28 01:07:02 -07:00
|
|
|
var parser = cli.bind(cli_handler, allocator);
|
2023-03-28 23:35:54 -07:00
|
|
|
var context: u32 = 2;
|
2023-03-30 00:19:00 -07:00
|
|
|
|
|
|
|
var subcon = subcommand.bind(sub_handler, allocator);
|
|
|
|
try parser.add_subcommand("verb", subcon.interface());
|
|
|
|
|
2023-03-28 23:35:54 -07:00
|
|
|
const iface = parser.interface(&context);
|
|
|
|
try iface.execute();
|
2023-03-25 16:08:59 -07:00
|
|
|
}
|