diff --git a/demo/demo.zig b/demo/demo.zig index 68f0017..237f22c 100644 --- a/demo/demo.zig +++ b/demo/demo.zig @@ -5,27 +5,47 @@ const context: []const u8 = "hello friend"; const ContextType = @TypeOf(context); const subcommand = blk: { - var cmd = noclip.Command(ContextType, .{ .name = "subcommand", .help = "this a sub command" }); + var cmd = noclip.Command(ContextType, .{ .name = "verb", .help = "this a sub command" }); cmd.add(cmd.defaultHelpFlag); cmd.add(cmd.StringOption{ .name = "meta", .short = "-m" }); - cmd.add(cmd.StringArgument{ .name = "sub" }); + cmd.add(cmd.StringArgument{ .name = "argument" }); + cmd.add(cmd.Argument(u32){ .name = "another", .default = 0 }); break :blk cmd; }; const command = blk: { - var cmd = noclip.Command(ContextType, .{ .name = "main", .help = "main CLI entry point" }); + var cmd = noclip.Command(ContextType, .{ + .name = "main", + .help = + \\This is the main CLI entry point for the noclip demo + \\ + \\This program demonstrates the major features of noclip both in terms of API + \\usage (in its source code) and argument parsing (in its execution). + , + }); + cmd.add(cmd.defaultHelpFlag); cmd.add(cmd.Flag{ .name = "flag", .truthy = .{ .short = "-f", .long = "--flag" }, .falsy = .{ .long = "--no-flag" } }); cmd.add(cmd.StringOption{ .name = "input", .short = "-i", .long = "--input", - .handler = printHandler, + .help = "some generic input", + .default = "in", .envVar = "OPTS_INPUT", }); - cmd.add(cmd.StringOption{ .name = "output", .long = "--output", .default = "waoh" }); - cmd.add(cmd.Option(i32){ .name = "number", .short = "-n", .long = "--number" }); - cmd.add(cmd.StringArgument{ .name = "argument" }); - cmd.add(cmd.Argument(u32){ .name = "another", .default = 0 }); + cmd.add(cmd.StringOption{ + .name = "output", + .long = "--output", + .default = "waoh", + .help = "name of the output", + }); + cmd.add(cmd.Option(i32){ + .name = "number", + .short = "-n", + .long = "--number", + .help = "a number", + .default = 0, + }); cmd.add(subcommand.Parser(subCallback)); break :blk cmd; @@ -40,34 +60,27 @@ pub fn subCallback(_: ContextType, result: subcommand.CommandResult()) !void { std.debug.print( \\subcommand: {{ \\ .meta = {s} - \\ .sub = {s} + \\ .argument = {s} + \\ .another = {d} \\}} \\ , - .{ result.meta, result.sub }, + .{ result.meta, result.argument, result.another }, ); } pub fn mainCommand(_: ContextType, result: command.CommandResult()) !void { + // std.debug.print("{any}", .{result}); std.debug.print( \\arguments: {{ \\ .flag = {any} \\ .input = {s} \\ .output = {s} \\ .number = {d} - \\ .argument = {s} - \\ .another = {d} \\}} \\ , - .{ - result.flag, - result.input, - result.output, - result.number, - result.argument, - result.another, - }, + .{ result.flag, result.input, result.output, result.number }, ); } @@ -79,7 +92,6 @@ pub fn main() !void { const allocator = arena.allocator(); var argit = try std.process.argsWithAllocator(allocator); - _ = argit.next(); try parser.execute(allocator, std.process.ArgIterator, &argit, context); } diff --git a/source/bakery.zig b/source/bakery.zig index e507c12..1553223 100644 --- a/source/bakery.zig +++ b/source/bakery.zig @@ -6,21 +6,20 @@ fn GenCommand(comptime UserContext: type, comptime cData: params.CommandData) ty return struct { argspec: meta.MutableTuple = .{}, - StringOption: type = params.Option(.{ .Output = []const u8, .UserContext = UserContext }), - StringArgument: type = params.Argument(.{ .Output = []const u8, .UserContext = UserContext }), + StringOption: type = params.StringOption(UserContext), + StringArgument: type = params.StringArg(UserContext), Flag: type = params.Flag(UserContext), - defaultHelpFlag: params.Flag(UserContext) = HelpFlag(.{}), + defaultHelpFlag: params.Flag(UserContext) = HelpFlag(undefined, .{}), - // have to provide the first argument in order for these functions to be - // accessible from an instance, which is kind of annoying. pub fn Option(comptime _: @This(), comptime Output: type) type { return params.Option(.{ .Output = Output, .UserContext = UserContext }); } + pub fn Argument(comptime _: @This(), comptime Output: type) type { return params.Argument(.{ .Output = Output, .UserContext = UserContext }); } - pub fn HelpFlag(comptime args: params.HelpFlagArgs) params.Flag(UserContext) { + pub fn HelpFlag(comptime _: @This(), comptime args: params.HelpFlagArgs) params.Flag(UserContext) { return params.HelpFlag(UserContext, args); } diff --git a/source/noclip.zig b/source/noclip.zig index 2d8ae6b..d62b0ea 100644 --- a/source/noclip.zig +++ b/source/noclip.zig @@ -34,12 +34,22 @@ pub fn CommandParser( comptime UserContext: type, comptime callback: *const fn (UserContext, CommandResult(spec, UserContext)) anyerror!void, ) type { - comptime var argCount = 0; - comptime for (spec) |param| { - switch (@TypeOf(param).brand) { - .Argument => argCount += 1, - .Option, .Flag, .Command => continue, + 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); @@ -63,6 +73,15 @@ pub fn CommandParser( try extractEnvVars(alloc, &result, &required, context); + // TODO: this does not even slightly work with subcommands + const progName = std.fs.path.basename(argit.next() orelse unreachable); + + // 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] == '-') { @@ -88,8 +107,6 @@ pub fn CommandParser( specloop: inline for (spec) |param| { switch (@TypeOf(param).brand) { .Option => { - // have to force lower the handler to runtime - // var handler = param.handler.?; if (param.long) |flag| { if (std.mem.eql(u8, flag, arg)) { if (comptime param.required()) { @@ -109,7 +126,7 @@ pub fn CommandParser( if (variant[0]) |flag| { if (std.mem.eql(u8, flag, arg)) { if (param.eager) |handler| { - try handler(context, data); + try handler(context, helpDescription); } if (param.hideResult == false) { @@ -132,7 +149,6 @@ pub fn CommandParser( specloop: inline for (spec) |param| { switch (@TypeOf(param).brand) { .Option => { - // var handler = param.handler.?; if (param.short) |flag| { if (flag[1] == shorty) { if (comptime param.required()) { @@ -156,7 +172,7 @@ pub fn CommandParser( if (variant[0]) |flag| { if (flag[1] == shorty) { if (param.eager) |handler| { - try handler(context, data); + try handler(context, helpDescription); } if (param.hideResult == false) { @@ -193,7 +209,6 @@ pub fn CommandParser( if (comptime param.required()) { @field(required, param.name) = true; } - // var handler = param.handler; @field(result, param.name) = try param.handler.?(context, arg); continue :argloop; } @@ -208,14 +223,260 @@ pub fn CommandParser( 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 (param_count.args > 0) { + try writer.writeAll("\n\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 < argCount) { + if (seenArgs < param_count.args) { return OptionError.MissingArgument; - } else if (seenArgs > argCount) { + } else if (seenArgs > param_count.args) { return OptionError.ExtraArguments; } @@ -291,7 +552,7 @@ pub fn CommandParser( .Option => { if (param.envVar) |want| { if (env.get(want)) |value| { - if (param.required()) { + if (comptime param.required()) { @field(required, param.name) = true; } diff --git a/source/params.zig b/source/params.zig index f130085..4eb45c3 100644 --- a/source/params.zig +++ b/source/params.zig @@ -21,6 +21,7 @@ pub const ArgCount = union(enum) { pub const ParameterArgs = struct { Output: type, UserContext: type, + nice_type: ?[]const u8 = null, }; pub fn Option(comptime args: ParameterArgs) type { @@ -67,13 +68,18 @@ pub fn Option(comptime args: ParameterArgs) type { pub fn required(self: @This()) bool { return !@TypeOf(self).mayBeOptional and self.default == null; } + + pub fn type_name(self: @This()) []const u8 { + if (args.nice_type) |name| return name; + return @typeName(@TypeOf(self).ResultType); + } }; return result; } pub fn StringOption(comptime UserContext: type) type { - return Option(.{ .Output = []const u8, .UserContext = UserContext }); + return Option(.{ .Output = []const u8, .UserContext = UserContext, .nice_type = "string" }); } // this could be Option(bool) except it allows truthy/falsy flag variants @@ -105,8 +111,12 @@ pub fn Flag(comptime UserContext: type) type { eager: ?*const fn (UserContext, CommandData) anyerror!void = null, pub fn required(self: @This()) bool { - if (self.default) return true; - return false; + if (self.default) |_| return false; + return true; + } + + pub fn type_name(_: @This()) []const u8 { + return "bool"; } }; } @@ -131,7 +141,7 @@ pub const HelpFlagArgs = struct { name: []const u8 = "help", short: ?*const [2]u8 = "-h", long: ?[]const u8 = "--help", - help: []const u8 = "print this help message", + help: []const u8 = "print this help message and exit", }; pub fn HelpFlag(comptime UserContext: type, comptime args: HelpFlagArgs) Flag(UserContext) { @@ -172,11 +182,16 @@ pub fn Argument(comptime args: ParameterArgs) type { pub fn required(self: @This()) bool { return !@TypeOf(self).mayBeOptional and self.default == null; } + + pub fn type_name(self: @This()) []const u8 { + if (args.nice_type) |name| return name; + return @typeName(@TypeOf(self).ResultType); + } }; } pub fn StringArg(comptime UserContext: type) type { - return Argument(.{ .Output = []const u8, .UserContext = UserContext }); + return Argument(.{ .Output = []const u8, .UserContext = UserContext, .nice_type = "string" }); } pub const CommandData = struct {