diff --git a/demo/demo.zig b/demo/demo.zig index 07f0aff..012710b 100644 --- a/demo/demo.zig +++ b/demo/demo.zig @@ -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> 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) diff --git a/source/help.zig b/source/help.zig index 3ed22f7..714644c 100644 --- a/source/help.zig +++ b/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) " " else "", + if (subcommands.count() > 0) " [ ...]" 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, + }; + } } diff --git a/source/meta.zig b/source/meta.zig index d3a8418..2a0003f 100644 --- a/source/meta.zig +++ b/source/meta.zig @@ -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); diff --git a/source/parameters.zig b/source/parameters.zig index 96415c6..09d9f14 100644 --- a/source/parameters.zig +++ b/source/parameters.zig @@ -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) { diff --git a/source/parser.zig b/source/parser.zig index 1f550db..4e480ef 100644 --- a/source/parser.zig +++ b/source/parser.zig @@ -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; } }; }