NOCLIP/source/help.zig
torque 0d5dd9b36c
help: implement subcommand descriptions
I believe we've produced a superset of the functionality that was
present before rewriting all of the code.

There are still a lot of fiddly little details that need to be thought
through in order to produce something that is righteously flexible,
but I think this is in reasonable shape to start inventing real-world
uses for it.

Adding some tests, cleaning up a little bit of the allocation handling
(make better use of the arena allocators—we are definitely sort of
leaking memory at the moment), and writing documentation are still on
the roadmap.
2023-04-06 18:31:29 -07:00

414 lines
14 KiB
Zig

const std = @import("std");
const ncmeta = @import("./meta.zig");
const parser = @import("./parser.zig");
const FixedCount = @import("./parameters.zig").FixedCount;
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: parser.CommandMap,
) ![]const u8 {
const writer = self.writebuffer.writer();
try writer.print(
"Usage: {s}{s}{s}{s}\n\n",
.{
name,
self.option_brief(),
try self.args_brief(),
self.subcommands_brief(subcommands),
},
);
try writer.writeAll(std.mem.trim(u8, command.description, " \n"));
try writer.writeAll("\n\n");
const arguments = try self.describe_arguments();
const options = try self.describe_options();
const env_vars = try self.describe_env();
const subcs = try self.describe_subcommands(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 writer.print(
" {[0]s: <[1]}{[2]s}\n",
.{ pair.left, max_just + 3, pair.right },
);
}
try writer.writeAll("\n");
}
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, max_just + 3, pair.right },
);
}
try writer.writeAll("\n");
}
if (env_vars.pairs.len > 0) {
try writer.writeAll("Environment variables:\n");
for (env_vars.pairs) |pair| {
try writer.print(
" {[0]s: <[1]}{[2]s}\n",
.{ pair.left, max_just + 3, pair.right },
);
}
try writer.writeAll("\n");
}
if (subcs.pairs.len > 0) {
try writer.writeAll("Subcommands:\n");
for (subcs.pairs) |pair| {
try writer.print(
" {[0]s: <[1]}{[2]s}\n",
.{ pair.left, max_just + 3, pair.right },
);
}
try writer.writeAll("\n");
}
return self.writebuffer.toOwnedSlice();
}
fn option_brief(_: @This()) []const u8 {
return if (comptime help_info.options.len > 0)
" [options...]"
else
"";
}
fn args_brief(self: @This()) ![]const u8 {
var buf = std.ArrayList(u8).init(self.writebuffer.allocator);
const writer = buf.writer();
for (comptime help_info.arguments) |arg| {
try writer.writeAll(" ");
if (!arg.required) try writer.writeAll("[");
try writer.print("<{s}>", .{arg.name});
if (!arg.required) try writer.writeAll("]");
}
return buf.toOwnedSlice();
}
fn subcommands_brief(
_: @This(),
subcommands: parser.CommandMap,
) []const u8 {
return if (subcommands.count() > 0)
" <subcommand ...>"
else
"";
}
fn describe_arguments(self: @This()) !OptionDescription {
var pairs = std.ArrayList(AlignablePair).init(self.writebuffer.allocator);
var just: usize = 0;
for (comptime help_info.arguments) |arg| {
if (arg.description.len == 0) continue;
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 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.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 describe_env(self: @This()) !OptionDescription {
var pairs = std.ArrayList(AlignablePair).init(self.writebuffer.allocator);
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 describe_subcommands(self: @This(), subcommands: parser.CommandMap) !OptionDescription {
var pairs = std.ArrayList(AlignablePair).init(self.writebuffer.allocator);
var just: usize = 0;
var iter = subcommands.keyIterator();
while (iter.next()) |key| {
const subif = subcommands.get(key.*).?;
const short = ncmeta.partition(u8, subif.describe(), "\n");
const pair: AlignablePair = .{
.left = key.*,
.right = short[0],
};
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 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 == .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 (env_only(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.
writer.print("{any}", .{def}) catch @compileError("whoah");
last_option.default = buf.buffer;
}
}
if (last_name.len > 0) {
if (env_only(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 env_only(option: OptHelp) bool {
return option.short_truthy == null and
option.long_truthy == null and
option.short_falsy == null and
option.long_falsy == null;
}