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) " " 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; }