There are still quirks here. It's worth deciding if help descriptions should be automatically hard-wrapped. Multi-line descriptions require appropriate indentation after the first line. Long descriptions will automatically be wrapped by the terminal. The refactoring itch continues to grow.
718 lines
28 KiB
Zig
718 lines
28 KiB
Zig
// Copyright (c) 2022 torque <torque@users.noreply.github.com>
|
|
|
|
// Permission to use, copy, modify, and/or distribute this software for any
|
|
// purpose with or without fee is hereby granted, provided that the above
|
|
// copyright notice and this permission notice appear in all copies.
|
|
|
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
|
|
// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
// PERFORMANCE OF THIS SOFTWARE.
|
|
|
|
const std = @import("std");
|
|
const StructField = std.builtin.Type.StructField;
|
|
pub const meta = @import("./meta.zig");
|
|
pub const params = @import("./params.zig");
|
|
pub const Command = @import("./bakery.zig").Command;
|
|
|
|
pub const OptionError = error{
|
|
BadShortOption,
|
|
BadLongOption,
|
|
UnknownOption,
|
|
MissingOption,
|
|
MissingArgument,
|
|
ExtraArguments,
|
|
};
|
|
|
|
/// spec is a tuple of Option, Flag, and Argument
|
|
pub fn CommandParser(
|
|
comptime commandData: params.CommandData,
|
|
comptime spec: anytype,
|
|
comptime UserContext: type,
|
|
comptime callback: *const fn (UserContext, CommandResult(spec, UserContext)) anyerror!void,
|
|
) type {
|
|
const param_count: struct {
|
|
opts: comptime_int,
|
|
args: comptime_int,
|
|
subs: comptime_int,
|
|
} = comptime comp: {
|
|
var optc = 0;
|
|
var argc = 0;
|
|
var subc = 0;
|
|
for (spec) |param| {
|
|
switch (@TypeOf(param).brand) {
|
|
.Argument => argc += 1,
|
|
.Option, .Flag => optc += 1,
|
|
.Command => subc += 1,
|
|
}
|
|
}
|
|
break :comp .{ .opts = optc, .args = argc, .subs = subc };
|
|
};
|
|
|
|
const ResultType = CommandResult(spec, UserContext);
|
|
const RequiredType = RequiredTracker(spec);
|
|
|
|
const ParseState = enum { Mixed, ForcedArgs };
|
|
|
|
return struct {
|
|
pub const brand: params.Brand = .Command;
|
|
pub const ContextType = UserContext;
|
|
// this should be copied at compile time
|
|
var data: params.CommandData = commandData;
|
|
|
|
pub fn execute(self: @This(), alloc: std.mem.Allocator, comptime argit_type: type, argit: *argit_type, context: UserContext) !void {
|
|
return try self.internal_execute(alloc, argit_type, argit, context, null);
|
|
}
|
|
|
|
fn internal_execute(self: @This(), alloc: std.mem.Allocator, comptime argit_type: type, argit: *argit_type, context: UserContext, prog: ?[]const u8) !void {
|
|
try self.attachSubcommands(alloc);
|
|
|
|
var result: ResultType = createCommandresult();
|
|
var required: RequiredType = .{};
|
|
var parseState: ParseState = .Mixed;
|
|
|
|
try extractEnvVars(alloc, &result, &required, context);
|
|
|
|
// TODO: this does not even slightly work with subcommands
|
|
const progName = prog orelse std.fs.path.basename(argit.next() orelse @panic("base, name?"));
|
|
|
|
// TODO: only do this if the help flag has been passed. Alternatively, try
|
|
// to assemble this at comptime?
|
|
var helpDescription: params.CommandData = .{ .name = data.name };
|
|
try buildHelpDescription(progName, &helpDescription, alloc);
|
|
defer alloc.free(helpDescription.help);
|
|
|
|
var seenArgs: u32 = 0;
|
|
argloop: while (argit.next()) |arg| {
|
|
if (parseState == .Mixed and arg.len > 1 and arg[0] == '-') {
|
|
if (std.mem.eql(u8, "--", arg)) {
|
|
// TODO: the way this works, -- only forces argument
|
|
// parsing until a subcommand is found. This seems
|
|
// reasonable to me, but it may be unexpected that
|
|
// `command -a -- subcommand -b` parses b as an option
|
|
// flag. We could propagate the forced args flag to
|
|
// subcommands, but I'm not sure that would be better.
|
|
//
|
|
// Another option is to stop parsing altogether when --
|
|
// is hit, but that means that subcommands cannot be
|
|
// invoked at the same time as forced arguments, which
|
|
// seems worse somehow, as it affects macroscopic CLI
|
|
// behavior.
|
|
parseState = .ForcedArgs;
|
|
continue :argloop;
|
|
}
|
|
|
|
if (arg[1] == '-') {
|
|
// we have a long flag or option
|
|
specloop: inline for (spec) |param| {
|
|
switch (@TypeOf(param).brand) {
|
|
.Option => {
|
|
if (param.long) |flag| {
|
|
if (std.mem.startsWith(u8, arg, flag) and (flag.len == arg.len or arg[flag.len] == '=')) {
|
|
const val = if (flag.len == arg.len)
|
|
argit.next() orelse return OptionError.MissingArgument
|
|
else
|
|
arg[flag.len + 1 ..];
|
|
|
|
if (comptime param.required()) {
|
|
@field(required, param.name) = true;
|
|
}
|
|
|
|
if (param.hideResult == false) {
|
|
@field(result, param.name) = try param.handler.?(context, val);
|
|
}
|
|
continue :argloop;
|
|
}
|
|
}
|
|
},
|
|
.Flag => {
|
|
inline for (.{ .{ param.truthy.long, true }, .{ param.falsy.long, false } }) |variant| {
|
|
if (variant[0]) |flag| {
|
|
if (std.mem.eql(u8, flag, arg)) {
|
|
if (param.eager) |handler| {
|
|
try handler(context, helpDescription);
|
|
}
|
|
|
|
if (param.hideResult == false) {
|
|
@field(result, param.name) = variant[1];
|
|
}
|
|
continue :argloop;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
.Argument, .Command => continue :specloop,
|
|
}
|
|
}
|
|
|
|
// nothing matched
|
|
return OptionError.UnknownOption;
|
|
} else {
|
|
// we have a short flag, which may be multiple fused flags
|
|
shortloop: for (arg[1..], 0..) |shorty, idx| {
|
|
specloop: inline for (spec) |param| {
|
|
switch (@TypeOf(param).brand) {
|
|
.Option => {
|
|
if (param.short) |flag| {
|
|
if (flag[1] == shorty) {
|
|
if (comptime param.required()) {
|
|
@field(required, param.name) = true;
|
|
}
|
|
|
|
const val = if (arg.len > (idx + 2))
|
|
arg[(idx + 2)..]
|
|
else
|
|
argit.next() orelse return OptionError.MissingArgument;
|
|
|
|
if (param.hideResult == false) {
|
|
@field(result, param.name) = try param.handler.?(context, val);
|
|
}
|
|
continue :argloop;
|
|
}
|
|
}
|
|
},
|
|
.Flag => {
|
|
inline for (.{ .{ param.truthy.short, true }, .{ param.falsy.short, false } }) |variant| {
|
|
if (variant[0]) |flag| {
|
|
if (flag[1] == shorty) {
|
|
if (param.eager) |handler| {
|
|
try handler(context, helpDescription);
|
|
}
|
|
|
|
if (param.hideResult == false) {
|
|
@field(result, param.name) = variant[1];
|
|
}
|
|
continue :shortloop;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
.Argument, .Command => continue :specloop,
|
|
}
|
|
}
|
|
// nothing matched
|
|
return OptionError.UnknownOption;
|
|
}
|
|
}
|
|
} else {
|
|
// we have a subcommand or an Argument. Arguments are parsed first, exclusively.
|
|
defer seenArgs += 1;
|
|
comptime var idx = 0;
|
|
inline for (spec) |param| {
|
|
switch (@TypeOf(param).brand) {
|
|
.Command => {
|
|
const name = @TypeOf(param).data.name;
|
|
if (std.mem.eql(u8, name, arg)) {
|
|
// we're calling a subcommand
|
|
try checkErrors(seenArgs, required);
|
|
try callback(context, result);
|
|
|
|
const combined = try std.mem.join(alloc, " ", &[_][]const u8{ progName, name });
|
|
defer alloc.free(combined);
|
|
return param.internal_execute(alloc, argit_type, argit, context, combined);
|
|
}
|
|
},
|
|
.Argument => {
|
|
if (seenArgs == idx) {
|
|
if (comptime param.required()) {
|
|
@field(required, param.name) = true;
|
|
}
|
|
@field(result, param.name) = try param.handler.?(context, arg);
|
|
continue :argloop;
|
|
}
|
|
idx += 1;
|
|
},
|
|
else => continue,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
try checkErrors(seenArgs, required);
|
|
try callback(context, result);
|
|
}
|
|
|
|
fn buildHelpDescription(
|
|
progName: []const u8,
|
|
inData: *params.CommandData,
|
|
alloc: std.mem.Allocator,
|
|
) !void {
|
|
var seen: u32 = 0;
|
|
var maxlen: usize = 0;
|
|
|
|
var argnames: [param_count.args][]const u8 = undefined;
|
|
var args: [param_count.args]ParamRow = undefined;
|
|
inline for (spec) |param| {
|
|
switch (@TypeOf(param).brand) {
|
|
.Argument => {
|
|
argnames[seen] = param.name;
|
|
args[seen] = try describeArgument(param, alloc);
|
|
maxlen = @max(args[seen].flags.len, maxlen);
|
|
seen += 1;
|
|
},
|
|
else => continue,
|
|
}
|
|
}
|
|
|
|
seen = 0;
|
|
var rows: [param_count.opts]ParamRow = undefined;
|
|
inline for (spec) |param| {
|
|
const describer = switch (@TypeOf(param).brand) {
|
|
.Option => describeOption,
|
|
.Flag => describeFlag,
|
|
else => continue,
|
|
};
|
|
rows[seen] = try describer(param, alloc);
|
|
maxlen = @max(rows[seen].flags.len, maxlen);
|
|
seen += 1;
|
|
}
|
|
|
|
seen = 0;
|
|
var subs: [param_count.subs]ParamRow = undefined;
|
|
inline for (spec) |param| {
|
|
switch (@TypeOf(param).brand) {
|
|
.Command => {
|
|
subs[seen] = try describeSubcommand(param, alloc);
|
|
maxlen = @max(subs[seen].flags.len, maxlen);
|
|
seen += 1;
|
|
},
|
|
else => continue,
|
|
}
|
|
}
|
|
|
|
var buffer = std.ArrayList(u8).init(alloc);
|
|
const writer = buffer.writer();
|
|
|
|
for (argnames) |name| {
|
|
try std.fmt.format(writer, " <{s}>", .{name});
|
|
}
|
|
|
|
const short_args = try buffer.toOwnedSlice();
|
|
defer alloc.free(short_args);
|
|
|
|
try std.fmt.format(
|
|
writer,
|
|
"Usage: {s}{s}{s}{s}\n\n",
|
|
.{
|
|
progName,
|
|
if (param_count.opts > 0) " [options]" else "",
|
|
if (param_count.args > 0) short_args else "",
|
|
if (param_count.subs > 0) " [subcommand] ..." else "",
|
|
},
|
|
);
|
|
|
|
try writer.writeAll(data.help);
|
|
if (!std.mem.endsWith(u8, data.help, "\n")) {
|
|
try writer.writeAll("\n");
|
|
}
|
|
|
|
if (param_count.args > 0) {
|
|
try writer.writeAll("\nArguments:\n");
|
|
|
|
for (args) |arg| {
|
|
defer arg.deinit(alloc);
|
|
try std.fmt.format(
|
|
writer,
|
|
" {[0]s: <[1]}{[2]s}\n",
|
|
.{ arg.flags, maxlen + 2, arg.description },
|
|
);
|
|
}
|
|
}
|
|
|
|
if (param_count.opts > 0) {
|
|
try writer.writeAll("\nOptions:\n");
|
|
|
|
for (rows) |row| {
|
|
defer row.deinit(alloc);
|
|
try std.fmt.format(
|
|
writer,
|
|
" {[0]s: <[1]}{[2]s}\n",
|
|
.{ row.flags, maxlen + 2, row.description },
|
|
);
|
|
}
|
|
}
|
|
|
|
if (param_count.subs > 0) {
|
|
try writer.writeAll("\nSubcommands:\n");
|
|
// try std.fmt.format(writer, "\nSubcommands {d}:\n", .{param_count.subs});
|
|
for (subs) |sub| {
|
|
defer sub.deinit(alloc);
|
|
try std.fmt.format(
|
|
writer,
|
|
" {[0]s: <[1]}{[2]s}\n",
|
|
.{ sub.flags, maxlen + 2, sub.description },
|
|
);
|
|
}
|
|
}
|
|
|
|
inData.help = try buffer.toOwnedSlice();
|
|
}
|
|
|
|
const ParamRow = struct {
|
|
flags: []const u8,
|
|
description: []const u8,
|
|
|
|
pub fn deinit(self: @This(), alloc: std.mem.Allocator) void {
|
|
alloc.free(self.flags);
|
|
alloc.free(self.description);
|
|
}
|
|
};
|
|
|
|
fn describeArgument(comptime param: anytype, alloc: std.mem.Allocator) !ParamRow {
|
|
var buffer = std.ArrayList(u8).init(alloc);
|
|
const writer = buffer.writer();
|
|
|
|
try writer.writeAll(param.name);
|
|
try std.fmt.format(writer, " ({s})", .{param.type_name()});
|
|
|
|
const flags = try buffer.toOwnedSlice();
|
|
|
|
if (param.help) |help| {
|
|
try writer.writeAll(help);
|
|
}
|
|
if (param.required()) {
|
|
try writer.writeAll(" [required]");
|
|
}
|
|
const description = try buffer.toOwnedSlice();
|
|
|
|
return ParamRow{ .flags = flags, .description = description };
|
|
}
|
|
|
|
fn describeOption(comptime param: anytype, alloc: std.mem.Allocator) !ParamRow {
|
|
var buffer = std.ArrayList(u8).init(alloc);
|
|
const writer = buffer.writer();
|
|
|
|
if (param.envVar) |varName| {
|
|
try std.fmt.format(writer, "{s}", .{varName});
|
|
}
|
|
if (param.short) |short| {
|
|
if (buffer.items.len > 0) {
|
|
try writer.writeAll(", ");
|
|
}
|
|
try writer.writeAll(short);
|
|
}
|
|
if (param.long) |long| {
|
|
if (buffer.items.len > 0) {
|
|
try writer.writeAll(", ");
|
|
}
|
|
try writer.writeAll(long);
|
|
}
|
|
try std.fmt.format(writer, " ({s})", .{param.type_name()});
|
|
|
|
const flags = try buffer.toOwnedSlice();
|
|
|
|
if (param.help) |help| {
|
|
try writer.writeAll(help);
|
|
}
|
|
if (param.required()) {
|
|
try writer.writeAll(" [required]");
|
|
}
|
|
const description = try buffer.toOwnedSlice();
|
|
|
|
return ParamRow{ .flags = flags, .description = description };
|
|
}
|
|
|
|
fn describeFlag(comptime param: anytype, alloc: std.mem.Allocator) !ParamRow {
|
|
var buffer = std.ArrayList(u8).init(alloc);
|
|
const writer = buffer.writer();
|
|
|
|
var truthy_seen: bool = false;
|
|
var falsy_seen: bool = false;
|
|
|
|
if (param.truthy.short) |short| {
|
|
try writer.writeAll(short);
|
|
truthy_seen = true;
|
|
}
|
|
if (param.truthy.long) |long| {
|
|
if (truthy_seen) {
|
|
try writer.writeAll(", ");
|
|
}
|
|
try writer.writeAll(long);
|
|
truthy_seen = true;
|
|
}
|
|
|
|
if (param.falsy.short) |short| {
|
|
if (truthy_seen) {
|
|
try writer.writeAll("/");
|
|
}
|
|
try writer.writeAll(short);
|
|
falsy_seen = true;
|
|
}
|
|
if (param.falsy.long) |long| {
|
|
if (falsy_seen) {
|
|
try writer.writeAll(", ");
|
|
} else if (truthy_seen) {
|
|
try writer.writeAll("/");
|
|
}
|
|
try writer.writeAll(long);
|
|
falsy_seen = true;
|
|
}
|
|
|
|
if (param.envVar) |varName| {
|
|
try std.fmt.format(writer, " ({s})", .{varName});
|
|
}
|
|
|
|
const flags = try buffer.toOwnedSlice();
|
|
|
|
if (param.help) |help| {
|
|
try writer.writeAll(help);
|
|
}
|
|
if (param.required()) {
|
|
try writer.writeAll(" [required]");
|
|
}
|
|
const description = try buffer.toOwnedSlice();
|
|
|
|
return ParamRow{ .flags = flags, .description = description };
|
|
}
|
|
|
|
fn describeSubcommand(comptime param: anytype, alloc: std.mem.Allocator) !ParamRow {
|
|
var buffer = std.ArrayList(u8).init(alloc);
|
|
const writer = buffer.writer();
|
|
|
|
const paramdata = @TypeOf(param).data;
|
|
|
|
try writer.writeAll(paramdata.name);
|
|
|
|
const flags = try buffer.toOwnedSlice();
|
|
|
|
try writer.writeAll(paramdata.help);
|
|
const description = try buffer.toOwnedSlice();
|
|
|
|
return ParamRow{ .flags = flags, .description = description };
|
|
}
|
|
|
|
pub fn OutType() type {
|
|
return CommandResult(spec, UserContext);
|
|
}
|
|
|
|
inline fn checkErrors(seenArgs: u32, required: RequiredType) OptionError!void {
|
|
if (seenArgs < param_count.args) {
|
|
return OptionError.MissingArgument;
|
|
} else if (seenArgs > param_count.args) {
|
|
return OptionError.ExtraArguments;
|
|
}
|
|
|
|
describeError(required);
|
|
|
|
inline for (@typeInfo(@TypeOf(required)).Struct.fields) |field| {
|
|
if (@field(required, field.name) == false) {
|
|
return OptionError.MissingOption;
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn describeError(required: RequiredType) void {
|
|
inline for (@typeInfo(@TypeOf(required)).Struct.fields) |field| {
|
|
if (@field(required, field.name) == false) {
|
|
std.debug.print("missing {s}\n", .{field.name});
|
|
}
|
|
}
|
|
}
|
|
|
|
fn attachSubcommands(_: @This(), alloc: std.mem.Allocator) !void {
|
|
if (data.subcommands == null) {
|
|
data.subcommands = std.ArrayList(*params.CommandData).init(alloc);
|
|
}
|
|
|
|
inline for (spec) |param| {
|
|
switch (@TypeOf(param).brand) {
|
|
.Command => {
|
|
try data.subcommands.?.append(&@TypeOf(param).data);
|
|
},
|
|
else => continue,
|
|
}
|
|
}
|
|
}
|
|
|
|
fn scryTruthiness(input: []const u8) bool {
|
|
// empty string is 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;
|
|
}
|
|
}
|
|
}
|
|
// TODO: actually try float conversion on input string? This seems
|
|
// really silly to me, in the context of the shell, but for example
|
|
// MY_VAR=0 evaluates to false but MY_VAR=0.0 evaluates to true. And
|
|
// if we accept multiple representations of zero, a whole can of
|
|
// worms gets opened. Should 0x0 be falsy? 0o0? That's a lot of
|
|
// goofy edge cases.
|
|
|
|
// any nonempty value is considered to be truthy.
|
|
return true;
|
|
}
|
|
|
|
fn extractEnvVars(
|
|
alloc: std.mem.Allocator,
|
|
result: *ResultType,
|
|
required: *RequiredType,
|
|
context: UserContext,
|
|
) !void {
|
|
var env: std.process.EnvMap = try std.process.getEnvMap(alloc);
|
|
defer env.deinit();
|
|
|
|
inline for (spec) |param| {
|
|
const ParamType = @TypeOf(param);
|
|
switch (ParamType.brand) {
|
|
.Option => {
|
|
if (param.envVar) |want| {
|
|
if (env.get(want)) |value| {
|
|
if (comptime param.required()) {
|
|
@field(required, param.name) = true;
|
|
}
|
|
|
|
@field(result, param.name) = try param.handler.?(context, value);
|
|
}
|
|
}
|
|
},
|
|
.Flag => {
|
|
if (param.envVar) |want| {
|
|
if (env.get(want)) |value| {
|
|
@field(result, param.name) = scryTruthiness(value);
|
|
}
|
|
}
|
|
},
|
|
.Argument, .Command => continue,
|
|
}
|
|
}
|
|
}
|
|
|
|
inline fn createCommandresult() ResultType {
|
|
var result: ResultType = undefined;
|
|
inline for (spec) |param| {
|
|
switch (@TypeOf(param).brand) {
|
|
.Command => continue,
|
|
else => if (param.hideResult == false) {
|
|
@field(result, param.name) = param.default orelse continue;
|
|
},
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
};
|
|
}
|
|
|
|
pub fn CommandResult(comptime spec: anytype, comptime UserContext: type) type {
|
|
comptime {
|
|
// not sure how to do this without iterating twice, so let's iterate
|
|
// twice
|
|
var outsize = 0;
|
|
for (spec) |param| {
|
|
const ParamType = @TypeOf(param);
|
|
if (ParamType.ContextType != UserContext) {
|
|
@compileError("param \"" ++ param.name ++ "\" has wrong context type (wanted: " ++ @typeName(UserContext) ++ ", got: " ++ @typeName(ParamType.ContextType) ++ ")");
|
|
}
|
|
switch (ParamType.brand) {
|
|
.Argument, .Option => {
|
|
if (param.handler == null) {
|
|
@compileError("param \"" ++ param.name ++ "\" does not have a handler");
|
|
}
|
|
},
|
|
else => {},
|
|
}
|
|
|
|
switch (ParamType.brand) {
|
|
.Command => continue,
|
|
else => {
|
|
if (param.hideResult == false) {
|
|
outsize += 1;
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|
|
var fields: [outsize]StructField = undefined;
|
|
|
|
var idx = 0;
|
|
for (spec) |param| {
|
|
const ParamType = @TypeOf(param);
|
|
switch (ParamType.brand) {
|
|
.Command => continue,
|
|
else => if (param.hideResult == true) continue,
|
|
}
|
|
|
|
const FieldType = ParamType.ResultType;
|
|
|
|
fields[idx] = .{
|
|
.name = param.name,
|
|
.type = FieldType,
|
|
.default_value = @ptrCast(?*const anyopaque, ¶m.default),
|
|
.is_comptime = false,
|
|
.alignment = @alignOf(FieldType),
|
|
};
|
|
|
|
idx += 1;
|
|
}
|
|
|
|
return @Type(.{ .Struct = .{
|
|
.layout = .Auto,
|
|
.fields = &fields,
|
|
.decls = &.{},
|
|
.is_tuple = false,
|
|
} });
|
|
}
|
|
}
|
|
|
|
fn RequiredTracker(comptime spec: anytype) type {
|
|
comptime {
|
|
// not sure how to do this without iterating twice, so let's iterate
|
|
// twice
|
|
var outsize = 0;
|
|
for (spec) |param| {
|
|
const ParamType = @TypeOf(param);
|
|
switch (ParamType.brand) {
|
|
// flags are always optional, and commands don't map into the
|
|
// output type.
|
|
.Flag, .Command => continue,
|
|
.Argument, .Option => if (param.required()) {
|
|
// if mayBeOptional is false, then the argument/option is
|
|
// required. Otherwise, we have to check if a default has
|
|
// been provided.
|
|
outsize += 1;
|
|
},
|
|
}
|
|
}
|
|
|
|
var fields: [outsize]StructField = undefined;
|
|
|
|
var idx = 0;
|
|
for (spec) |param| {
|
|
const ParamType = @TypeOf(param);
|
|
switch (ParamType.brand) {
|
|
.Flag, .Command => continue,
|
|
.Argument, .Option => if (param.required()) {
|
|
fields[idx] = .{
|
|
.name = param.name,
|
|
.type = bool,
|
|
.default_value = &false,
|
|
.is_comptime = false,
|
|
.alignment = @alignOf(bool),
|
|
};
|
|
|
|
idx += 1;
|
|
},
|
|
}
|
|
}
|
|
|
|
return @Type(.{ .Struct = .{
|
|
.layout = .Auto,
|
|
.fields = &fields,
|
|
.decls = &.{},
|
|
.is_tuple = false,
|
|
} });
|
|
}
|
|
}
|
|
|
|
test {
|
|
_ = meta;
|
|
}
|