help: start grinding away at help text generation
I am resisting the urge to try to codegolf this into fewer lines. It's going to end up being a sprawl, but it is what it is. The main part of this that will actually require intelligent thought is the column wrapping and alignment. I think I will probably be implementing a custom writer style thing to handle that. There are a lot of annoying loose odds and ends here. Choice types should list all the choices. But we can't necessarily assume that an enum-typed parameter is a choice type (only if it uses the default converter). Perhaps the conversion stuff should be turned into an interface that can also be responsible for converting the default value and providing additional information. For now I will probably just hack it so that I can move on to other things.
This commit is contained in:
parent
2f591fc02f
commit
facda65271
@ -16,12 +16,17 @@ const cli = cmd: {
|
||||
.short_tag = "-t",
|
||||
.long_tag = "--test",
|
||||
.env_var = "NOCLIP_TEST",
|
||||
.description = "multi-value test option",
|
||||
.nice_type_name = "int> <int",
|
||||
});
|
||||
cmd.add_option(.{ .OutputType = Choice }, .{
|
||||
.name = "choice",
|
||||
.short_tag = "-c",
|
||||
.long_tag = "--choice",
|
||||
.default = .second,
|
||||
.env_var = "NOCLIP_CHOICE",
|
||||
.description = "enum choice option",
|
||||
.nice_type_name = "choice",
|
||||
});
|
||||
cmd.add_option(.{ .OutputType = u32 }, .{
|
||||
.name = "default",
|
||||
@ -29,23 +34,28 @@ const cli = cmd: {
|
||||
.long_tag = "--default",
|
||||
.env_var = "NOCLIP_DEFAULT",
|
||||
.default = 100,
|
||||
.description = "default value integer option",
|
||||
.nice_type_name = "uint",
|
||||
});
|
||||
cmd.add_option(.{ .OutputType = u8, .multi = true }, .{
|
||||
.name = "multi",
|
||||
.short_tag = "-m",
|
||||
.long_tag = "--multi",
|
||||
.env_var = "NOCLIP_MULTI",
|
||||
.description = "multiple specification test option",
|
||||
});
|
||||
cmd.add_flag(.{}, .{
|
||||
.name = "flag",
|
||||
.truthy = .{ .short_tag = "-f", .long_tag = "--flag" },
|
||||
.falsy = .{ .long_tag = "--no-flag" },
|
||||
.falsy = .{ .short_tag = "-F", .long_tag = "--no-flag" },
|
||||
.env_var = "NOCLIP_FLAG",
|
||||
.description = "boolean flag",
|
||||
});
|
||||
cmd.add_flag(.{ .multi = true }, .{
|
||||
.name = "multiflag",
|
||||
.truthy = .{ .short_tag = "-M" },
|
||||
.env_var = "NOCLIP_MULTIFLAG",
|
||||
.description = "multiple specification test flag ",
|
||||
});
|
||||
cmd.add_argument(.{ .OutputType = []const u8 }, .{
|
||||
.name = "arg",
|
||||
|
@ -1,6 +1,7 @@
|
||||
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");
|
||||
@ -103,6 +104,7 @@ pub fn CommandBuilder(comptime UserContext: type) type {
|
||||
return Parser(self, callback){
|
||||
.allocator = allocator,
|
||||
.subcommands = std.hash_map.StringHashMap(ParserInterface).init(allocator),
|
||||
.help_builder = help.HelpBuilder(self).init(allocator),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -21,7 +21,7 @@ pub fn default_converter(comptime gen: ParameterGenerics) ?ConverterSignature(ge
|
||||
string_converter(gen)
|
||||
else
|
||||
null,
|
||||
.Enum => choice_converter(gen),
|
||||
.Enum => |info| if (info.is_exhaustive) choice_converter(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)
|
||||
|
308
source/help.zig
308
source/help.zig
@ -1,4 +1,308 @@
|
||||
const std = @import("std");
|
||||
|
||||
fn HelpBuilder(comptime command: anytype) type {
|
||||
_ = command;
|
||||
const ncmeta = @import("./meta.zig");
|
||||
const parser = @import("./parser.zig");
|
||||
const FixedCount = @import("./parameters.zig").FixedCount;
|
||||
|
||||
// error.OutOfMemory
|
||||
|
||||
const AlignablePair = struct {
|
||||
left: []const u8,
|
||||
right: []const u8,
|
||||
};
|
||||
|
||||
const OptionDescription = struct {
|
||||
pairs: []AlignablePair,
|
||||
just: usize,
|
||||
};
|
||||
|
||||
pub fn HelpBuilder(comptime command: anytype) type {
|
||||
const help_info = opt_info(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 build_message(
|
||||
self: *@This(),
|
||||
name: []const u8,
|
||||
subcommands: std.hash_map.StringHashMap(parser.ParserInterface),
|
||||
) ![]const u8 {
|
||||
try self.describe_command(name, subcommands);
|
||||
|
||||
return self.writebuffer.toOwnedSlice();
|
||||
}
|
||||
|
||||
fn describe_command(
|
||||
self: *@This(),
|
||||
name: []const u8,
|
||||
subcommands: std.hash_map.StringHashMap(parser.ParserInterface),
|
||||
) !void {
|
||||
const writer = self.writebuffer.writer();
|
||||
try writer.print(
|
||||
"Usage: {s}{s}{s}{s}\n\n",
|
||||
.{
|
||||
name,
|
||||
try self.option_brief(),
|
||||
if (help_info.arguments.len > 0) " <arguments>" else "",
|
||||
if (subcommands.count() > 0) " [<subcommand> ...]" else "",
|
||||
},
|
||||
);
|
||||
|
||||
try writer.writeAll(std.mem.trim(u8, command.description, " \n"));
|
||||
try writer.writeAll("\n\n");
|
||||
|
||||
const options = try self.describe_options();
|
||||
if (options.pairs.len > 0) {
|
||||
try writer.writeAll("Options:\n");
|
||||
}
|
||||
|
||||
for (options.pairs) |pair| {
|
||||
try writer.print(
|
||||
" {[0]s: <[1]}{[2]s}\n",
|
||||
.{ pair.left, options.just + 3, pair.right },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn option_brief(self: @This()) ![]const u8 {
|
||||
if (comptime help_info.options.len > 1) {
|
||||
return " [options...]";
|
||||
} else if (comptime help_info.options.len == 1) {
|
||||
return " [option]";
|
||||
} else if (comptime help_info.options.len == 0) {
|
||||
return "";
|
||||
} else {
|
||||
var buffer = std.ArrayList(u8).init(self.writebuffer.allocator);
|
||||
const writer = buffer.writer();
|
||||
|
||||
for (comptime help_info.options) |opt| {
|
||||
try writer.writeAll(" [");
|
||||
|
||||
var written = false;
|
||||
if (opt.short_truthy) |tag| {
|
||||
try writer.writeAll(tag);
|
||||
written = true;
|
||||
}
|
||||
if (opt.long_truthy) |tag| {
|
||||
if (written) try writer.writeAll(" | ");
|
||||
try writer.writeAll(tag);
|
||||
written = true;
|
||||
}
|
||||
if (opt.short_falsy) |tag| {
|
||||
if (written) try writer.writeAll(" | ");
|
||||
try writer.writeAll(tag);
|
||||
written = true;
|
||||
}
|
||||
if (opt.long_falsy) |tag| {
|
||||
if (written) try writer.writeAll(" | ");
|
||||
try writer.writeAll(tag);
|
||||
}
|
||||
|
||||
try writer.writeAll("]");
|
||||
}
|
||||
|
||||
return buffer.toOwnedSlice();
|
||||
}
|
||||
}
|
||||
|
||||
fn describe_options(self: @This()) !OptionDescription {
|
||||
var pairs = std.ArrayList(AlignablePair).init(self.writebuffer.allocator);
|
||||
|
||||
var just: usize = 0;
|
||||
for (comptime help_info.options) |opt| {
|
||||
const pair = try self.describe_option(opt);
|
||||
if (pair.left.len > just) just = pair.left.len;
|
||||
try pairs.append(pair);
|
||||
}
|
||||
|
||||
return .{
|
||||
.pairs = try pairs.toOwnedSlice(),
|
||||
.just = just,
|
||||
};
|
||||
}
|
||||
|
||||
fn describe_option(self: @This(), opt: OptHelp) !AlignablePair {
|
||||
var buffer = std.ArrayList(u8).init(self.writebuffer.allocator);
|
||||
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.description.len > 0) {
|
||||
try writer.writeAll(opt.description);
|
||||
}
|
||||
|
||||
if (comptime opt.default) |def| {
|
||||
if (buffer.items.len > 0) try writer.writeAll(" ");
|
||||
try writer.print("(default: {s})", .{def});
|
||||
}
|
||||
|
||||
if (comptime opt.required) {
|
||||
if (buffer.items.len > 0) try writer.writeAll(" ");
|
||||
try writer.writeAll("[required]");
|
||||
}
|
||||
const right = try buffer.toOwnedSlice();
|
||||
|
||||
return .{ .left = left, .right = right };
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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 = "",
|
||||
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 = "",
|
||||
type_name: []const u8 = "",
|
||||
multi: bool = false,
|
||||
required: bool = true,
|
||||
};
|
||||
|
||||
pub fn opt_info(comptime command: anytype) CommandHelp {
|
||||
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 != .Nominal) continue :paramloop;
|
||||
|
||||
if (!std.mem.eql(u8, last_name, param.name)) {
|
||||
if (last_name.len > 0) {
|
||||
if (last_option.short_truthy != null or
|
||||
last_option.long_truthy != null or
|
||||
last_option.short_falsy != null or
|
||||
last_option.long_falsy != null)
|
||||
{
|
||||
options = options ++ &[_]OptHelp{last_option};
|
||||
} else {
|
||||
env_vars = env_vars ++ &[_]EnvHelp{.{
|
||||
.env_var = last_option.env_var,
|
||||
.description = last_option.description,
|
||||
.default = last_option.default,
|
||||
}};
|
||||
}
|
||||
}
|
||||
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.
|
||||
writer.print("{any}", .{def}) catch @compileError("whoah");
|
||||
last_option.default = buf.buffer;
|
||||
}
|
||||
}
|
||||
|
||||
if (last_name.len > 0) {
|
||||
if (last_option.short_truthy != null or
|
||||
last_option.long_truthy != null or
|
||||
last_option.short_falsy != null or
|
||||
last_option.long_falsy != null)
|
||||
{
|
||||
options = options ++ &[_]OptHelp{last_option};
|
||||
} else {
|
||||
env_vars = env_vars ++ &[_]EnvHelp{.{
|
||||
.env_var = last_option.env_var,
|
||||
.description = last_option.description,
|
||||
.default = last_option.default,
|
||||
}};
|
||||
}
|
||||
}
|
||||
|
||||
return .{
|
||||
.options = options,
|
||||
.arguments = arguments,
|
||||
.env_vars = env_vars,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -52,6 +52,65 @@ pub fn enum_length(comptime T: type) comptime_int {
|
||||
return @typeInfo(T).Enum.fields.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);
|
||||
|
@ -9,10 +9,12 @@ const ParameterType = enum {
|
||||
Executable,
|
||||
};
|
||||
|
||||
pub const FixedCount = u32;
|
||||
|
||||
pub const ValueCount = union(enum) {
|
||||
flag: void,
|
||||
count: void,
|
||||
fixed: u32,
|
||||
fixed: FixedCount,
|
||||
};
|
||||
|
||||
pub const FlagBias = enum {
|
||||
@ -227,12 +229,12 @@ fn OptionType(comptime generics: ParameterGenerics) type {
|
||||
|
||||
fn check_short(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);
|
||||
if (short.len != 2 or short[0] != '-') @compileError("bad short tag: " ++ short);
|
||||
}
|
||||
|
||||
fn check_long(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);
|
||||
if (long.len < 3 or long[0] != '-' or long[1] != '-') @compileError("bad long tag: " ++ long);
|
||||
}
|
||||
|
||||
pub fn make_option(comptime generics: ParameterGenerics, comptime opts: OptionConfig(generics)) OptionType(generics) {
|
||||
|
@ -1,7 +1,8 @@
|
||||
const std = @import("std");
|
||||
|
||||
const ncmeta = @import("./meta.zig");
|
||||
const errors = @import("./errors.zig");
|
||||
const help = @import("./help.zig");
|
||||
const ncmeta = @import("./meta.zig");
|
||||
|
||||
const ParseError = errors.ParseError;
|
||||
const NoclipError = errors.NoclipError;
|
||||
@ -75,6 +76,7 @@ pub fn Parser(comptime command: anytype, comptime callback: anytype) type {
|
||||
allocator: std.mem.Allocator,
|
||||
subcommands: std.hash_map.StringHashMap(ParserInterface),
|
||||
subcommand: ?ParserInterface = null,
|
||||
help_builder: help.HelpBuilder(command),
|
||||
|
||||
pub fn add_subcommand(self: *@This(), verb: []const u8, parser: ParserInterface) !void {
|
||||
try self.subcommands.put(verb, parser);
|
||||
@ -119,15 +121,6 @@ pub fn Parser(comptime command: anytype, comptime callback: anytype) type {
|
||||
try self.read_environment(env);
|
||||
try self.convert_eager(context);
|
||||
|
||||
inline for (@typeInfo(@TypeOf(self.intermediate)).Struct.fields) |field| {
|
||||
if (@field(self.intermediate, field.name) == null) {
|
||||
std.debug.print("{s}: null,\n", .{field.name});
|
||||
} else {
|
||||
std.debug.print("{s}: ", .{field.name});
|
||||
self.print_value(@field(self.intermediate, field.name).?, "");
|
||||
}
|
||||
}
|
||||
|
||||
if (self.subcommand) |verb| try verb.parse(args[sliceto..], env);
|
||||
}
|
||||
|
||||
@ -423,10 +416,13 @@ pub fn Parser(comptime command: anytype, comptime callback: anytype) type {
|
||||
}
|
||||
}
|
||||
|
||||
fn print_help(self: @This()) void {
|
||||
_ = self;
|
||||
std.debug.print("help!!!\n", .{});
|
||||
std.process.exit(0);
|
||||
fn print_help(self: *@This()) noreturn {
|
||||
defer std.process.exit(0);
|
||||
const stderr = std.io.getStdErr().writer();
|
||||
if (self.help_builder.build_message("test", self.subcommands)) |message|
|
||||
stderr.writeAll(message) catch return
|
||||
else |_|
|
||||
stderr.writeAll("There was a problem generating the help.") catch return;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user