disaster, wrought by mine own hands
This commit is contained in:
@@ -1,531 +0,0 @@
|
||||
const std = @import("std");
|
||||
const StructField = std.builtin.Type.StructField;
|
||||
|
||||
const help = @import("./help.zig");
|
||||
const ncmeta = @import("./meta.zig");
|
||||
const parameters = @import("./parameters.zig");
|
||||
const parser = @import("./parser.zig");
|
||||
|
||||
const ValueCount = parameters.ValueCount;
|
||||
const ParameterGenerics = parameters.ParameterGenerics;
|
||||
const OptionConfig = parameters.OptionConfig;
|
||||
const FlagConfig = parameters.FlagConfig;
|
||||
const ShortLongPair = parameters.ShortLongPair;
|
||||
const FlagBias = parameters.FlagBias;
|
||||
const makeOption = parameters.makeOption;
|
||||
const makeArgument = parameters.makeArgument;
|
||||
|
||||
const Parser = parser.Parser;
|
||||
const ParserInterface = parser.ParserInterface;
|
||||
|
||||
fn BuilderGenerics(comptime UserContext: type) type {
|
||||
return struct {
|
||||
OutputType: type = void,
|
||||
value_count: ValueCount = .{ .fixed = 1 },
|
||||
multi: bool = false,
|
||||
|
||||
pub fn argGen(comptime self: @This()) ParameterGenerics {
|
||||
if (self.value_count == .flag) @compileError("argument may not be a flag");
|
||||
if (self.value_count == .count) @compileError("argument may not be a count");
|
||||
|
||||
return ParameterGenerics{
|
||||
.UserContext = UserContext,
|
||||
.OutputType = self.OutputType,
|
||||
.param_type = .Ordinal,
|
||||
.value_count = ParameterGenerics.fixedValueCount(self.OutputType, self.value_count),
|
||||
.multi = self.multi,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn optGen(comptime self: @This()) ParameterGenerics {
|
||||
if (self.value_count == .flag) @compileError("option may not be a flag");
|
||||
if (self.value_count == .count) @compileError("option may not be a count");
|
||||
|
||||
return ParameterGenerics{
|
||||
.UserContext = UserContext,
|
||||
.OutputType = self.OutputType,
|
||||
.param_type = .Nominal,
|
||||
.value_count = ParameterGenerics.fixedValueCount(self.OutputType, self.value_count),
|
||||
.multi = self.multi,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn countGen(comptime _: @This()) ParameterGenerics {
|
||||
return ParameterGenerics{
|
||||
.UserContext = UserContext,
|
||||
.OutputType = usize,
|
||||
.param_type = .Nominal,
|
||||
.value_count = .count,
|
||||
.multi = true,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn flagGen(comptime self: @This()) ParameterGenerics {
|
||||
return ParameterGenerics{
|
||||
.UserContext = UserContext,
|
||||
.OutputType = bool,
|
||||
.param_type = .Nominal,
|
||||
.value_count = .flag,
|
||||
.multi = self.multi,
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub const GroupOptions = struct {
|
||||
help_flag: ShortLongPair = .{ .short_tag = "-h", .long_tag = "--help" },
|
||||
description: []const u8,
|
||||
};
|
||||
|
||||
pub fn commandGroup(allocator: std.mem.Allocator, comptime options: GroupOptions) !ParserInterface {
|
||||
const cmd = comptime CommandBuilder(void){
|
||||
.help_flag = options.help_flag,
|
||||
.description = options.description,
|
||||
.subcommand_required = true,
|
||||
};
|
||||
|
||||
return try cmd.createInterface(allocator, cmd.noopCallback());
|
||||
}
|
||||
|
||||
fn InterfaceCreator(comptime Command: type) type {
|
||||
return if (Command.ICC.InputType()) |Type|
|
||||
struct {
|
||||
pub fn createInterface(
|
||||
comptime self: Command,
|
||||
allocator: std.mem.Allocator,
|
||||
comptime callback: self.CallbackSignature(),
|
||||
context: Type,
|
||||
) !ParserInterface {
|
||||
return try self._createInterfaceImpl(allocator, callback, context);
|
||||
}
|
||||
}
|
||||
else
|
||||
struct {
|
||||
pub fn createInterface(
|
||||
comptime self: Command,
|
||||
allocator: std.mem.Allocator,
|
||||
comptime callback: self.CallbackSignature(),
|
||||
) !ParserInterface {
|
||||
return try self._createInterfaceImpl(allocator, callback, void{});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub const InterfaceContextCategory = union(enum) {
|
||||
empty,
|
||||
pointer: type,
|
||||
value: type,
|
||||
|
||||
pub fn fromType(comptime ContextType: type) InterfaceContextCategory {
|
||||
return switch (@typeInfo(ContextType)) {
|
||||
.void => .empty,
|
||||
.pointer => |info| if (info.size == .slice) .{ .value = ContextType } else .{ .pointer = ContextType },
|
||||
// technically, i0, u0, and struct{} should be treated as empty, probably
|
||||
else => .{ .value = ContextType },
|
||||
};
|
||||
}
|
||||
|
||||
pub fn InputType(comptime self: InterfaceContextCategory) ?type {
|
||||
return switch (self) {
|
||||
.empty => null,
|
||||
.pointer => |Type| Type,
|
||||
.value => |Type| *const Type,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn OutputType(comptime self: InterfaceContextCategory) type {
|
||||
return switch (self) {
|
||||
.empty => void,
|
||||
.pointer => |Type| Type,
|
||||
.value => |Type| Type,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub fn CommandBuilder(comptime UserContext: type) type {
|
||||
return struct {
|
||||
param_spec: ncmeta.TupleBuilder = .{},
|
||||
// this is a strange hack, but it's easily the path of least resistance
|
||||
help_flag: ShortLongPair = .{ .short_tag = "-h", .long_tag = "--help" },
|
||||
/// if any subcommands are provided, one of them must be specified, or the command has failed.
|
||||
subcommand_required: bool = true,
|
||||
description: []const u8,
|
||||
|
||||
pub const UserContextType = UserContext;
|
||||
pub const ICC: InterfaceContextCategory = InterfaceContextCategory.fromType(UserContextType);
|
||||
|
||||
pub fn createParser(
|
||||
comptime self: @This(),
|
||||
comptime callback: self.CallbackSignature(),
|
||||
allocator: std.mem.Allocator,
|
||||
) !Parser(self, callback) {
|
||||
// note: this is freed in Parser.deinit
|
||||
var arena = try allocator.create(std.heap.ArenaAllocator);
|
||||
arena.* = std.heap.ArenaAllocator.init(allocator);
|
||||
const arena_alloc = arena.allocator();
|
||||
|
||||
return Parser(self, callback){
|
||||
.arena = arena,
|
||||
.allocator = arena_alloc,
|
||||
.subcommands = parser.CommandMap.init(arena_alloc),
|
||||
.help_builder = help.HelpBuilder(self).init(arena_alloc),
|
||||
};
|
||||
}
|
||||
|
||||
pub const ifc = InterfaceCreator(@This());
|
||||
pub const createInterface = ifc.createInterface;
|
||||
|
||||
fn _createInterfaceImpl(
|
||||
comptime self: @This(),
|
||||
allocator: std.mem.Allocator,
|
||||
comptime callback: self.CallbackSignature(),
|
||||
context: (ICC.InputType() orelse void),
|
||||
) !ParserInterface {
|
||||
var arena = try allocator.create(std.heap.ArenaAllocator);
|
||||
arena.* = std.heap.ArenaAllocator.init(allocator);
|
||||
const arena_alloc = arena.allocator();
|
||||
|
||||
var this_parser = try arena_alloc.create(Parser(self, callback));
|
||||
this_parser.* = .{
|
||||
.arena = arena,
|
||||
.allocator = arena_alloc,
|
||||
.subcommands = parser.CommandMap.init(arena_alloc),
|
||||
.help_builder = help.HelpBuilder(self).init(arena_alloc),
|
||||
};
|
||||
|
||||
if (comptime ICC == .empty) {
|
||||
return this_parser.interface();
|
||||
} else {
|
||||
return this_parser.interface(context);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setHelpFlag(
|
||||
comptime self: *@This(),
|
||||
comptime tags: ShortLongPair,
|
||||
) void {
|
||||
self.help_flag = tags;
|
||||
}
|
||||
|
||||
const string_generics = BuilderGenerics(UserContext){ .OutputType = [:0]const u8 };
|
||||
|
||||
pub fn stringOption(
|
||||
comptime self: *@This(),
|
||||
comptime cfg: OptionConfig(string_generics.optGen()),
|
||||
) void {
|
||||
const config = if (cfg.nice_type_name == null)
|
||||
ncmeta.copyStruct(@TypeOf(cfg), cfg, .{ .nice_type_name = "string" })
|
||||
else
|
||||
cfg;
|
||||
|
||||
self.addOption(string_generics, config);
|
||||
}
|
||||
|
||||
pub fn stringArgument(
|
||||
comptime self: *@This(),
|
||||
comptime cfg: OptionConfig(string_generics.argGen()),
|
||||
) void {
|
||||
const config = if (cfg.nice_type_name == null)
|
||||
ncmeta.copyStruct(@TypeOf(cfg), cfg, .{ .nice_type_name = "string" })
|
||||
else
|
||||
cfg;
|
||||
|
||||
self.addArgument(string_generics, config);
|
||||
}
|
||||
|
||||
pub fn simpleFlag(
|
||||
comptime self: *@This(),
|
||||
comptime cfg: FlagConfig(string_generics.flagGen()),
|
||||
) void {
|
||||
self.addFlag(string_generics, cfg);
|
||||
}
|
||||
|
||||
pub fn addArgument(
|
||||
comptime self: *@This(),
|
||||
comptime bgen: BuilderGenerics(UserContext),
|
||||
comptime config: OptionConfig(bgen.argGen()),
|
||||
) void {
|
||||
self.param_spec.add(makeArgument(bgen.argGen(), config));
|
||||
}
|
||||
|
||||
pub fn addOption(
|
||||
comptime self: *@This(),
|
||||
comptime bgen: BuilderGenerics(UserContext),
|
||||
comptime config: OptionConfig(bgen.optGen()),
|
||||
) void {
|
||||
if (comptime bgen.value_count == .fixed and bgen.value_count.fixed == 0) {
|
||||
@compileError(
|
||||
"please use add_flag rather than add_option to " ++
|
||||
"create a 0-argument option",
|
||||
);
|
||||
}
|
||||
|
||||
self.param_spec.add(makeOption(bgen.optGen(), config));
|
||||
}
|
||||
|
||||
pub fn addFlag(
|
||||
comptime self: *@This(),
|
||||
comptime bgen: BuilderGenerics(UserContext),
|
||||
comptime config: FlagConfig(bgen.flagGen()),
|
||||
) void {
|
||||
comptime {
|
||||
if (config.truthy == null and config.falsy == null and config.env_var == null) {
|
||||
@compileError(
|
||||
"flag " ++
|
||||
config.name ++
|
||||
" must have at least one of truthy flags, falsy flags, or env_var flags",
|
||||
);
|
||||
}
|
||||
|
||||
const generics = bgen.flagGen();
|
||||
var args = OptionConfig(generics){
|
||||
.name = config.name,
|
||||
//
|
||||
.short_tag = null,
|
||||
.long_tag = null,
|
||||
.env_var = null,
|
||||
//
|
||||
.description = config.description,
|
||||
.default = config.default,
|
||||
.converter = config.converter,
|
||||
//
|
||||
.eager = config.eager,
|
||||
.required = config.required,
|
||||
.global = config.global,
|
||||
//
|
||||
.secret = config.secret,
|
||||
.nice_type_name = "flag",
|
||||
};
|
||||
|
||||
if (config.truthy) |truthy_pair| {
|
||||
if (truthy_pair.short_tag == null and truthy_pair.long_tag == null) {
|
||||
@compileError(
|
||||
"flag " ++
|
||||
config.name ++
|
||||
" truthy pair must have at least short or long tags set",
|
||||
);
|
||||
}
|
||||
|
||||
args.short_tag = truthy_pair.short_tag;
|
||||
args.long_tag = truthy_pair.long_tag;
|
||||
args.flag_bias = .truthy;
|
||||
|
||||
self.param_spec.add(makeOption(generics, args));
|
||||
}
|
||||
|
||||
if (config.falsy) |falsy_pair| {
|
||||
if (falsy_pair.short_tag == null and falsy_pair.long_tag == null) {
|
||||
@compileError(
|
||||
"flag " ++
|
||||
config.name ++
|
||||
" falsy pair must have at least short or long tags set",
|
||||
);
|
||||
}
|
||||
|
||||
args.short_tag = falsy_pair.short_tag;
|
||||
args.long_tag = falsy_pair.long_tag;
|
||||
args.flag_bias = .falsy;
|
||||
|
||||
self.param_spec.add(makeOption(generics, args));
|
||||
}
|
||||
|
||||
if (config.env_var) |env_var| {
|
||||
// @compileLog(env_var);
|
||||
args.short_tag = null;
|
||||
args.long_tag = null;
|
||||
args.env_var = env_var;
|
||||
args.flag_bias = .unbiased;
|
||||
|
||||
self.param_spec.add(makeOption(generics, args));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate(comptime self: @This()) self.param_spec.TupleType() {
|
||||
return self.param_spec.realTuple();
|
||||
}
|
||||
|
||||
pub fn noopCallback(comptime self: @This()) self.CallbackSignature() {
|
||||
return struct {
|
||||
fn callback(_: UserContextType, _: self.Output()) !void {}
|
||||
}.callback;
|
||||
}
|
||||
|
||||
pub fn CallbackSignature(comptime self: @This()) type {
|
||||
return *const fn (UserContextType, self.Output()) anyerror!void;
|
||||
}
|
||||
|
||||
pub fn Output(comptime self: @This()) type {
|
||||
comptime {
|
||||
const spec = self.generate();
|
||||
var fields: []const StructField = &[_]StructField{};
|
||||
var flag_skip = 0;
|
||||
|
||||
var tag_fields: []const StructField = &[_]StructField{};
|
||||
var env_var_fields: []const StructField = &[_]StructField{};
|
||||
|
||||
paramloop: for (spec, 0..) |param, idx| {
|
||||
const PType = @TypeOf(param);
|
||||
// these three blocks are to check for redundantly defined tags and
|
||||
// environment variables. This only works within a command. It
|
||||
// doesn't support compile time checks for conflict into
|
||||
// subcommands because those are attached at runtime. also, only
|
||||
// global tags and env_vars would conflict, which is less common.
|
||||
if (param.short_tag) |short|
|
||||
tag_fields = tag_fields ++ &[_]StructField{.{
|
||||
// this goofy construct coerces the comptime []const u8 to
|
||||
// [:0]const u8.
|
||||
.name = short ++ "",
|
||||
.type = void,
|
||||
.default_value_ptr = null,
|
||||
.is_comptime = false,
|
||||
.alignment = 0,
|
||||
}};
|
||||
|
||||
if (param.long_tag) |long|
|
||||
tag_fields = tag_fields ++ &[_]StructField{.{
|
||||
.name = long ++ "",
|
||||
.type = void,
|
||||
.default_value_ptr = null,
|
||||
.is_comptime = false,
|
||||
.alignment = 0,
|
||||
}};
|
||||
|
||||
if (param.env_var) |env_var|
|
||||
env_var_fields = env_var_fields ++ &[_]StructField{.{
|
||||
.name = env_var ++ "",
|
||||
.type = void,
|
||||
.default_value_ptr = null,
|
||||
.is_comptime = false,
|
||||
.alignment = 0,
|
||||
}};
|
||||
|
||||
if (!PType.has_output) continue :paramloop;
|
||||
|
||||
while (flag_skip > 0) {
|
||||
flag_skip -= 1;
|
||||
continue :paramloop;
|
||||
}
|
||||
|
||||
if (PType.is_flag) {
|
||||
var peek = idx + 1;
|
||||
var bias_seen: [ncmeta.enumLength(FlagBias)]bool = [_]bool{false} ** ncmeta.enumLength(FlagBias);
|
||||
bias_seen[@intFromEnum(param.flag_bias)] = true;
|
||||
while (peek < spec.len) : (peek += 1) {
|
||||
const peek_param = spec[peek];
|
||||
|
||||
if (@TypeOf(peek_param).is_flag and std.mem.eql(u8, param.name, peek_param.name)) {
|
||||
if (bias_seen[@intFromEnum(peek_param.flag_bias)] == true) {
|
||||
@compileError("redundant flag!!!! " ++ param.name);
|
||||
} else {
|
||||
bias_seen[@intFromEnum(peek_param.flag_bias)] = true;
|
||||
}
|
||||
flag_skip += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// the default field is already the optional type. Stripping
|
||||
// the optional wrapper is an interesting idea for required
|
||||
// fields. I do not foresee this greatly increasing complexity here.
|
||||
const FieldType = if (param.required or param.default != null)
|
||||
PType.G.ConvertedType()
|
||||
else
|
||||
?PType.G.ConvertedType();
|
||||
|
||||
const default = if (param.default) |def| &@as(FieldType, def) else @as(?*const anyopaque, null);
|
||||
|
||||
fields = fields ++ &[_]StructField{.{
|
||||
.name = param.name ++ "",
|
||||
.type = FieldType,
|
||||
.default_value_ptr = @ptrCast(default),
|
||||
.is_comptime = false,
|
||||
.alignment = @alignOf(FieldType),
|
||||
}};
|
||||
}
|
||||
|
||||
_ = @Type(.{ .@"struct" = .{
|
||||
.layout = .auto,
|
||||
.fields = tag_fields,
|
||||
.decls = &.{},
|
||||
.is_tuple = false,
|
||||
} });
|
||||
|
||||
_ = @Type(.{ .@"struct" = .{
|
||||
.layout = .auto,
|
||||
.fields = env_var_fields,
|
||||
.decls = &.{},
|
||||
.is_tuple = false,
|
||||
} });
|
||||
|
||||
return @Type(.{ .@"struct" = .{
|
||||
.layout = .auto,
|
||||
.fields = fields,
|
||||
.decls = &.{},
|
||||
.is_tuple = false,
|
||||
} });
|
||||
}
|
||||
}
|
||||
|
||||
pub fn Intermediate(comptime self: @This()) type {
|
||||
comptime {
|
||||
const spec = self.generate();
|
||||
var fields: []const StructField = &[0]StructField{};
|
||||
var flag_skip = 0;
|
||||
|
||||
paramloop: for (spec, 0..) |param, idx| {
|
||||
while (flag_skip > 0) {
|
||||
flag_skip -= 1;
|
||||
continue :paramloop;
|
||||
}
|
||||
|
||||
const PType = @TypeOf(param);
|
||||
if (PType.is_flag) {
|
||||
var peek = idx + 1;
|
||||
var bias_seen: [ncmeta.enumLength(FlagBias)]bool = [_]bool{false} ** ncmeta.enumLength(FlagBias);
|
||||
bias_seen[@intFromEnum(param.flag_bias)] = true;
|
||||
while (peek < spec.len) : (peek += 1) {
|
||||
const peek_param = spec[peek];
|
||||
|
||||
if (@TypeOf(peek_param).is_flag and std.mem.eql(u8, param.name, peek_param.name)) {
|
||||
if (bias_seen[@intFromEnum(peek_param.flag_bias)] == true) {
|
||||
@compileError("redundant flag!!!! " ++ param.name);
|
||||
} else {
|
||||
bias_seen[@intFromEnum(peek_param.flag_bias)] = true;
|
||||
}
|
||||
flag_skip += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const FieldType = if (PType.value_count == .count)
|
||||
PType.G.IntermediateType()
|
||||
else
|
||||
?PType.G.IntermediateType();
|
||||
|
||||
fields = &(@as([fields.len]StructField, fields[0..fields.len].*) ++ [1]StructField{.{
|
||||
.name = param.name ++ "",
|
||||
.type = FieldType,
|
||||
.default_value_ptr = @ptrCast(&@as(
|
||||
FieldType,
|
||||
if (PType.value_count == .count) 0 else null,
|
||||
)),
|
||||
.is_comptime = false,
|
||||
.alignment = @alignOf(?[]const u8),
|
||||
}});
|
||||
}
|
||||
|
||||
return @Type(.{ .@"struct" = .{
|
||||
.layout = .auto,
|
||||
.fields = fields,
|
||||
.decls = &.{},
|
||||
.is_tuple = false,
|
||||
} });
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
@@ -1,147 +0,0 @@
|
||||
const std = @import("std");
|
||||
|
||||
const ConversionError = @import("./errors.zig").ConversionError;
|
||||
const ncmeta = @import("./meta.zig");
|
||||
const parameters = @import("./parameters.zig");
|
||||
|
||||
const ValueCount = parameters.ValueCount;
|
||||
const ParameterGenerics = parameters.ParameterGenerics;
|
||||
|
||||
const ErrorWriter = std.ArrayList(u8).Writer;
|
||||
|
||||
pub fn ConverterSignature(comptime gen: ParameterGenerics) type {
|
||||
return *const fn (
|
||||
context: gen.UserContext,
|
||||
input: gen.IntermediateType(),
|
||||
failure: ErrorWriter,
|
||||
) ConversionError!gen.ConvertedType();
|
||||
}
|
||||
|
||||
pub fn DefaultConverter(comptime gen: ParameterGenerics) ?ConverterSignature(gen) {
|
||||
return if (comptime gen.multi)
|
||||
MultiConverter(gen)
|
||||
else switch (@typeInfo(gen.OutputType)) {
|
||||
.bool => FlagConverter(gen),
|
||||
.int => IntConverter(gen),
|
||||
.pointer => |info| if (info.size == .slice and info.child == u8)
|
||||
StringConverter(gen)
|
||||
else
|
||||
null,
|
||||
.@"enum" => |info| if (info.is_exhaustive) ChoiceConverter(gen) else null,
|
||||
// TODO: how to handle structs with field defaults? maybe this should only work
|
||||
// for tuples, which I don't think can have defaults.
|
||||
.@"struct" => |info| if (gen.value_count == .fixed and gen.value_count.fixed == info.fields.len)
|
||||
StructConverter(gen)
|
||||
else
|
||||
null,
|
||||
else => null,
|
||||
};
|
||||
}
|
||||
|
||||
fn MultiConverter(comptime gen: ParameterGenerics) ?ConverterSignature(gen) {
|
||||
const converter = DefaultConverter(
|
||||
ncmeta.copyStruct(ParameterGenerics, gen, .{ .multi = false }),
|
||||
) orelse
|
||||
@compileError("no default converter");
|
||||
const Intermediate = gen.IntermediateType();
|
||||
|
||||
return struct {
|
||||
pub fn handler(context: gen.UserContext, input: Intermediate, failure: ErrorWriter) ConversionError!std.ArrayList(gen.OutputType) {
|
||||
var output = std.ArrayList(gen.OutputType).initCapacity(input.allocator, input.items.len) catch
|
||||
return ConversionError.ConversionFailed;
|
||||
|
||||
for (input.items) |item| {
|
||||
output.appendAssumeCapacity(try converter(context, item, failure));
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
}.handler;
|
||||
}
|
||||
|
||||
fn FlagConverter(comptime gen: ParameterGenerics) ConverterSignature(gen) {
|
||||
return struct {
|
||||
pub fn handler(_: gen.UserContext, input: [:0]const u8, _: ErrorWriter) ConversionError!bool {
|
||||
// treat an empty string as falsy
|
||||
if (input.len == 0) return false;
|
||||
|
||||
if (input.len <= 5) {
|
||||
var lowerBuf: [5]u8 = undefined;
|
||||
const comp = std.ascii.lowerString(&lowerBuf, input);
|
||||
|
||||
inline for ([_][]const u8{ "false", "no", "0" }) |candidate| {
|
||||
if (std.mem.eql(u8, comp, candidate)) return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}.handler;
|
||||
}
|
||||
|
||||
fn StringConverter(comptime gen: ParameterGenerics) ConverterSignature(gen) {
|
||||
return struct {
|
||||
pub fn handler(_: gen.UserContext, input: [:0]const u8, _: ErrorWriter) ConversionError![:0]const u8 {
|
||||
return input;
|
||||
}
|
||||
}.handler;
|
||||
}
|
||||
|
||||
fn IntConverter(comptime gen: ParameterGenerics) ConverterSignature(gen) {
|
||||
const IntType = gen.OutputType;
|
||||
|
||||
return struct {
|
||||
pub fn handler(_: gen.UserContext, input: [:0]const u8, failure: ErrorWriter) ConversionError!IntType {
|
||||
return std.fmt.parseInt(IntType, input, 0) catch {
|
||||
try failure.print("cannot interpret \"{s}\" as an integer", .{input});
|
||||
return ConversionError.ConversionFailed;
|
||||
};
|
||||
}
|
||||
}.handler;
|
||||
}
|
||||
|
||||
fn StructConverter(comptime gen: ParameterGenerics) ConverterSignature(gen) {
|
||||
const StructType = gen.OutputType;
|
||||
const type_info = @typeInfo(StructType).@"struct";
|
||||
const Intermediate = gen.IntermediateType();
|
||||
|
||||
return struct {
|
||||
pub fn handler(context: gen.UserContext, input: Intermediate, failure: ErrorWriter) ConversionError!StructType {
|
||||
if (input.items.len != type_info.fields.len) {
|
||||
try failure.print(
|
||||
"Wrong number of fields provided. Got {d}, needed {d}",
|
||||
.{ input.items.len, type_info.fields.len },
|
||||
);
|
||||
return ConversionError.ConversionFailed;
|
||||
}
|
||||
|
||||
var result: StructType = undefined;
|
||||
inline for (comptime type_info.fields, 0..) |field, idx| {
|
||||
const Converter = comptime DefaultConverter(
|
||||
ncmeta.copyStruct(ParameterGenerics, gen, .{
|
||||
.OutputType = field.type,
|
||||
.value_count = @as(parameters.ValueCount, .{ .fixed = 1 }),
|
||||
}),
|
||||
) orelse
|
||||
@compileError("cannot get converter for field" ++ field.name);
|
||||
|
||||
@field(result, field.name) = try Converter(context, input.items[idx], failure);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}.handler;
|
||||
}
|
||||
|
||||
fn ChoiceConverter(comptime gen: ParameterGenerics) ConverterSignature(gen) {
|
||||
const EnumType = gen.OutputType;
|
||||
|
||||
return struct {
|
||||
pub fn handler(_: gen.UserContext, input: [:0]const u8, failure: ErrorWriter) ConversionError!EnumType {
|
||||
return std.meta.stringToEnum(gen.ConvertedType(), input) orelse {
|
||||
try failure.print("\"{s}\" is not a valid choice", .{input});
|
||||
return ConversionError.ConversionFailed;
|
||||
};
|
||||
}
|
||||
}.handler;
|
||||
}
|
@@ -1,18 +0,0 @@
|
||||
pub const ConversionError = error{
|
||||
OutOfMemory,
|
||||
ConversionFailed,
|
||||
};
|
||||
|
||||
pub const ParseError = error{
|
||||
UnexpectedFailure,
|
||||
EmptyArgs,
|
||||
MissingValue,
|
||||
ExtraValue,
|
||||
FusedShortTagValueMissing,
|
||||
UnknownLongTagParameter,
|
||||
UnknownShortTagParameter,
|
||||
RequiredParameterMissing,
|
||||
OutOfMemory,
|
||||
};
|
||||
|
||||
pub const NoclipError = ParseError || ConversionError;
|
525
source/help.zig
525
source/help.zig
@@ -1,525 +0,0 @@
|
||||
const std = @import("std");
|
||||
|
||||
const NoclipError = @import("./errors.zig").NoclipError;
|
||||
const ncmeta = @import("./meta.zig");
|
||||
const FixedCount = @import("./parameters.zig").FixedCount;
|
||||
const parser = @import("./parser.zig");
|
||||
|
||||
const AlignablePair = struct {
|
||||
left: []const u8,
|
||||
right: []const u8,
|
||||
};
|
||||
|
||||
const OptionDescription = struct {
|
||||
pairs: []AlignablePair,
|
||||
just: usize,
|
||||
};
|
||||
|
||||
pub fn StructuredPrinter(comptime Writer: type) type {
|
||||
return struct {
|
||||
wrap_width: usize = 100,
|
||||
writer: Writer,
|
||||
|
||||
pub fn printPair(self: *@This(), pair: AlignablePair, leading_indent: u8, tabstop: usize) !void {
|
||||
try self.writer.writeByteNTimes(' ', leading_indent);
|
||||
const left = std.mem.trim(u8, pair.left, " \n");
|
||||
try self.writer.writeAll(left);
|
||||
|
||||
const offset: usize = leading_indent + left.len;
|
||||
// TODO: lol return a real error
|
||||
if (offset > tabstop) return NoclipError.UnexpectedFailure;
|
||||
|
||||
try self.writer.writeByteNTimes(' ', tabstop - offset);
|
||||
try self.printRewrap(std.mem.trim(u8, pair.right, " \n"), tabstop);
|
||||
try self.writer.writeByte('\n');
|
||||
}
|
||||
|
||||
pub fn printPairBrief(self: *@This(), pair: AlignablePair, leading_indent: u8, tabstop: usize) !void {
|
||||
const brief = ncmeta.partition(u8, pair.right, &[_][]const u8{"\n\n"})[0];
|
||||
const simulacrum: AlignablePair = .{
|
||||
.left = pair.left,
|
||||
.right = brief,
|
||||
};
|
||||
|
||||
try self.printPair(simulacrum, leading_indent, tabstop);
|
||||
}
|
||||
|
||||
pub fn printWrapped(self: *@This(), text: []const u8, leading_indent: usize) !void {
|
||||
try self.writer.writeByteNTimes(' ', leading_indent);
|
||||
try self.printRewrap(std.mem.trim(u8, text, "\n"), leading_indent);
|
||||
}
|
||||
|
||||
fn printRewrap(self: *@This(), text: []const u8, indent: usize) !void {
|
||||
// TODO: lol return a real error
|
||||
if (indent >= self.wrap_width) return NoclipError.UnexpectedFailure;
|
||||
|
||||
if (text.len == 0) return;
|
||||
|
||||
// this assumes output stream has already had the first line properly
|
||||
// indented.
|
||||
var splitter = std.mem.splitScalar(u8, text, '\n');
|
||||
|
||||
var location: usize = indent;
|
||||
while (splitter.next()) |line| {
|
||||
if (line.len == 0) {
|
||||
// we have a trailing line that needs to be cleaned up
|
||||
if (location > indent)
|
||||
_ = try self.clearLine(indent);
|
||||
|
||||
location = try self.clearLine(indent);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line[0] == '>') maybe: {
|
||||
if (line.len > 1) {
|
||||
if (line[1] == ' ') {
|
||||
try self.writer.writeAll(line[2..]);
|
||||
} else break :maybe;
|
||||
}
|
||||
location = try self.clearLine(indent);
|
||||
continue;
|
||||
}
|
||||
|
||||
var choppee = line;
|
||||
var need_forced_break = false;
|
||||
choppa: while (choppee.len > 0) {
|
||||
const breakoff = self.wrap_width - location;
|
||||
|
||||
if (breakoff >= choppee.len) {
|
||||
if (location > indent)
|
||||
try self.writer.writeByte(' ');
|
||||
|
||||
try self.writer.writeAll(choppee);
|
||||
location += choppee.len;
|
||||
break;
|
||||
}
|
||||
|
||||
var split = breakoff;
|
||||
while (choppee[split] != ' ') : (split -= 1) {
|
||||
if (split == 0) {
|
||||
// we have encountered a word that is too long to break,
|
||||
// so force breaking it
|
||||
if (need_forced_break) {
|
||||
split = breakoff;
|
||||
break;
|
||||
}
|
||||
if (location != indent)
|
||||
location = try self.clearLine(indent);
|
||||
|
||||
need_forced_break = true;
|
||||
continue :choppa;
|
||||
}
|
||||
}
|
||||
if (location > indent)
|
||||
try self.writer.writeByte(' ');
|
||||
|
||||
try self.writer.writeAll(choppee[0..split]);
|
||||
location = try self.clearLine(indent);
|
||||
choppee = choppee[split + 1 ..];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn clearLine(self: *@This(), indent: usize) !usize {
|
||||
try self.writer.writeByte('\n');
|
||||
try self.writer.writeByteNTimes(' ', indent);
|
||||
return indent;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn HelpBuilder(comptime command: anytype) type {
|
||||
const help_info = optInfo(command.generate());
|
||||
|
||||
return struct {
|
||||
writebuffer: std.ArrayList(u8),
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator) @This() {
|
||||
return @This(){
|
||||
.writebuffer = std.ArrayList(u8).init(allocator),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn buildMessage(
|
||||
self: *@This(),
|
||||
name: []const u8,
|
||||
subcommands: parser.CommandMap,
|
||||
) ![]const u8 {
|
||||
const writer = self.writebuffer.writer();
|
||||
try writer.print(
|
||||
"Usage: {s}{s}{s}{s}\n\n",
|
||||
.{
|
||||
name,
|
||||
self.optionBrief(),
|
||||
try self.argsBrief(),
|
||||
self.subcommandsBrief(subcommands),
|
||||
},
|
||||
);
|
||||
|
||||
var printer = StructuredPrinter(@TypeOf(writer)){ .writer = writer };
|
||||
try printer.printWrapped(command.description, 2);
|
||||
try writer.writeAll("\n\n");
|
||||
|
||||
const arguments = try self.describeArguments();
|
||||
const options = try self.describeOptions();
|
||||
const env_vars = try self.describeEnv();
|
||||
const subcs = try self.describeSubcommands(subcommands);
|
||||
const max_just = @max(arguments.just, @max(options.just, @max(env_vars.just, subcs.just)));
|
||||
|
||||
if (arguments.pairs.len > 0) {
|
||||
try writer.writeAll("Arguments:\n");
|
||||
for (arguments.pairs) |pair|
|
||||
try printer.printPair(pair, 2, max_just + 4);
|
||||
|
||||
try writer.writeAll("\n");
|
||||
}
|
||||
|
||||
if (options.pairs.len > 0) {
|
||||
try writer.writeAll("Options:\n");
|
||||
for (options.pairs) |pair|
|
||||
try printer.printPair(pair, 2, max_just + 4);
|
||||
|
||||
try writer.writeAll("\n");
|
||||
}
|
||||
|
||||
if (env_vars.pairs.len > 0) {
|
||||
try writer.writeAll("Environment variables:\n");
|
||||
for (env_vars.pairs) |pair|
|
||||
try printer.printPair(pair, 2, max_just + 4);
|
||||
|
||||
try writer.writeAll("\n");
|
||||
}
|
||||
|
||||
if (subcs.pairs.len > 0) {
|
||||
try writer.writeAll("Subcommands:\n");
|
||||
for (subcs.pairs) |pair|
|
||||
try printer.printPairBrief(pair, 2, max_just + 4);
|
||||
|
||||
try writer.writeAll("\n");
|
||||
}
|
||||
|
||||
return self.writebuffer.toOwnedSlice();
|
||||
}
|
||||
|
||||
fn optionBrief(_: @This()) []const u8 {
|
||||
return if (comptime help_info.options.len > 0)
|
||||
" [options...]"
|
||||
else
|
||||
"";
|
||||
}
|
||||
|
||||
fn argsBrief(self: @This()) ![]const u8 {
|
||||
var buf = std.ArrayList(u8).init(self.writebuffer.allocator);
|
||||
defer buf.deinit();
|
||||
|
||||
const writer = buf.writer();
|
||||
|
||||
for (comptime help_info.arguments) |arg| {
|
||||
try writer.writeAll(" ");
|
||||
if (!arg.required) try writer.writeAll("[");
|
||||
try writer.writeByte('<');
|
||||
try writer.writeAll(arg.name);
|
||||
if (arg.multi)
|
||||
try writer.print(" [{s} ...]", .{arg.name});
|
||||
try writer.writeByte('>');
|
||||
if (!arg.required) try writer.writeAll("]");
|
||||
}
|
||||
|
||||
return buf.toOwnedSlice();
|
||||
}
|
||||
|
||||
fn subcommandsBrief(_: @This(), subcommands: parser.CommandMap) []const u8 {
|
||||
return if (subcommands.count() > 0)
|
||||
" <subcommand ...>"
|
||||
else
|
||||
"";
|
||||
}
|
||||
|
||||
fn describeArguments(self: @This()) !OptionDescription {
|
||||
var pairs = std.ArrayList(AlignablePair).init(self.writebuffer.allocator);
|
||||
defer pairs.deinit();
|
||||
|
||||
var just: usize = 0;
|
||||
inline for (comptime help_info.arguments) |arg| {
|
||||
const pair: AlignablePair = .{
|
||||
.left = arg.name,
|
||||
.right = arg.description,
|
||||
};
|
||||
if (pair.left.len > just) just = pair.left.len;
|
||||
try pairs.append(pair);
|
||||
}
|
||||
|
||||
return .{
|
||||
.pairs = try pairs.toOwnedSlice(),
|
||||
.just = just,
|
||||
};
|
||||
}
|
||||
|
||||
fn describeOptions(self: @This()) !OptionDescription {
|
||||
var pairs = std.ArrayList(AlignablePair).init(self.writebuffer.allocator);
|
||||
defer pairs.deinit();
|
||||
|
||||
var just: usize = 0;
|
||||
inline for (help_info.options) |opt| {
|
||||
const pair = try self.describeOption(opt);
|
||||
if (pair.left.len > just) just = pair.left.len;
|
||||
try pairs.append(pair);
|
||||
}
|
||||
|
||||
return .{
|
||||
.pairs = try pairs.toOwnedSlice(),
|
||||
.just = just,
|
||||
};
|
||||
}
|
||||
|
||||
fn describeOption(self: @This(), comptime opt: OptHelp) !AlignablePair {
|
||||
var buffer = std.ArrayList(u8).init(self.writebuffer.allocator);
|
||||
defer buffer.deinit();
|
||||
const writer = buffer.writer();
|
||||
|
||||
if (comptime opt.short_truthy) |tag| {
|
||||
if (buffer.items.len > 0) try writer.writeAll(", ");
|
||||
try writer.writeAll(tag);
|
||||
}
|
||||
if (comptime opt.long_truthy) |tag| {
|
||||
if (buffer.items.len > 0) try writer.writeAll(", ");
|
||||
try writer.writeAll(tag);
|
||||
}
|
||||
|
||||
var falsy_seen = false;
|
||||
if (comptime opt.short_falsy) |tag| {
|
||||
if (buffer.items.len > 0)
|
||||
try writer.writeAll(" / ")
|
||||
else
|
||||
try writer.writeAll("/ ");
|
||||
try writer.writeAll(tag);
|
||||
falsy_seen = true;
|
||||
}
|
||||
if (comptime opt.long_falsy) |tag| {
|
||||
if (falsy_seen)
|
||||
try writer.writeAll(", ")
|
||||
else if (buffer.items.len > 0)
|
||||
try writer.writeAll(" / ");
|
||||
|
||||
try writer.writeAll(tag);
|
||||
}
|
||||
if (opt.value_count > 0) {
|
||||
try writer.print(" <{s}>", .{opt.type_name});
|
||||
}
|
||||
|
||||
const left = try buffer.toOwnedSlice();
|
||||
|
||||
if (comptime opt.required) {
|
||||
try writer.writeAll("[required]");
|
||||
}
|
||||
|
||||
if (comptime opt.description.len > 0) {
|
||||
if (buffer.items.len > 0) try writer.writeAll(" ");
|
||||
try writer.writeAll(opt.description);
|
||||
}
|
||||
|
||||
if (comptime opt.env_var) |env| {
|
||||
if (buffer.items.len > 0) try writer.writeAll(" ");
|
||||
try writer.print("(env: {s})", .{env});
|
||||
}
|
||||
|
||||
if (comptime opt.default) |def| {
|
||||
if (buffer.items.len > 0) try writer.writeAll(" ");
|
||||
try writer.print("(default: {s})", .{def});
|
||||
}
|
||||
|
||||
const right = try buffer.toOwnedSlice();
|
||||
|
||||
return .{ .left = left, .right = right };
|
||||
}
|
||||
|
||||
fn describeEnv(self: @This()) !OptionDescription {
|
||||
var pairs = std.ArrayList(AlignablePair).init(self.writebuffer.allocator);
|
||||
defer pairs.deinit();
|
||||
|
||||
var just: usize = 0;
|
||||
for (comptime help_info.env_vars) |env| {
|
||||
if (env.description.len == 0) continue;
|
||||
|
||||
const pair: AlignablePair = .{
|
||||
.left = env.env_var,
|
||||
.right = env.description,
|
||||
};
|
||||
if (pair.left.len > just) just = pair.left.len;
|
||||
try pairs.append(pair);
|
||||
}
|
||||
|
||||
return .{
|
||||
.pairs = try pairs.toOwnedSlice(),
|
||||
.just = just,
|
||||
};
|
||||
}
|
||||
|
||||
fn describeSubcommands(self: @This(), subcommands: parser.CommandMap) !OptionDescription {
|
||||
var pairs = std.ArrayList(AlignablePair).init(self.writebuffer.allocator);
|
||||
defer pairs.deinit();
|
||||
|
||||
var just: usize = 0;
|
||||
for (subcommands.keys()) |key| {
|
||||
const pair: AlignablePair = .{
|
||||
.left = key,
|
||||
.right = subcommands.get(key).?.describe(),
|
||||
};
|
||||
if (pair.left.len > just) just = pair.left.len;
|
||||
try pairs.append(pair);
|
||||
}
|
||||
|
||||
return .{
|
||||
.pairs = try pairs.toOwnedSlice(),
|
||||
.just = just,
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const CommandHelp = struct {
|
||||
options: []const OptHelp,
|
||||
arguments: []const ArgHelp,
|
||||
env_vars: []const EnvHelp,
|
||||
};
|
||||
|
||||
const OptHelp = struct {
|
||||
short_truthy: ?[]const u8 = null,
|
||||
long_truthy: ?[]const u8 = null,
|
||||
short_falsy: ?[]const u8 = null,
|
||||
long_falsy: ?[]const u8 = null,
|
||||
env_var: ?[]const u8 = null,
|
||||
description: []const u8 = "",
|
||||
type_name: []const u8 = "",
|
||||
extra: []const u8 = "",
|
||||
default: ?[]const u8 = null,
|
||||
// this is the pivot
|
||||
value_count: FixedCount = 0,
|
||||
required: bool = false,
|
||||
multi: bool = false,
|
||||
};
|
||||
|
||||
const EnvHelp = struct {
|
||||
env_var: []const u8 = "",
|
||||
description: []const u8 = "",
|
||||
default: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
const ArgHelp = struct {
|
||||
name: []const u8 = "",
|
||||
description: []const u8 = "",
|
||||
type_name: []const u8 = "",
|
||||
multi: bool = false,
|
||||
required: bool = true,
|
||||
};
|
||||
|
||||
pub fn optInfo(comptime command: anytype) CommandHelp {
|
||||
// TODO: this could be runtime and it would be slightly simpler.
|
||||
comptime {
|
||||
var options: []const OptHelp = &[_]OptHelp{};
|
||||
var env_vars: []const EnvHelp = &[_]EnvHelp{};
|
||||
var arguments: []const ArgHelp = &[_]ArgHelp{};
|
||||
|
||||
var last_name: []const u8 = "";
|
||||
var last_option: OptHelp = .{};
|
||||
|
||||
paramloop: for (command) |param| {
|
||||
const PType = @TypeOf(param);
|
||||
if (PType.param_type == .Ordinal) {
|
||||
arguments = arguments ++ &[_]ArgHelp{.{
|
||||
.name = param.name,
|
||||
.description = param.description,
|
||||
.type_name = param.nice_type_name,
|
||||
.multi = PType.multi,
|
||||
.required = param.required,
|
||||
}};
|
||||
|
||||
continue :paramloop;
|
||||
}
|
||||
|
||||
if (!std.mem.eql(u8, last_name, param.name)) {
|
||||
if (last_name.len > 0) {
|
||||
if (envOnly(last_option)) {
|
||||
env_vars = env_vars ++ &[_]EnvHelp{.{
|
||||
.env_var = last_option.env_var,
|
||||
.description = last_option.description,
|
||||
.default = last_option.default,
|
||||
}};
|
||||
} else {
|
||||
options = options ++ &[_]OptHelp{last_option};
|
||||
}
|
||||
}
|
||||
last_name = param.name;
|
||||
last_option = .{};
|
||||
}
|
||||
|
||||
if (PType.is_flag) {
|
||||
switch (param.flag_bias) {
|
||||
.truthy => {
|
||||
last_option.short_truthy = param.short_tag;
|
||||
last_option.long_truthy = param.long_tag;
|
||||
},
|
||||
.falsy => {
|
||||
last_option.short_falsy = param.short_tag;
|
||||
last_option.long_falsy = param.long_tag;
|
||||
},
|
||||
.unbiased => last_option.env_var = param.env_var,
|
||||
}
|
||||
} else {
|
||||
last_option.short_truthy = param.short_tag;
|
||||
last_option.long_truthy = param.long_tag;
|
||||
last_option.env_var = param.env_var;
|
||||
last_option.value_count = PType.value_count.fixed;
|
||||
}
|
||||
last_option.type_name = param.nice_type_name;
|
||||
last_option.description = param.description;
|
||||
last_option.required = param.required;
|
||||
last_option.multi = PType.multi;
|
||||
|
||||
if (param.default) |def| {
|
||||
var buf = ncmeta.ComptimeSliceBuffer{};
|
||||
const writer = buf.writer();
|
||||
// TODO: this is only acceptable for some types. It behaves poorly on
|
||||
// enum-based choice types because it prints the whole type name rather
|
||||
// than just the tag name. Roll our own eventually.
|
||||
blk: {
|
||||
switch (@typeInfo(@TypeOf(def))) {
|
||||
.pointer => |info| if (info.size == .Slice and info.child == u8) {
|
||||
writer.print("{s}", .{def}) catch @compileError("no");
|
||||
break :blk;
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
writer.print("{any}", .{def}) catch @compileError("whoah");
|
||||
}
|
||||
|
||||
last_option.default = buf.buffer;
|
||||
}
|
||||
}
|
||||
|
||||
if (last_name.len > 0) {
|
||||
if (envOnly(last_option)) {
|
||||
env_vars = env_vars ++ &[_]EnvHelp{.{
|
||||
.env_var = last_option.env_var.?,
|
||||
.description = last_option.description,
|
||||
.default = last_option.default,
|
||||
}};
|
||||
} else {
|
||||
options = options ++ &[_]OptHelp{last_option};
|
||||
}
|
||||
}
|
||||
|
||||
return .{
|
||||
.options = options,
|
||||
.arguments = arguments,
|
||||
.env_vars = env_vars,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
inline fn envOnly(option: OptHelp) bool {
|
||||
return option.short_truthy == null and
|
||||
option.long_truthy == null and
|
||||
option.short_falsy == null and
|
||||
option.long_falsy == null;
|
||||
}
|
335
source/meta.zig
335
source/meta.zig
@@ -1,335 +0,0 @@
|
||||
const std = @import("std");
|
||||
const StructField = std.builtin.Type.StructField;
|
||||
|
||||
/// Given a type and a struct literal of defaults to add, this function creates
|
||||
/// a simulacrum type with additional defaults set on its fields.
|
||||
///
|
||||
/// This function cannot remove default values from fields, but it can add some
|
||||
/// to fields that don't have them, and it can overwrite existing defaults
|
||||
pub fn UpdateDefaults(comptime input: type, comptime defaults: anytype) type {
|
||||
comptime {
|
||||
const inputInfo = @typeInfo(input);
|
||||
const fieldcount = switch (inputInfo) {
|
||||
.@"struct" => |spec| blk: {
|
||||
if (spec.decls.len > 0) {
|
||||
@compileError("UpdateDefaults only works on structs " ++
|
||||
"without decls due to limitations in @Type.");
|
||||
}
|
||||
break :blk spec.fields.len;
|
||||
},
|
||||
else => @compileError("can only add default value to struct type"),
|
||||
};
|
||||
|
||||
var fields: [fieldcount]StructField = undefined;
|
||||
for (inputInfo.@"struct".fields, 0..) |field, idx| {
|
||||
fields[idx] = .{
|
||||
.name = field.name,
|
||||
.field_type = field.field_type,
|
||||
// the cast ostensibly does type checking for us. It also makes
|
||||
// setting null defaults work, and it converts comptime_int to
|
||||
// the appropriate type, which is nice for ergonomics. Not sure
|
||||
// if it introduces weird edge cases. Probably it's fine?
|
||||
.default_value_ptr = if (@hasField(@TypeOf(defaults), field.name))
|
||||
@ptrCast(&@as(field.field_type, @field(defaults, field.name)))
|
||||
else
|
||||
field.default_value_ptr,
|
||||
.is_comptime = field.is_comptime,
|
||||
.alignment = field.alignment,
|
||||
};
|
||||
}
|
||||
|
||||
return @Type(.{ .@"struct" = .{
|
||||
.layout = inputInfo.@"struct".layout,
|
||||
.backing_integer = inputInfo.@"struct".backing_integer,
|
||||
.fields = &fields,
|
||||
.decls = inputInfo.@"struct".decls,
|
||||
.is_tuple = inputInfo.@"struct".is_tuple,
|
||||
} });
|
||||
}
|
||||
}
|
||||
|
||||
pub fn enumLength(comptime T: type) comptime_int {
|
||||
return @typeInfo(T).@"enum".fields.len;
|
||||
}
|
||||
|
||||
pub fn partition(comptime T: type, input: []const T, wedge: []const []const T) [3][]const T {
|
||||
var idx: usize = 0;
|
||||
while (idx < input.len) : (idx += 1) {
|
||||
for (wedge) |splitter| {
|
||||
if (input.len - idx < splitter.len) continue;
|
||||
if (std.mem.eql(T, input[idx .. idx + splitter.len], splitter)) {
|
||||
return [3][]const T{
|
||||
input[0..idx],
|
||||
input[idx..(idx + splitter.len)],
|
||||
input[(idx + splitter.len)..],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [3][]const T{
|
||||
input[0..],
|
||||
input[input.len..],
|
||||
input[input.len..],
|
||||
};
|
||||
}
|
||||
|
||||
pub fn ComptimeWriter(
|
||||
comptime Context: type,
|
||||
comptime writeFn: fn (comptime context: Context, comptime bytes: []const u8) error{}!usize,
|
||||
) type {
|
||||
return struct {
|
||||
context: Context,
|
||||
|
||||
const Self = @This();
|
||||
pub const Error = error{};
|
||||
|
||||
pub fn write(comptime self: Self, comptime bytes: []const u8) Error!usize {
|
||||
return writeFn(self.context, bytes);
|
||||
}
|
||||
|
||||
pub fn writeAll(comptime self: Self, comptime bytes: []const u8) Error!void {
|
||||
var index: usize = 0;
|
||||
while (index != bytes.len) {
|
||||
index += try self.write(bytes[index..]);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn print(comptime self: Self, comptime format: []const u8, args: anytype) Error!void {
|
||||
return std.fmt.format(self, format, args) catch @compileError("woah");
|
||||
}
|
||||
|
||||
pub fn writeByte(comptime self: Self, byte: u8) Error!void {
|
||||
const array = [1]u8{byte};
|
||||
return self.writeAll(&array);
|
||||
}
|
||||
|
||||
pub fn writeByteNTimes(comptime self: Self, byte: u8, n: usize) Error!void {
|
||||
var bytes: [256]u8 = undefined;
|
||||
std.mem.set(u8, bytes[0..], byte);
|
||||
|
||||
var remaining: usize = n;
|
||||
while (remaining > 0) {
|
||||
const to_write = std.math.min(remaining, bytes.len);
|
||||
try self.writeAll(bytes[0..to_write]);
|
||||
remaining -= to_write;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub const ComptimeSliceBuffer = struct {
|
||||
buffer: []const u8 = &[_]u8{},
|
||||
|
||||
const Writer = ComptimeWriter(*@This(), appendslice);
|
||||
|
||||
pub fn writer(comptime self: *@This()) Writer {
|
||||
return .{ .context = self };
|
||||
}
|
||||
|
||||
fn appendslice(comptime self: *@This(), comptime bytes: []const u8) error{}!usize {
|
||||
self.buffer = self.buffer ++ bytes;
|
||||
return bytes.len;
|
||||
}
|
||||
};
|
||||
|
||||
pub fn SliceIterator(comptime T: type) type {
|
||||
// could be expanded to use std.meta.Elem, perhaps
|
||||
const ResultType = std.meta.Child(T);
|
||||
|
||||
return struct {
|
||||
index: usize,
|
||||
data: T,
|
||||
|
||||
pub const InitError = error{};
|
||||
|
||||
pub fn wrap(value: T) @This() {
|
||||
return @This(){ .index = 0, .data = value };
|
||||
}
|
||||
|
||||
pub fn next(self: *@This()) ?ResultType {
|
||||
if (self.index == self.data.len) return null;
|
||||
|
||||
defer self.index += 1;
|
||||
return self.data[self.index];
|
||||
}
|
||||
|
||||
pub fn peek(self: *@This()) ?ResultType {
|
||||
if (self.index == self.data.len) return null;
|
||||
|
||||
return self.data[self.index];
|
||||
}
|
||||
|
||||
pub fn rewind(self: *@This()) void {
|
||||
if (self.index == 0) return;
|
||||
|
||||
self.index -= 1;
|
||||
}
|
||||
|
||||
pub fn skip(self: *@This()) void {
|
||||
if (self.index == self.data.len) return;
|
||||
|
||||
self.index += 1;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn MutatingZSplitter(comptime T: type) type {
|
||||
return struct {
|
||||
buffer: [:0]T,
|
||||
delimiter: T,
|
||||
index: ?usize = 0,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
/// Returns a slice of the next field, or null if splitting is complete.
|
||||
pub fn next(self: *Self) ?[:0]T {
|
||||
const start = self.index orelse return null;
|
||||
|
||||
const end = if (std.mem.indexOfScalarPos(T, self.buffer, start, self.delimiter)) |delim_idx| blk: {
|
||||
self.buffer[delim_idx] = 0;
|
||||
self.index = delim_idx + 1;
|
||||
break :blk delim_idx;
|
||||
} else blk: {
|
||||
self.index = null;
|
||||
break :blk self.buffer.len;
|
||||
};
|
||||
|
||||
return self.buffer[start..end :0];
|
||||
}
|
||||
|
||||
/// Returns a slice of the remaining bytes. Does not affect iterator state.
|
||||
pub fn rest(self: Self) [:0]T {
|
||||
const end = self.buffer.len;
|
||||
const start = self.index orelse end;
|
||||
return self.buffer[start..end :0];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn copyStruct(comptime T: type, source: T, field_overrides: anytype) T {
|
||||
var result: T = undefined;
|
||||
|
||||
comptime for (@typeInfo(@TypeOf(field_overrides)).@"struct".fields) |field| {
|
||||
if (!@hasField(T, field.name)) @compileError("override contains bad field" ++ field);
|
||||
};
|
||||
|
||||
inline for (comptime @typeInfo(T).@"struct".fields) |field| {
|
||||
if (comptime @hasField(@TypeOf(field_overrides), field.name))
|
||||
@field(result, field.name) = @field(field_overrides, field.name)
|
||||
else
|
||||
@field(result, field.name) = @field(source, field.name);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Stores type-erased pointers to items in comptime extensible data structures,
|
||||
/// which allows e.g. assembling a tuple through multiple calls rather than all
|
||||
/// at once.
|
||||
pub const TupleBuilder = struct {
|
||||
pointers: []const *const anyopaque = &[0]*const anyopaque{},
|
||||
types: []const type = &[0]type{},
|
||||
|
||||
pub fn add(comptime self: *@This(), comptime item: anytype) void {
|
||||
self.pointers = self.pointers ++ &[_]*const anyopaque{@as(*const anyopaque, &item)};
|
||||
self.types = self.types ++ &[_]type{@TypeOf(item)};
|
||||
}
|
||||
|
||||
pub fn retrieve(comptime self: @This(), comptime index: comptime_int) self.types[index] {
|
||||
return @as(*const self.types[index], @ptrCast(@alignCast(self.pointers[index]))).*;
|
||||
}
|
||||
|
||||
pub fn realTuple(comptime self: @This()) self.TupleType() {
|
||||
comptime {
|
||||
var result: self.TupleType() = undefined;
|
||||
var idx = 0;
|
||||
while (idx < self.types.len) : (idx += 1) {
|
||||
result[idx] = self.retrieve(idx);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn TupleType(comptime self: @This()) type {
|
||||
comptime {
|
||||
var fields: [self.types.len]StructField = undefined;
|
||||
for (self.types, 0..) |Type, idx| {
|
||||
fields[idx] = .{
|
||||
.name = std.fmt.comptimePrint("{d}", .{idx}),
|
||||
.type = Type,
|
||||
.default_value_ptr = null,
|
||||
// TODO: is this the right thing to do?
|
||||
.is_comptime = false,
|
||||
.alignment = if (@sizeOf(Type) > 0) @alignOf(Type) else 0,
|
||||
};
|
||||
}
|
||||
|
||||
return @Type(.{ .@"struct" = .{
|
||||
.layout = .auto,
|
||||
.fields = &fields,
|
||||
.decls = &.{},
|
||||
.is_tuple = true,
|
||||
} });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
test "add basic default" {
|
||||
const Base = struct { a: u8 };
|
||||
const Defaulted = UpdateDefaults(Base, .{ .a = 4 });
|
||||
|
||||
const value = Defaulted{};
|
||||
try std.testing.expectEqual(@as(u8, 4), value.a);
|
||||
}
|
||||
|
||||
test "overwrite basic default" {
|
||||
const Base = struct { a: u8 = 0 };
|
||||
const Defaulted = UpdateDefaults(Base, .{ .a = 1 });
|
||||
|
||||
const value = Defaulted{};
|
||||
try std.testing.expectEqual(@as(u8, 1), value.a);
|
||||
}
|
||||
|
||||
test "add string default" {
|
||||
const Base = struct { a: []const u8 };
|
||||
const Defaulted = UpdateDefaults(Base, .{ .a = "hello" });
|
||||
|
||||
const value = Defaulted{};
|
||||
try std.testing.expectEqual(@as([]const u8, "hello"), value.a);
|
||||
}
|
||||
|
||||
test "add null default" {
|
||||
const Base = struct { a: ?u8 };
|
||||
const Defaulted = UpdateDefaults(Base, .{ .a = null });
|
||||
|
||||
const value = Defaulted{};
|
||||
try std.testing.expectEqual(@as(?u8, null), value.a);
|
||||
}
|
||||
|
||||
test "add enum default" {
|
||||
const Options = enum { good, bad };
|
||||
const Base = struct { a: Options };
|
||||
const Defaulted = UpdateDefaults(Base, .{ .a = .good });
|
||||
|
||||
const value = Defaulted{};
|
||||
try std.testing.expectEqual(Options.good, value.a);
|
||||
}
|
||||
|
||||
test "preserve existing default" {
|
||||
const Base = struct { a: ?u8 = 2, b: u8 };
|
||||
const Defaulted = UpdateDefaults(Base, .{ .b = 3 });
|
||||
|
||||
const value = Defaulted{};
|
||||
try std.testing.expectEqual(@as(?u8, 2), value.a);
|
||||
try std.testing.expectEqual(@as(?u8, 3), value.b);
|
||||
}
|
||||
|
||||
test "add multiple defaults" {
|
||||
const Base = struct { a: u8, b: i8, c: ?u8 };
|
||||
const Defaulted = UpdateDefaults(Base, .{ .a = 3, .c = 2 });
|
||||
|
||||
const value = Defaulted{ .b = -1 };
|
||||
try std.testing.expectEqual(@as(u8, 3), value.a);
|
||||
try std.testing.expectEqual(@as(i8, -1), value.b);
|
||||
try std.testing.expectEqual(@as(?u8, 2), value.c);
|
||||
}
|
@@ -1,11 +1,241 @@
|
||||
pub const command = @import("./command.zig");
|
||||
pub const converters = @import("./converters.zig");
|
||||
pub const errors = @import("./errors.zig");
|
||||
pub const help = @import("./help.zig");
|
||||
pub const ncmeta = @import("./meta.zig");
|
||||
pub const parameters = @import("./parameters.zig");
|
||||
pub const parser = @import("./parser.zig");
|
||||
pub const CommandOptions = struct {
|
||||
context_type: type = void,
|
||||
|
||||
pub const CommandBuilder = command.CommandBuilder;
|
||||
pub const commandGroup = command.commandGroup;
|
||||
pub const ParserInterface = parser.ParserInterface;
|
||||
default_help_flags: bool = true,
|
||||
create_help_command: enum { always, never, if_subcommands } = .if_subcommands,
|
||||
create_completion_helper: bool = true,
|
||||
allow_colored_output: bool = true,
|
||||
output_strategy: enum { type, iterator } = .type,
|
||||
parse_error_behavior: enum { exit, propagate } = .propagate,
|
||||
// pop the callback stack after parsing all arguments for the current subcommand
|
||||
pipeline_subcommands: bool = false,
|
||||
};
|
||||
|
||||
const __Canary = opaque {};
|
||||
|
||||
pub const ErrorReport = struct {};
|
||||
|
||||
pub fn Status(comptime T: type) type {
|
||||
return union(enum) {
|
||||
success: T,
|
||||
failure: ErrorReport,
|
||||
};
|
||||
}
|
||||
|
||||
pub const String = struct {
|
||||
bytes: []const u8,
|
||||
};
|
||||
|
||||
pub const Codepoint = u21;
|
||||
|
||||
pub const ParameterType = enum {
|
||||
flag,
|
||||
// counter
|
||||
// fixed_value
|
||||
// aggregate_flag
|
||||
option,
|
||||
// aggregate_option
|
||||
argument,
|
||||
// aggregate_argument
|
||||
// group,
|
||||
};
|
||||
|
||||
pub const Scope = enum { local, global };
|
||||
|
||||
pub const MultiMode = enum {
|
||||
first,
|
||||
last,
|
||||
accumulate,
|
||||
count,
|
||||
};
|
||||
|
||||
pub fn Accumulate(comptime T: type) type {
|
||||
return struct {
|
||||
const __noclip_canary__ = __Canary;
|
||||
|
||||
pub const Result: type = T;
|
||||
pub const multi_mode: MultiMode = .accumulate;
|
||||
};
|
||||
}
|
||||
|
||||
pub fn Count(comptime T: type) type {
|
||||
if (!@typeInfo(T) == .int) unreachable;
|
||||
return struct {
|
||||
const __noclip_canary__ = __Canary;
|
||||
|
||||
pub const Result: type = T;
|
||||
pub const multi_mode: MultiMode = .count;
|
||||
};
|
||||
}
|
||||
|
||||
pub const FlagSet = struct {
|
||||
pub const Result = bool;
|
||||
pub const param_type: ParameterType = .flag;
|
||||
|
||||
description: []const u8 = "",
|
||||
truthy: Pair = .{},
|
||||
falsy: Pair = .{},
|
||||
env: ?[]const u8 = null,
|
||||
/// If true, at least one of the variants of the flag must be provided by
|
||||
/// the user on the command line, otherwise a parse error will be produced.
|
||||
required: bool = false,
|
||||
/// A default value that will be forwarded if the option is not provided on
|
||||
/// the command line by the user. If a default is provided, then the
|
||||
/// corresponding parsed value will not be optional. Note that flags are
|
||||
/// tri-state values that may be `null`, `true`, or `false`. `null` will
|
||||
/// never be forwarded if this is set to `true` or `false`, as `null` only
|
||||
/// indicates that the flag was not specified on the command line.
|
||||
default: ?Result = null,
|
||||
// multi: Multi = .last,
|
||||
scope: Scope = .local,
|
||||
eager: bool = false,
|
||||
hidden: bool = false,
|
||||
|
||||
pub const Pair = struct {
|
||||
/// a single unicode codepoint that identifies this flag on the command
|
||||
/// line, e.g. 'v'.
|
||||
short: ?Codepoint = null,
|
||||
/// a string, beginning with the long flag sequence `--` that identifies
|
||||
/// this flag on the command line, e.g. "--version". Multiple words
|
||||
/// should be skewercase, i.e. "--multiple-words".
|
||||
long: ?[]const u8 = null,
|
||||
};
|
||||
};
|
||||
|
||||
pub const Counter = struct {
|
||||
pub const Result = u64;
|
||||
pub const param_type: ParameterType = .flag;
|
||||
|
||||
description: []const u8 = "",
|
||||
short: ?Codepoint = null,
|
||||
long: ?[]const u8 = null,
|
||||
required: bool = false,
|
||||
scope: Scope = .local,
|
||||
hidden: bool = false,
|
||||
};
|
||||
|
||||
pub fn Group(comptime R: type) type {
|
||||
return struct {
|
||||
pub const Result = R;
|
||||
// pub const param_type: ParameterType = .group;
|
||||
|
||||
// description: []const u8 = "",
|
||||
// env: ?[]const u8 = null,
|
||||
/// at least one of the parameters in the group must be provided
|
||||
required: bool = false,
|
||||
parameters: type,
|
||||
|
||||
pub fn validate(self: @This()) Status(void) {
|
||||
comptime {
|
||||
for (@typeInfo(@TypeOf(self.parameters)).Struct.decls) |td| {
|
||||
const decl = @field(@TypeOf(self.parameters), td.name);
|
||||
std.debug.assert(decl.Result == Result);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// figure this out: this is a zero-parameter flag that produces a non-boolean
|
||||
// value, e.g. an int. for like -9 on gz. A flag is just a FixedValue with
|
||||
pub fn FixedValue(comptime R: type) type {
|
||||
return struct {
|
||||
pub const Result = R;
|
||||
pub const param_type: ParameterType = .flag;
|
||||
|
||||
description: []const u8 = "",
|
||||
short: ?Codepoint = null,
|
||||
long: ?[]const u8 = null,
|
||||
env: ?[]const u8 = null,
|
||||
/// Require that the user always provide a value for this option on the
|
||||
/// command line.
|
||||
required: bool = false,
|
||||
/// A default value that will be forwarded if the option is not provided
|
||||
/// on the command line by the user. If a default is provided, then the
|
||||
/// corresponding parsed value will not be optional.
|
||||
value: Result,
|
||||
scope: Scope = .local,
|
||||
eager: bool = false,
|
||||
hidden: bool = false,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn Option(comptime R: type) type {
|
||||
return struct {
|
||||
pub const Result = R;
|
||||
pub const param_type: ParameterType = .option;
|
||||
|
||||
description: []const u8 = "",
|
||||
short: ?Codepoint = null,
|
||||
long: ?[]const u8 = null,
|
||||
env: ?[]const u8 = null,
|
||||
/// Require that the user always provide a value for this option on the
|
||||
/// command line.
|
||||
required: bool = false,
|
||||
/// A default value that will be forwarded if the option is not provided
|
||||
/// on the command line by the user. If a default is provided, then the
|
||||
/// corresponding parsed value will not be optional.
|
||||
default: ?Result = null,
|
||||
/// note: .count is only valid for flags
|
||||
/// note: .accumulate requires R to be a slice
|
||||
// multi: Multi = .last,
|
||||
scope: Scope = .local,
|
||||
eager: bool = false,
|
||||
hidden: bool = false,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn Argument(comptime R: type) type {
|
||||
return struct {
|
||||
pub const Result = R;
|
||||
|
||||
description: []const u8 = "",
|
||||
// note: equivalent to .accumulate. Requires R to be a slice
|
||||
multi: bool = false,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn execute(comptime spec: type, args: ExecArgs(spec)) ReturnType(spec) {
|
||||
var parser = Parser(spec).init(args.alloc, args.context);
|
||||
defer parser.deinit();
|
||||
switch (parser.parse(args.args, args.env)) {
|
||||
.success => |callstack| {
|
||||
for (callstack) |runner| {
|
||||
try runner();
|
||||
}
|
||||
},
|
||||
.fail => |report| {
|
||||
switch (spec.info.options.parse_error_behavior) {
|
||||
.exit => {},
|
||||
.propagate => {
|
||||
if (@hasField(spec, "err"))
|
||||
spec.err(report);
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ExecArgs(comptime spec: type) type {
|
||||
return struct {
|
||||
alloc: std.mem.Allocator,
|
||||
args: []const [:0]const u8,
|
||||
env: std.process.EnvMap,
|
||||
context: spec.info.context_type,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn ReturnType(comptime spec: type) type {
|
||||
const info = @typeInfo(@TypeOf(spec.run)).Fn;
|
||||
return switch (@typeInfo(info.return_type.?)) {
|
||||
.ErrorUnion => |eu| blk: {
|
||||
if (eu.payload != void) unreachable;
|
||||
break :blk info.return_type.?;
|
||||
},
|
||||
.Void => void,
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
pub const Parser = @import("./parser.zig");
|
||||
const std = @import("std");
|
||||
|
@@ -1,348 +0,0 @@
|
||||
const std = @import("std");
|
||||
|
||||
const converters = @import("./converters.zig");
|
||||
const ConverterSignature = converters.ConverterSignature;
|
||||
|
||||
const ParameterType = enum { Nominal, Ordinal };
|
||||
|
||||
pub const FixedCount = u32;
|
||||
|
||||
pub const ValueCount = union(enum) {
|
||||
flag: void,
|
||||
count: void,
|
||||
fixed: FixedCount,
|
||||
};
|
||||
|
||||
pub const FlagBias = enum {
|
||||
falsy,
|
||||
truthy,
|
||||
unbiased,
|
||||
|
||||
pub fn string(comptime self: @This()) [:0]const u8 {
|
||||
return switch (comptime self) {
|
||||
.truthy => "true",
|
||||
.falsy => "false",
|
||||
else => @compileError("flag tag with unbiased bias?"),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const ParameterGenerics = struct {
|
||||
UserContext: type = void,
|
||||
/// If void, do not expose this parameter in the aggregate converted parameter
|
||||
/// object. The converter for this parameter shall not return a value. This may be
|
||||
/// useful for implementing complex conversion that produces output through its
|
||||
/// side effects or by modifying the user context.
|
||||
OutputType: type = void,
|
||||
|
||||
param_type: ParameterType,
|
||||
value_count: ValueCount,
|
||||
/// allow this named parameter to be passed multiple times.
|
||||
/// values will be appended when it is encountered. If false, only the
|
||||
/// final encountered instance will be used.
|
||||
// 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.
|
||||
multi: bool,
|
||||
|
||||
pub fn fixedValueCount(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 hasContext(comptime self: @This()) bool {
|
||||
return comptime self.UserContext != void;
|
||||
}
|
||||
|
||||
pub fn hasOutput(comptime self: @This()) bool {
|
||||
return self.OutputType != void;
|
||||
}
|
||||
|
||||
pub fn isFlag(comptime self: @This()) bool {
|
||||
return comptime switch (self.value_count) {
|
||||
.flag, .count => true,
|
||||
.fixed => false,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn ConvertedType(comptime self: @This()) type {
|
||||
// is this the correct way to collapse this?
|
||||
return comptime if (self.multi and self.value_count != .count and self.OutputType != void)
|
||||
std.ArrayList(self.ReturnValue())
|
||||
else
|
||||
self.ReturnValue();
|
||||
}
|
||||
|
||||
pub fn IntermediateType(comptime self: @This()) type {
|
||||
return comptime if (self.multi and self.value_count != .count)
|
||||
std.ArrayList(self.IntermediateValue())
|
||||
else
|
||||
self.IntermediateValue();
|
||||
}
|
||||
|
||||
pub fn ReturnValue(comptime self: @This()) type {
|
||||
return comptime switch (self.value_count) {
|
||||
.flag => bool,
|
||||
.count => usize,
|
||||
.fixed => |count| switch (count) {
|
||||
0 => @compileError("bad fixed-zero parameter"),
|
||||
1 => self.OutputType,
|
||||
// it's actually impossible to use a list in the general case
|
||||
// because the result may have varying types. A tuple would
|
||||
// work, but cannot be iterated over without inline for. It may
|
||||
// be worth adding a ".structured" value count for a type that
|
||||
// consumes many inputs but produces a single output. It would
|
||||
// be nice to parse a tag into a struct directly. For that use
|
||||
// case, the output type must be decoupled from the input type.
|
||||
else => self.OutputType,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn IntermediateValue(comptime self: @This()) type {
|
||||
return comptime switch (self.value_count) {
|
||||
.flag => [:0]const u8,
|
||||
.count => usize,
|
||||
.fixed => |count| switch (count) {
|
||||
0 => @compileError("bad fixed-zero parameter"),
|
||||
1 => [:0]const u8,
|
||||
else => std.ArrayList([:0]const u8),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn nonscalar(comptime self: @This()) bool {
|
||||
return comptime switch (self.value_count) {
|
||||
.flag, .count => false,
|
||||
.fixed => |count| switch (count) {
|
||||
0 => @compileError("bad fixed-zero parameter"),
|
||||
1 => false,
|
||||
else => true,
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Consider a "namespace" parameter e.g. -Dfoo=val style. The namespace would be "D" and
|
||||
// it takes the place of the second "-", but otherwise this is a long-style parameter.
|
||||
// Could be parsed as forced-fused. Would work for flags as well, e.g. -fno-flag
|
||||
pub fn OptionConfig(comptime generics: ParameterGenerics) type {
|
||||
return struct {
|
||||
name: []const u8,
|
||||
|
||||
short_tag: ?[]const u8 = null,
|
||||
long_tag: ?[]const u8 = null,
|
||||
env_var: ?[]const u8 = null,
|
||||
description: []const u8 = "", // description for output in help text
|
||||
|
||||
default: ?generics.OutputType = null,
|
||||
converter: ?ConverterSignature(generics) = null,
|
||||
|
||||
eager: bool = false,
|
||||
required: bool = generics.param_type == .Ordinal,
|
||||
global: bool = false,
|
||||
|
||||
secret: bool = false,
|
||||
nice_type_name: ?[]const u8 = null,
|
||||
flag_bias: FlagBias = .unbiased,
|
||||
};
|
||||
}
|
||||
|
||||
pub const ShortLongPair = struct {
|
||||
short_tag: ?[]const u8 = null,
|
||||
long_tag: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
pub fn FlagConfig(comptime generics: ParameterGenerics) 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(generics) = null,
|
||||
|
||||
eager: bool = false,
|
||||
required: bool = false,
|
||||
global: bool = false,
|
||||
|
||||
secret: bool = false,
|
||||
};
|
||||
}
|
||||
|
||||
fn OptionType(comptime generics: ParameterGenerics) type {
|
||||
return struct {
|
||||
pub const G: ParameterGenerics = generics;
|
||||
pub const param_type: ParameterType = generics.param_type;
|
||||
pub const is_flag: bool = generics.isFlag();
|
||||
pub const value_count: ValueCount = generics.value_count;
|
||||
pub const multi: bool = generics.multi;
|
||||
pub const has_output: bool = generics.hasOutput();
|
||||
|
||||
name: []const u8,
|
||||
short_tag: ?[]const u8,
|
||||
long_tag: ?[]const u8,
|
||||
env_var: ?[]const u8,
|
||||
/// description for output in help text
|
||||
description: []const u8,
|
||||
|
||||
default: ?generics.OutputType,
|
||||
converter: ConverterSignature(generics),
|
||||
|
||||
/// the option converter will be run eagerly, before full command line
|
||||
/// validation.
|
||||
eager: bool,
|
||||
/// the option cannot be omitted from the command line.
|
||||
required: bool,
|
||||
/// this option is parsed in a pre-parsing pass that consumes it. It
|
||||
/// may be present anywhere on the command line. A different way to
|
||||
/// solve this problem is by using an environment variable. It must be
|
||||
/// a tagged option.
|
||||
global: bool,
|
||||
|
||||
/// if false, do not expose the resulting value in the output type.
|
||||
/// the converter must have side effects for this option to do anything.
|
||||
/// do not print help for this parameter
|
||||
secret: bool,
|
||||
|
||||
/// friendly type name ("string" is better than "[]const u8")
|
||||
nice_type_name: []const u8,
|
||||
/// internal field for handling flag value biasing. Do not overwrite unless you
|
||||
/// want weird things to happen.
|
||||
flag_bias: FlagBias,
|
||||
|
||||
pub fn describe(self: @This(), allocator: std.mem.Allocator) std.mem.Allocator.Error![]const u8 {
|
||||
var buf = std.ArrayList(u8).init(allocator);
|
||||
|
||||
try buf.append('"');
|
||||
try buf.appendSlice(self.name);
|
||||
try buf.append('"');
|
||||
if (self.short_tag != null or self.long_tag != null) {
|
||||
try buf.appendSlice(" (");
|
||||
if (self.short_tag) |short|
|
||||
try buf.appendSlice(short);
|
||||
if (self.short_tag != null and self.long_tag != null)
|
||||
try buf.appendSlice(", ");
|
||||
if (self.long_tag) |long|
|
||||
try buf.appendSlice(long);
|
||||
try buf.append(')');
|
||||
}
|
||||
|
||||
return try buf.toOwnedSlice();
|
||||
}
|
||||
|
||||
pub fn IntermediateValue(comptime _: @This()) type {
|
||||
return generics.IntermediateValue();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn checkShort(comptime short_tag: ?[]const u8) void {
|
||||
const short = comptime short_tag orelse return;
|
||||
if (short.len != 2 or short[0] != '-') @compileError("bad short tag: " ++ short);
|
||||
}
|
||||
|
||||
fn checkLong(comptime long_tag: ?[]const u8) void {
|
||||
const long = comptime long_tag orelse return;
|
||||
if (long.len < 3 or long[0] != '-' or long[1] != '-') @compileError("bad long tag: " ++ long);
|
||||
}
|
||||
|
||||
pub fn makeOption(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",
|
||||
);
|
||||
}
|
||||
|
||||
checkShort(opts.short_tag);
|
||||
checkLong(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.DefaultConverter(generics) orelse @compileError(
|
||||
"no converter provided for " ++
|
||||
opts.name ++
|
||||
"and no default exists",
|
||||
));
|
||||
|
||||
return OptionType(generics){
|
||||
.name = opts.name,
|
||||
//
|
||||
.short_tag = opts.short_tag,
|
||||
.long_tag = opts.long_tag,
|
||||
.env_var = opts.env_var,
|
||||
//
|
||||
.description = opts.description,
|
||||
.default = opts.default,
|
||||
.converter = converter,
|
||||
//
|
||||
.eager = opts.eager,
|
||||
.required = opts.required,
|
||||
.global = opts.global,
|
||||
//
|
||||
.secret = opts.secret,
|
||||
.nice_type_name = opts.nice_type_name orelse @typeName(generics.OutputType),
|
||||
.flag_bias = opts.flag_bias,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn makeArgument(
|
||||
comptime generics: ParameterGenerics,
|
||||
comptime opts: OptionConfig(generics),
|
||||
) OptionType(generics) {
|
||||
comptime {
|
||||
if (opts.short_tag != null or opts.long_tag != null or opts.env_var != null) {
|
||||
@compileError("argument " ++ opts.name ++ " must not have a long or short tag or an env var");
|
||||
}
|
||||
|
||||
if (opts.global) {
|
||||
@compileError("argument " ++ opts.name ++ " cannot be global");
|
||||
}
|
||||
|
||||
const converter = opts.converter orelse
|
||||
(converters.DefaultConverter(generics) orelse @compileError(
|
||||
"no converter provided for " ++
|
||||
opts.name ++
|
||||
"and no default exists",
|
||||
));
|
||||
|
||||
return OptionType(generics){
|
||||
.name = opts.name,
|
||||
//
|
||||
.short_tag = opts.short_tag,
|
||||
.long_tag = opts.long_tag,
|
||||
.env_var = opts.env_var,
|
||||
//
|
||||
.description = opts.description,
|
||||
.default = opts.default,
|
||||
.converter = converter,
|
||||
//
|
||||
.eager = opts.eager,
|
||||
.required = opts.required,
|
||||
.global = opts.global,
|
||||
//
|
||||
.secret = opts.secret,
|
||||
.nice_type_name = opts.nice_type_name orelse @typeName(generics.OutputType),
|
||||
.flag_bias = .unbiased,
|
||||
};
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user