diff --git a/clap.zig b/clap.zig index 5b69704..993a761 100644 --- a/clap.zig +++ b/clap.zig @@ -2132,14 +2132,16 @@ test "usage" { test { _ = args; + _ = ccw; _ = parsers; _ = streaming; - _ = ccw; + _ = v2; } pub const args = @import("clap/args.zig"); +pub const ccw = @import("clap/codepoint_counting_writer.zig"); pub const parsers = @import("clap/parsers.zig"); pub const streaming = @import("clap/streaming.zig"); -pub const ccw = @import("clap/codepoint_counting_writer.zig"); +pub const v2 = @import("clap/v2.zig"); const std = @import("std"); diff --git a/clap/v2.zig b/clap/v2.zig new file mode 100644 index 0000000..31967a0 --- /dev/null +++ b/clap/v2.zig @@ -0,0 +1,838 @@ +pub fn Params(comptime T: type) type { + const info = @typeInfo(T).@"struct"; + + var params: [info.fields.len + 2]std.builtin.Type.StructField = undefined; + const name_default_value: ?[]const u8 = null; + params[0] = .{ + .name = "name", + .type = ?[]const u8, + .alignment = @alignOf(?[]const u8), + .default_value = @ptrCast(&name_default_value), + .is_comptime = false, + }; + + const description_default_value: []const u8 = ""; + params[1] = .{ + .name = "description", + .type = []const u8, + .alignment = @alignOf([]const u8), + .default_value = @ptrCast(&description_default_value), + .is_comptime = false, + }; + + var used_shorts = std.StaticBitSet(std.math.maxInt(u8) + 1).initEmpty(); + used_shorts.set('h'); + used_shorts.set('v'); + + for (info.fields, params[2..]) |field, *param| { + const field_info = @typeInfo(field.type); + + const Next = ?*const fn (field.type) ParseError!field.type; + const default_next = switch (field_info) { + .bool => struct { + fn next(_: bool) !bool { + return true; + } + }.next, + .int => struct { + fn next(i: field.type) !field.type { + return i + 1; + } + }.next, + else => null, + }; + + const ValueParser = ?*const fn ([]const u8) ParseError!field.type; + const default_parser = switch (field_info) { + .int => struct { + fn parse(str: []const u8) ParseError!field.type { + return std.fmt.parseInt(field.type, str, 0) catch + return error.ParsingFailed; + } + }.parse, + .@"enum" => struct { + fn parse(str: []const u8) ParseError!field.type { + return std.meta.stringToEnum(field.type, str) orelse + return error.ParsingFailed; + } + }.parse, + else => null, + }; + + const Command = switch (field_info) { + .@"union" => |un| blk: { + var cmd_fields: [un.fields.len]std.builtin.Type.StructField = undefined; + for (un.fields, &cmd_fields) |un_field, *cmd_field| { + const CmdParam = Params(un_field.type); + const cmd_default_value = CmdParam{}; + cmd_field.* = .{ + .name = un_field.name, + .type = CmdParam, + .alignment = @alignOf(CmdParam), + .default_value = @ptrCast(&cmd_default_value), + .is_comptime = false, + }; + } + + break :blk @Type(.{ .@"struct" = .{ + .layout = .auto, + .fields = &cmd_fields, + .decls = &.{}, + .is_tuple = false, + } }); + }, + else => struct {}, + }; + + const default_short = if (used_shorts.isSet(field.name[0])) null else blk: { + used_shorts.set(field.name[0]); + break :blk field.name[0]; + }; + + const Param = struct { + short: ?u8 = default_short, + long: ?[]const u8 = field.name, + description: []const u8 = "", + next: Next = default_next, + parse: ValueParser = default_parser, + command: Command = .{}, + kind: enum { + flag, + option, + positional, + command, + } = switch (@typeInfo(field.type)) { + .@"union" => .command, + .bool => .flag, + else => .option, + }, + }; + + const default_value = Param{}; + param.* = .{ + .name = field.name, + .type = Param, + .alignment = @alignOf(Param), + .default_value = @ptrCast(&default_value), + .is_comptime = false, + }; + } + + return @Type(.{ .@"struct" = .{ + .layout = .auto, + .fields = ¶ms, + .decls = &.{}, + .is_tuple = false, + } }); +} + +fn validateParams(comptime T: type, params: Params(T)) void { + var command_count: usize = 0; + inline for (@typeInfo(T).@"struct".fields) |field| { + const param = @field(params, field.name); + switch (param.kind) { + .flag => { + if (param.next == null) + @panic("TODO: Good Error"); + if (field.default_value == null) + @panic("TODO: Good Error"); + }, + .option => { + if (param.parse == null) + @panic("TODO: Good Error"); + }, + .command => { + if (command_count != 0) + @panic("TODO: Good Error"); + command_count += 1; + }, + .positional => { + if (param.short != null) + @panic("TODO: Good Error"); + if (param.long != null) + @panic("TODO: Good Error"); + }, + } + } +} + +pub const HelpParam = struct { + short: ?u8 = 'h', + long: ?[]const u8 = "help", + command: ?[]const u8 = "help", +}; + +pub const VersionParam = struct { + string: []const u8 = "0.0.0", + short: ?u8 = 'v', + long: ?[]const u8 = "version", + command: ?[]const u8 = "version", +}; + +pub fn ParseIterOptions(comptime T: type) type { + return struct { + gpa: std.mem.Allocator, + params: Params(T) = .{}, + help: HelpParam = .{}, + version: VersionParam = .{}, + + /// The Writer used to write expected output like the help message when `-h` is passed. If + /// `null`, `std.io.getStdOut` will be used + stdout: ?std.io.AnyWriter = null, + + /// The Writer used to write errors. `std.io.getStdErr` will be used. If `null`, + /// `std.io.getStdOut` will be used + stderr: ?std.io.AnyWriter = null, + }; +} + +pub const ParseError = error{ + ParsingInterrupted, + ParsingFailed, +} || std.mem.Allocator.Error; + +pub fn parseIter(it: anytype, comptime T: type, opt: ParseIterOptions(T)) ParseError!T { + return Parser(@TypeOf(it), T).parse(it, opt); +} + +fn Parser(comptime Iter: type, comptime T: type) type { + return struct { + it: Iter, + opt: Options, + result: T, + has_been_set: HasBeenSet, + current_positional: usize, + + const Options = ParseIterOptions(T); + const Field = std.meta.FieldEnum(T); + const HasBeenSet = std.EnumSet(Field); + const fields = @typeInfo(T).@"struct".fields; + + fn init(it: Iter, opt: Options) @This() { + var res = @This(){ + .it = it, + .opt = opt, + .result = undefined, + .has_been_set = .{}, + .current_positional = 0, + }; + inline for (fields) |field| { + const opaque_default_value = field.default_value orelse continue; + const default_value: *const field.type = @alignCast(@ptrCast(opaque_default_value)); + @field(res.result, field.name) = default_value.*; + res.has_been_set.insert(@field(Field, field.name)); + } + + return res; + } + + fn parse(it: Iter, opt: Options) ParseError!T { + validateParams(T, opt.params); + + var parser = @This().init(it, opt); + while (parser.it.next()) |arg| { + if (std.mem.eql(u8, arg, "--")) + break; + + if (std.mem.startsWith(u8, arg, "--")) { + try parser.parseLong(arg[2..]); + } else if (std.mem.startsWith(u8, arg, "-")) { + try parser.parseShorts(arg[1..]); + } else if (try parser.parseCommand(arg)) { + return parser.result; + } else { + try parser.parsePositional(arg); + } + } + + while (parser.it.next()) |arg| + try parser.parsePositional(arg); + + return parser.result; + } + + fn parseLong(parser: *@This(), name: []const u8) ParseError!void { + if (parser.opt.help.long) |h| + if (std.mem.eql(u8, name, h)) + return parser.printHelp(); + if (parser.opt.version.long) |v| + if (std.mem.eql(u8, name, v)) + return parser.printVersion(); + + inline for (fields) |field| switch (@field(parser.opt.params, field.name).kind) { + .flag => case: { + const param = @field(parser.opt.params, field.name); + const long_name = param.long orelse break :case; + if (!std.mem.eql(u8, name, long_name)) + break :case; + + const next = param.next orelse unreachable; // See validateParams + const field_ptr = &@field(parser.result, field.name); + field_ptr.* = try next(field_ptr.*); + parser.has_been_set.insert(@field(Field, field.name)); + return; + }, + .option => case: { + const param = @field(parser.opt.params, field.name); + const long_name = param.long orelse break :case; + if (!std.mem.startsWith(u8, name, long_name)) + break :case; + + const value = if (name.len == long_name.len) blk: { + break :blk parser.it.next() orelse { + // TODO: Report proper error + return error.ParsingFailed; + }; + } else if (name[long_name.len] == '=') blk: { + break :blk name[long_name.len + 1 ..]; + } else { + break :case; + }; + + const parseValue = param.parse orelse unreachable; // See validateParams + @field(parser.result, field.name) = try parseValue(value); + parser.has_been_set.insert(@field(Field, field.name)); + return; + }, + .positional, .command => {}, + }; + + // TODO: Report proper error + return error.ParsingFailed; + } + + fn parseShorts(parser: *@This(), shorts: []const u8) ParseError!void { + var i: usize = 0; + while (i < shorts.len) + i = try parser.parseShort(shorts, i); + } + + fn parseShort(parser: *@This(), shorts: []const u8, pos: usize) ParseError!usize { + if (parser.opt.help.short) |h| + if (shorts[pos] == h) { + try parser.printHelp(); + return pos + 1; + }; + if (parser.opt.version.short) |v| + if (shorts[pos] == v) { + try parser.printVersion(); + return pos + 1; + }; + + inline for (fields) |field| switch (@field(parser.opt.params, field.name).kind) { + .flag => case: { + const param = @field(parser.opt.params, field.name); + const short_name = param.short orelse break :case; + if (shorts[pos] != short_name) + break :case; + + const next = param.next orelse unreachable; // See validateParams + const field_ptr = &@field(parser.result, field.name); + field_ptr.* = try next(field_ptr.*); + parser.has_been_set.insert(@field(Field, field.name)); + return pos + 1; + }, + .option => case: { + const param = @field(parser.opt.params, field.name); + const short_name = param.short orelse break :case; + if (shorts[pos] != short_name) + break :case; + + const value = if (pos + 1 == shorts.len) blk: { + break :blk parser.it.next() orelse { + // TODO: Report proper error + return error.ParsingFailed; + }; + } else shorts[pos + 1 + @intFromBool(shorts[pos + 1] == '=') ..]; + + const parseValue = param.parse orelse unreachable; // See validateParams + @field(parser.result, field.name) = try parseValue(value); + parser.has_been_set.insert(@field(Field, field.name)); + return shorts.len; + }, + .positional, .command => {}, + }; + + // TODO: Report proper error + return error.ParsingFailed; + } + + fn parseCommand(parser: *@This(), arg: []const u8) ParseError!bool { + if (parser.opt.help.command) |h| + if (std.mem.eql(u8, arg, h)) { + try parser.printHelp(); + return false; + }; + if (parser.opt.version.command) |v| + if (std.mem.eql(u8, arg, v)) { + try parser.printVersion(); + return false; + }; + + inline for (fields) |field| { + const union_field = switch (@typeInfo(field.type)) { + .@"union" => |u| u.fields, + else => continue, // TODO: It should be validated that commands are unions in validateParams + }; + + const param = @field(parser.opt.params, field.name); + switch (param.kind) { + .command => { + inline for (union_field) |cmd_field| cmd_field_loop: { + const cmd_params = @field(param.command, cmd_field.name); + if (!std.mem.eql(u8, arg, cmd_params.name orelse cmd_field.name)) + break :cmd_field_loop; + + const cmd_result = try Parser(Iter, cmd_field.type).parse(parser.it, .{ + .gpa = parser.opt.gpa, + .params = cmd_params, + .help = parser.opt.help, + }); + const cmd_union = @unionInit(field.type, cmd_field.name, cmd_result); + @field(parser.result, field.name) = cmd_union; + parser.has_been_set.insert(@field(Field, field.name)); + return true; + } + }, // TODO: + .flag, .option, .positional => {}, + } + } + + return false; + } + + fn parsePositional(parser: *@This(), arg: []const u8) ParseError!void { + _ = parser; // autofix + _ = arg; // autofix + } + + fn printHelp(parser: @This()) ParseError!void { + const stdout = std.io.getStdOut().writer(); + help(if (parser.opt.stdout) |o| o else stdout.any(), T, .{ + .params = parser.opt.params, + }) catch {}; + return error.ParsingInterrupted; + } + + fn printVersion(parser: @This()) ParseError!void { + const stdout = std.io.getStdOut().writer(); + const writer = if (parser.opt.stdout) |o| o else stdout.any(); + writer.writeAll(parser.opt.version.string) catch {}; + return error.ParsingInterrupted; + } + }; +} + +fn testParseIter(comptime T: type, opt: struct { + args: []const u8, + params: Params(T) = .{}, + + expected: anyerror!T, + expected_out: []const u8 = "", + expected_err: []const u8 = "", +}) !void { + const gpa = std.testing.allocator; + var it = try std.process.ArgIteratorGeneral(.{}).init(gpa, opt.args); + defer it.deinit(); + + var out = std.ArrayList(u8).init(gpa); + const out_writer = out.writer(); + defer out.deinit(); + + var err = std.ArrayList(u8).init(gpa); + const err_writer = err.writer(); + defer err.deinit(); + + const actual = parseIter(&it, T, .{ + .gpa = gpa, + .params = opt.params, + .stdout = out_writer.any(), + .stderr = err_writer.any(), + }); + try std.testing.expectEqualDeep(opt.expected, actual); + try std.testing.expectEqualDeep(opt.expected_out, out.items); + try std.testing.expectEqualDeep(opt.expected_err, err.items); +} + +test "parseIterParams" { + const S = struct { + a: bool = false, + b: u8 = 0, + c: enum { a, b, c, d } = .a, + }; + + try testParseIter(S, .{ + .args = "--a", + .expected = S{ .a = true }, + }); + try testParseIter(S, .{ + .args = "-a", + .expected = S{ .a = true }, + }); + + try testParseIter(S, .{ + .args = "--b", + .expected = S{ .b = 1 }, + .params = .{ .b = .{ .kind = .flag } }, + }); + try testParseIter(S, .{ + .args = "-b", + .expected = S{ .b = 1 }, + .params = .{ .b = .{ .kind = .flag } }, + }); + try testParseIter(S, .{ + .args = "-bb", + .expected = S{ .b = 2 }, + .params = .{ .b = .{ .kind = .flag } }, + }); + + try testParseIter(S, .{ + .args = "-aabb", + .expected = S{ .a = true, .b = 2 }, + .params = .{ .b = .{ .kind = .flag } }, + }); + + try testParseIter(S, .{ + .args = "--b 1", + .expected = S{ .b = 1 }, + }); + try testParseIter(S, .{ + .args = "--b=2", + .expected = S{ .b = 2 }, + }); + + try testParseIter(S, .{ + .args = "-b 1", + .expected = S{ .b = 1 }, + }); + try testParseIter(S, .{ + .args = "-b=2", + .expected = S{ .b = 2 }, + }); + try testParseIter(S, .{ + .args = "-b3", + .expected = S{ .b = 3 }, + }); + + try testParseIter(S, .{ + .args = "-aab4", + .expected = S{ .a = true, .b = 4 }, + .params = .{}, + }); + + try testParseIter(S, .{ + .args = "--c b", + .expected = S{ .c = .b }, + }); + try testParseIter(S, .{ + .args = "--c=c", + .expected = S{ .c = .c }, + }); + + try testParseIter(S, .{ + .args = "-c b", + .expected = S{ .c = .b }, + }); + try testParseIter(S, .{ + .args = "-c=c", + .expected = S{ .c = .c }, + }); + try testParseIter(S, .{ + .args = "-cd", + .expected = S{ .c = .d }, + }); + + try testParseIter(S, .{ + .args = "-bbcd", + .expected = S{ .b = 2, .c = .d }, + .params = .{ .b = .{ .kind = .flag } }, + }); +} + +test "parseIterCommand" { + const S = struct { + a: bool = false, + b: bool = false, + command: union(enum) { + sub1: struct { a: bool = false }, + sub2: struct { b: bool = false }, + }, + }; + + try testParseIter(S, .{ + .args = "sub1", + .expected = S{ .command = .{ .sub1 = .{} } }, + }); + try testParseIter(S, .{ + .args = "sub1 --a", + .expected = S{ .command = .{ .sub1 = .{ .a = true } } }, + }); + try testParseIter(S, .{ + .args = "--a --b sub1 --a", + .expected = S{ + .a = true, + .b = true, + .command = .{ .sub1 = .{ .a = true } }, + }, + }); + try testParseIter(S, .{ + .args = "sub2", + .expected = S{ .command = .{ .sub2 = .{} } }, + }); + try testParseIter(S, .{ + .args = "sub2 --b", + .expected = S{ .command = .{ .sub2 = .{ .b = true } } }, + }); + try testParseIter(S, .{ + .args = "--a --b sub2 --b", + .expected = S{ + .a = true, + .b = true, + .command = .{ .sub2 = .{ .b = true } }, + }, + }); + + try testParseIter(S, .{ + .args = "bob", + .params = .{ .command = .{ .command = .{ + .sub1 = .{ .name = "bob" }, + .sub2 = .{ .name = "kurt" }, + } } }, + .expected = S{ .command = .{ .sub1 = .{} } }, + }); + try testParseIter(S, .{ + .args = "kurt", + .params = .{ .command = .{ .command = .{ + .sub1 = .{ .name = "bob" }, + .sub2 = .{ .name = "kurt" }, + } } }, + .expected = S{ .command = .{ .sub2 = .{} } }, + }); +} + +test "parseIterHelp" { + const S = struct { + alice: bool = false, + bob: bool = false, + ben: bool = false, + command: union(enum) { + cmd1: struct {}, + cmd2: struct {}, + }, + }; + + const help_args = [_][]const u8{ "-h", "--help", "help" }; + for (help_args) |args| { + try testParseIter(S, .{ + .args = args, + .expected = error.ParsingInterrupted, + .expected_out = + \\Usage: test [OPTIONS] [COMMAND] + \\ + \\Commands: + \\ cmd1 + \\ cmd2 + \\ help Help + \\ version Version + \\ + \\Options: + \\ -a, --alice + \\ -b, --bob + \\ --ben + \\ -h, --help Help + \\ -v, --version Version + \\ + , + }); + try testParseIter(S, .{ + .args = args, + .params = .{ + .description = "This is a test", + .alice = .{ .description = "Who is this?" }, + .bob = .{ .description = "Bob the builder" }, + .ben = .{ .description = "One of the people of all time" }, + .command = .{ .command = .{ + .cmd1 = .{ .name = "command1", .description = "Command 1" }, + .cmd2 = .{ .name = "command2", .description = "Command 2" }, + } }, + }, + .expected = error.ParsingInterrupted, + .expected_out = + \\This is a test + \\ + \\Usage: test [OPTIONS] [COMMAND] + \\ + \\Commands: + \\ command1 Command 1 + \\ command2 Command 2 + \\ help Help + \\ version Version + \\ + \\Options: + \\ -a, --alice Who is this? + \\ -b, --bob Bob the builder + \\ --ben One of the people of all time + \\ -h, --help Help + \\ -v, --version Version + \\ + , + }); + } +} + +pub fn HelpOptions(comptime T: type) type { + return struct { + params: Params(T) = .{}, + help: HelpParam = .{}, + version: VersionParam = .{}, + }; +} + +pub fn help(writer: anytype, comptime T: type, opt: HelpOptions(T)) !void { + const fields = @typeInfo(T).@"struct".fields; + + var self_exe_path_buf: [std.fs.max_path_bytes]u8 = undefined; + const program_name = opt.params.name orelse blk: { + const self_exe_path = std.fs.selfExePath(&self_exe_path_buf) catch + break :blk "program"; + break :blk std.fs.path.basename(self_exe_path); + }; + + if (opt.params.description.len != 0) { + try writer.writeAll(opt.params.description); + try writer.writeAll("\n\n"); + } + + try writer.writeAll("Usage: "); + try writer.writeAll(program_name); + try writer.writeAll(" [OPTIONS] [COMMAND]"); + + var padding: usize = 0; + if (opt.help.command) |h| + padding = @max(padding, h.len); + if (opt.version.command) |v| + padding = @max(padding, v.len); + + inline for (fields) |field| switch (@field(opt.params, field.name).kind) { + .flag, .option, .positional => {}, + .command => { + const param = @field(opt.params, field.name); + inline for (@typeInfo(@TypeOf(param.command)).@"struct".fields) |cmd_field| { + const cmd_param = @field(param.command, cmd_field.name); + padding = @max(padding, (cmd_param.name orelse cmd_field.name).len); + } + }, + }; + + try writer.writeAll("\n\nCommands:\n"); + inline for (fields) |field| switch (@field(opt.params, field.name).kind) { + .flag, .option, .positional => {}, + .command => { + const param = @field(opt.params, field.name); + inline for (@typeInfo(@TypeOf(param.command)).@"struct".fields) |cmd_field| { + const cmd_param = @field(param.command, cmd_field.name); + try printCommand(writer, padding, .{ + .name = cmd_param.name orelse cmd_field.name, + .description = cmd_param.description, + }); + } + }, + }; + if (opt.help.command) |h| + try printCommand(writer, padding, .{ .name = h, .description = "Help" }); + if (opt.version.command) |v| + try printCommand(writer, padding, .{ .name = v, .description = "Version" }); + + padding = 0; + if (opt.help.long) |h| + padding = @max(padding, h.len); + if (opt.version.long) |v| + padding = @max(padding, v.len); + inline for (fields) |field| + padding = @max(padding, (@field(opt.params, field.name).long orelse "").len); + + try writer.writeAll("\nOptions:\n"); + inline for (fields) |field| { + const param = @field(opt.params, field.name); + switch (param.kind) { + .command, .positional => {}, + .flag, .option => try printParam(writer, padding, .{ + .short = param.short, + .long = param.long, + .description = param.description, + }), + } + } + try printParam(writer, padding, .{ + .short = opt.help.short, + .long = opt.help.long, + .description = "Help", + }); + try printParam(writer, padding, .{ + .short = opt.version.short, + .long = opt.version.long, + .description = "Version", + }); +} + +fn printCommand(writer: anytype, padding: usize, command: struct { + name: []const u8, + description: []const u8, +}) !void { + try writer.writeByteNTimes(' ', 4); + try writer.writeAll(command.name); + if (command.description.len != 0) { + try writer.writeByteNTimes(' ', padding - command.name.len); + try writer.writeAll(" "); + try writer.writeAll(command.description); + } + try writer.writeAll("\n"); +} + +fn printParam(writer: anytype, padding: usize, param: struct { + short: ?u8, + long: ?[]const u8, + description: []const u8, + value: ?[]const u8 = null, +}) !void { + if (param.short == null and param.long == null) + return; + + try writer.writeByteNTimes(' ', 4); + if (param.short) |s| { + try writer.writeByte('-'); + try writer.writeByte(s); + try writer.writeByte(if (param.long) |_| ',' else ' '); + } else { + try writer.writeAll(" "); + } + if (param.long) |l| { + try writer.writeAll(" --"); + try writer.writeAll(l); + if (param.description.len != 0) + try writer.writeByteNTimes(' ', (padding - l.len)); + } else { + if (param.description.len != 0) + try writer.writeByteNTimes(' ', padding + 2); + } + + if (param.description.len != 0) + try writer.writeAll(" "); + try writer.writeAll(param.description); + try writer.writeByte('\n'); +} + +fn testHelp(comptime T: type, opt: struct { + params: Params(T) = .{}, + expected: []const u8, +}) !void { + var buf: [std.mem.page_size]u8 = undefined; + var fbs = std.io.fixedBufferStream(&buf); + + try help(fbs.writer(), T, .{ + .params = opt.params, + }); + try std.testing.expectEqualStrings(opt.expected, fbs.getWritten()); +} + +const std = @import("std");