From 3187bd48419bd1e4fcad525a6a0aeddba2a0a695 Mon Sep 17 00:00:00 2001 From: Komari Spaghetti Date: Wed, 17 Jun 2020 15:10:42 +0200 Subject: Create FUNDING.yml --- .github/FUNDING.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..2b7106c --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: [Hejsil] -- cgit v1.2.3 From ddca24a6fd9a7d23d168b2195932a32b92a18b09 Mon Sep 17 00:00:00 2001 From: Jimmi Holst Christensen Date: Sat, 15 Aug 2020 15:23:46 +0200 Subject: Fix expected type error on 32 bit systems fixes #23 --- README.md | 12 ++++++------ build.zig | 3 +++ clap.zig | 4 ++-- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 2b38281..06d29c5 100644 --- a/README.md +++ b/README.md @@ -46,9 +46,9 @@ pub fn main() !void { if (args.flag("--help")) debug.warn("--help\n", .{}); if (args.option("--number")) |n| - debug.warn("--number = {}\n", .{ n }); + debug.warn("--number = {}\n", .{n}); for (args.positionals()) |pos| - debug.warn("{}\n", .{ pos }); + debug.warn("{}\n", .{pos}); } ``` @@ -125,9 +125,9 @@ pub fn main() !void { if (args.flag("--help")) debug.warn("--help\n", .{}); if (args.option("--number")) |n| - debug.warn("--number = {}\n", .{ n }); + debug.warn("--number = {}\n", .{n}); for (args.positionals()) |pos| - debug.warn("{}\n", .{ pos }); + debug.warn("{}\n", .{pos}); } ``` @@ -179,12 +179,12 @@ pub fn main() !void { // arg.param will point to the parameter which matched the argument. switch (arg.param.id) { 'h' => debug.warn("Help!\n", .{}), - 'n' => debug.warn("--number = {}\n", .{ arg.value.? }), + 'n' => debug.warn("--number = {}\n", .{arg.value.?}), // arg.value == null, if arg.param.takes_value == false. // Otherwise, arg.value is the value passed with the argument, such as "-a=10" // or "-a 10". - 'f' => debug.warn("{}\n", .{ arg.value.? }), + 'f' => debug.warn("{}\n", .{arg.value.?}), else => unreachable, } } diff --git a/build.zig b/build.zig index ba92f3d..466f5d0 100644 --- a/build.zig +++ b/build.zig @@ -6,6 +6,7 @@ const Builder = std.build.Builder; pub fn build(b: *Builder) void { const mode = b.standardReleaseOptions(); + const target = b.standardTargetOptions(.{}); const fmt_step = b.addFmt(&[_][]const u8{ "build.zig", @@ -20,6 +21,7 @@ pub fn build(b: *Builder) void { const tests = b.addTest("clap.zig"); tests.setBuildMode(test_mode); + tests.setTarget(target); tests.setNamePrefix(mode_str ++ " "); const test_step = b.step("test-" ++ mode_str, "Run all tests in " ++ mode_str ++ "."); @@ -39,6 +41,7 @@ pub fn build(b: *Builder) void { const example = b.addExecutable(example_name, "example/" ++ example_name ++ ".zig"); example.addPackagePath("clap", "clap.zig"); example.setBuildMode(mode); + example.setTarget(target); example.install(); example_step.dependOn(&example.step); } diff --git a/clap.zig b/clap.zig index ff423cc..1711af9 100644 --- a/clap.zig +++ b/clap.zig @@ -290,7 +290,7 @@ pub fn helpFull( var counting_stream = io.countingOutStream(io.null_out_stream); try printParam(counting_stream.outStream(), Id, param, Error, context, valueText); if (res < counting_stream.bytes_written) - res = counting_stream.bytes_written; + res = @intCast(usize, counting_stream.bytes_written); } break :blk res; @@ -303,7 +303,7 @@ pub fn helpFull( var counting_stream = io.countingOutStream(stream); try stream.print("\t", .{}); try printParam(counting_stream.outStream(), Id, param, Error, context, valueText); - try stream.writeByteNTimes(' ', max_spacing - counting_stream.bytes_written); + try stream.writeByteNTimes(' ', max_spacing - @intCast(usize, counting_stream.bytes_written)); try stream.print("\t{}\n", .{try helpText(context, param)}); } } -- cgit v1.2.3 From 1180b1c2b52ae4f3f9aaf7e8308b3a46ee828eed Mon Sep 17 00:00:00 2001 From: Asherah Connor Date: Sun, 23 Aug 2020 12:21:26 +1000 Subject: parse multiple options --- clap.zig | 4 ++++ clap/comptime.zig | 31 ++++++++++++++++++++++--------- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/clap.zig b/clap.zig index 1711af9..588fa47 100644 --- a/clap.zig +++ b/clap.zig @@ -247,6 +247,10 @@ pub fn Args(comptime Id: type, comptime params: []const Param(Id)) type { return a.clap.option(name); } + pub fn allOptions(a: @This(), comptime name: []const u8) [][]const u8 { + return a.clap.allOptions(name); + } + pub fn positionals(a: @This()) []const []const u8 { return a.clap.positionals(); } diff --git a/clap/comptime.zig b/clap/comptime.zig index c25659b..90d34e9 100644 --- a/clap/comptime.zig +++ b/clap/comptime.zig @@ -27,7 +27,7 @@ pub fn ComptimeClap(comptime Id: type, comptime params: []const clap.Param(Id)) } return struct { - options: [options]?[]const u8, + options: [options]std.ArrayList([]const u8), flags: [flags]bool, pos: []const []const u8, allocator: *mem.Allocator, @@ -35,11 +35,14 @@ pub fn ComptimeClap(comptime Id: type, comptime params: []const clap.Param(Id)) pub fn parse(allocator: *mem.Allocator, comptime ArgIter: type, iter: *ArgIter) !@This() { var pos = std.ArrayList([]const u8).init(allocator); var res = @This(){ - .options = [_]?[]const u8{null} ** options, + .options = [_]std.ArrayList([]const u8){undefined} ** options, .flags = [_]bool{false} ** flags, .pos = undefined, .allocator = allocator, }; + for (res.options) |*init_opt| { + init_opt.* = std.ArrayList([]const u8).init(allocator); + } var stream = clap.StreamingClap(usize, ArgIter){ .params = converted_params, @@ -56,7 +59,7 @@ pub fn ComptimeClap(comptime Id: type, comptime params: []const clap.Param(Id)) // Hack: Utilize Zigs lazy analyzis to avoid a compiler error if (res.options.len != 0) - res.options[param.id] = arg.value.?; + try res.options[param.id].append(arg.value.?); } else { debug.assert(res.flags.len != 0); if (res.flags.len != 0) @@ -69,6 +72,8 @@ pub fn ComptimeClap(comptime Id: type, comptime params: []const clap.Param(Id)) } pub fn deinit(parser: *@This()) void { + for (parser.options) |o| + o.deinit(); parser.allocator.free(parser.pos); parser.* = undefined; } @@ -81,12 +86,17 @@ pub fn ComptimeClap(comptime Id: type, comptime params: []const clap.Param(Id)) return parser.flags[param.id]; } - pub fn option(parser: @This(), comptime name: []const u8) ?[]const u8 { + pub fn allOptions(parser: @This(), comptime name: []const u8) [][]const u8 { const param = comptime findParam(name); if (!param.takes_value) @compileError(name ++ " is a flag and not an option."); - return parser.options[param.id]; + return parser.options[param.id].items; + } + + pub fn option(parser: @This(), comptime name: []const u8) ?[]const u8 { + const items = parser.allOptions(name); + return if (items.len > 0) items[0] else null; } pub fn positionals(parser: @This()) []const []const u8 { @@ -117,6 +127,7 @@ test "clap.comptime.ComptimeClap" { clap.parseParam("-a, --aa ") catch unreachable, clap.parseParam("-b, --bb ") catch unreachable, clap.parseParam("-c, --cc ") catch unreachable, + clap.parseParam("-d, --dd ") catch unreachable, clap.Param(clap.Help){ .takes_value = true, }, @@ -126,7 +137,7 @@ test "clap.comptime.ComptimeClap" { var fb_allocator = heap.FixedBufferAllocator.init(buf[0..]); var iter = clap.args.SliceIterator{ .args = &[_][]const u8{ - "-a", "-c", "0", "something", + "-a", "-c", "0", "something", "-d", "a", "--dd", "b", }, }; var args = try Clap.parse(&fb_allocator.allocator, clap.args.SliceIterator, &iter); @@ -136,8 +147,10 @@ test "clap.comptime.ComptimeClap" { testing.expect(args.flag("--aa")); testing.expect(!args.flag("-b")); testing.expect(!args.flag("--bb")); - testing.expectEqualSlices(u8, "0", args.option("-c").?); - testing.expectEqualSlices(u8, "0", args.option("--cc").?); + testing.expectEqualStrings("0", args.option("-c").?); + testing.expectEqualStrings("0", args.option("--cc").?); testing.expectEqual(@as(usize, 1), args.positionals().len); - testing.expectEqualSlices(u8, "something", args.positionals()[0]); + testing.expectEqualStrings("something", args.positionals()[0]); + testing.expectEqualStrings("a", args.option("-d").?); + testing.expectEqualSlices([]const u8, &[_][]const u8{ "a", "b" }, args.allOptions("--dd")); } -- cgit v1.2.3 From 140ace899ae4ecfafc9b635609b656050c9c355d Mon Sep 17 00:00:00 2001 From: Asherah Connor Date: Sun, 23 Aug 2020 13:48:12 +1000 Subject: parse and validate multiple option --- clap.zig | 106 ++++++++++++++++++++++++++++++++++++----------------- clap/comptime.zig | 23 ++++++++---- clap/streaming.zig | 34 ++++++++++++----- 3 files changed, 111 insertions(+), 52 deletions(-) diff --git a/clap.zig b/clap.zig index 588fa47..1a3659b 100644 --- a/clap.zig +++ b/clap.zig @@ -25,6 +25,12 @@ pub const Names = struct { long: ?[]const u8 = null, }; +pub const Values = enum { + None, + One, + Many, +}; + /// Represents a parameter for the command line. /// Parameters come in three kinds: /// * Short ("-a"): Should be used for the most commonly used parameters in your program. @@ -50,7 +56,7 @@ pub fn Param(comptime Id: type) type { return struct { id: Id = Id{}, names: Names = Names{}, - takes_value: bool = false, + takes_value: Values = .None, }; } @@ -86,8 +92,13 @@ pub fn parseParam(line: []const u8) !Param(Help) { const start = mem.indexOfScalar(u8, help_msg, '<').? + 1; const len = mem.indexOfScalar(u8, help_msg[start..], '>') orelse break :blk; res.id.value = help_msg[start..][0..len]; - res.takes_value = true; - help_msg = help_msg[start + len + 1 ..]; + if (mem.startsWith(u8, help_msg[start + len + 1 ..], "...")) { + res.takes_value = .Many; + help_msg = help_msg[start + len + 1 + 3 ..]; + } else { + res.takes_value = .One; + help_msg = help_msg[start + len + 1 ..]; + } } } @@ -110,8 +121,13 @@ pub fn parseParam(line: []const u8) !Param(Help) { const start = mem.indexOfScalar(u8, help_msg, '<').? + 1; const len = mem.indexOfScalar(u8, help_msg[start..], '>') orelse break :blk; res.id.value = help_msg[start..][0..len]; - res.takes_value = true; - help_msg = help_msg[start + len + 1 ..]; + if (mem.startsWith(u8, help_msg[start + len + 1 ..], "...")) { + res.takes_value = .Many; + help_msg = help_msg[start + len + 1 + 3 ..]; + } else { + res.takes_value = .One; + help_msg = help_msg[start + len + 1 ..]; + } } } @@ -134,7 +150,20 @@ test "parseParam" { .short = 's', .long = find(text, "long"), }, - .takes_value = true, + .takes_value = .One, + }, try parseParam(text)); + + text = "-s, --long ... Help text"; + testing.expectEqual(Param(Help){ + .id = Help{ + .msg = find(text, "Help text"), + .value = find(text, "value"), + }, + .names = Names{ + .short = 's', + .long = find(text, "long"), + }, + .takes_value = .Many, }, try parseParam(text)); text = "--long Help text"; @@ -147,7 +176,7 @@ test "parseParam" { .short = null, .long = find(text, "long"), }, - .takes_value = true, + .takes_value = .One, }, try parseParam(text)); text = "-s Help text"; @@ -160,7 +189,7 @@ test "parseParam" { .short = 's', .long = null, }, - .takes_value = true, + .takes_value = .One, }, try parseParam(text)); text = "-s, --long Help text"; @@ -173,7 +202,7 @@ test "parseParam" { .short = 's', .long = find(text, "long"), }, - .takes_value = false, + .takes_value = .None, }, try parseParam(text)); text = "-s Help text"; @@ -186,7 +215,7 @@ test "parseParam" { .short = 's', .long = null, }, - .takes_value = false, + .takes_value = .None, }, try parseParam(text)); text = "--long Help text"; @@ -199,7 +228,7 @@ test "parseParam" { .short = null, .long = find(text, "long"), }, - .takes_value = false, + .takes_value = .None, }, try parseParam(text)); text = "--long Help text"; @@ -212,7 +241,7 @@ test "parseParam" { .short = null, .long = find(text, "long"), }, - .takes_value = true, + .takes_value = .One, }, try parseParam(text)); testing.expectError(error.NoParamFound, parseParam("Help")); @@ -334,8 +363,11 @@ fn printParam( try stream.print("--{}", .{l}); } - if (param.takes_value) - try stream.print(" <{}>", .{valueText(context, param)}); + switch (param.takes_value) { + .None => {}, + .One => try stream.print(" <{}>", .{valueText(context, param)}), + .Many => try stream.print(" <{}>...", .{valueText(context, param)}), + } } /// A wrapper around helpFull for simple helpText and valueText functions that @@ -400,28 +432,30 @@ test "clap.help" { try help( slice_stream.outStream(), comptime &[_]Param(Help){ - parseParam("-a Short flag. ") catch unreachable, - parseParam("-b Short option.") catch unreachable, - parseParam("--aa Long flag. ") catch unreachable, - parseParam("--bb Long option. ") catch unreachable, - parseParam("-c, --cc Both flag. ") catch unreachable, - parseParam("-d, --dd Both option. ") catch unreachable, + parseParam("-a Short flag. ") catch unreachable, + parseParam("-b Short option.") catch unreachable, + parseParam("--aa Long flag. ") catch unreachable, + parseParam("--bb Long option. ") catch unreachable, + parseParam("-c, --cc Both flag. ") catch unreachable, + parseParam("-d, --dd Both option. ") catch unreachable, + parseParam("-d, --dd ... Both repeated option. ") catch unreachable, Param(Help){ .id = Help{ .msg = "Positional. This should not appear in the help message.", }, - .takes_value = true, + .takes_value = .One, }, }, ); const expected = "" ++ - "\t-a \tShort flag.\n" ++ - "\t-b \tShort option.\n" ++ - "\t --aa \tLong flag.\n" ++ - "\t --bb \tLong option.\n" ++ - "\t-c, --cc \tBoth flag.\n" ++ - "\t-d, --dd \tBoth option.\n"; + "\t-a \tShort flag.\n" ++ + "\t-b \tShort option.\n" ++ + "\t --aa \tLong flag.\n" ++ + "\t --bb \tLong option.\n" ++ + "\t-c, --cc \tBoth flag.\n" ++ + "\t-d, --dd \tBoth option.\n" ++ + "\t-d, --dd ...\tBoth repeated option.\n"; const actual = slice_stream.getWritten(); if (!mem.eql(u8, actual, expected)) { @@ -458,7 +492,7 @@ pub fn usageFull( const cs = cos.outStream(); for (params) |param| { const name = param.names.short orelse continue; - if (param.takes_value) + if (param.takes_value != .None) continue; if (cos.bytes_written == 0) @@ -470,7 +504,7 @@ pub fn usageFull( var positional: ?Param(Id) = null; for (params) |param| { - if (!param.takes_value and param.names.short != null) + if (param.takes_value == .None and param.names.short != null) continue; const prefix = if (param.names.short) |_| "-" else "--"; @@ -485,8 +519,11 @@ pub fn usageFull( try cs.writeByte(' '); try cs.print("[{}{}", .{ prefix, name }); - if (param.takes_value) - try cs.print(" <{}>", .{try valueText(context, param)}); + switch (param.takes_value) { + .None => {}, + .One => try cs.print(" <{}>", .{try valueText(context, param)}), + .Many => try cs.print(" <{}>...", .{try valueText(context, param)}), + } try cs.writeByte(']'); } @@ -575,10 +612,10 @@ test "usage" { .id = Help{ .value = "file", }, - .takes_value = true, + .takes_value = .One, }, }); - try testUsage("[-ab] [-c ] [-d ] [--e] [--f] [--g ] [--h ] ", comptime &[_]Param(Help){ + try testUsage("[-ab] [-c ] [-d ] [--e] [--f] [--g ] [--h ] [-i ...] ", comptime &[_]Param(Help){ parseParam("-a") catch unreachable, parseParam("-b") catch unreachable, parseParam("-c ") catch unreachable, @@ -587,11 +624,12 @@ test "usage" { parseParam("--f") catch unreachable, parseParam("--g ") catch unreachable, parseParam("--h ") catch unreachable, + parseParam("-i ...") catch unreachable, Param(Help){ .id = Help{ .value = "file", }, - .takes_value = true, + .takes_value = .One, }, }); } diff --git a/clap/comptime.zig b/clap/comptime.zig index 90d34e9..cecfcb2 100644 --- a/clap/comptime.zig +++ b/clap/comptime.zig @@ -13,7 +13,7 @@ pub fn ComptimeClap(comptime Id: type, comptime params: []const clap.Param(Id)) for (params) |param| { var index: usize = 0; if (param.names.long != null or param.names.short != null) { - const ptr = if (param.takes_value) &options else &flags; + const ptr = if (param.takes_value != .None) &options else &flags; index = ptr.*; ptr.* += 1; } @@ -52,7 +52,7 @@ pub fn ComptimeClap(comptime Id: type, comptime params: []const clap.Param(Id)) const param = arg.param; if (param.names.long == null and param.names.short == null) { try pos.append(arg.value.?); - } else if (param.takes_value) { + } else if (param.takes_value != .None) { // If we don't have any optional parameters, then this code should // never be reached. debug.assert(res.options.len != 0); @@ -80,7 +80,7 @@ pub fn ComptimeClap(comptime Id: type, comptime params: []const clap.Param(Id)) pub fn flag(parser: @This(), comptime name: []const u8) bool { const param = comptime findParam(name); - if (param.takes_value) + if (param.takes_value != .None) @compileError(name ++ " is an option and not a flag."); return parser.flags[param.id]; @@ -88,14 +88,21 @@ pub fn ComptimeClap(comptime Id: type, comptime params: []const clap.Param(Id)) pub fn allOptions(parser: @This(), comptime name: []const u8) [][]const u8 { const param = comptime findParam(name); - if (!param.takes_value) + if (param.takes_value == .None) @compileError(name ++ " is a flag and not an option."); + if (param.takes_value == .One) + @compileError(name ++ " takes one option, not multiple."); return parser.options[param.id].items; } pub fn option(parser: @This(), comptime name: []const u8) ?[]const u8 { - const items = parser.allOptions(name); + const param = comptime findParam(name); + if (param.takes_value == .None) + @compileError(name ++ " is a flag and not an option."); + if (param.takes_value == .Many) + @compileError(name ++ " takes many options, not one."); + const items = parser.options[param.id].items; return if (items.len > 0) items[0] else null; } @@ -127,9 +134,9 @@ test "clap.comptime.ComptimeClap" { clap.parseParam("-a, --aa ") catch unreachable, clap.parseParam("-b, --bb ") catch unreachable, clap.parseParam("-c, --cc ") catch unreachable, - clap.parseParam("-d, --dd ") catch unreachable, + clap.parseParam("-d, --dd ...") catch unreachable, clap.Param(clap.Help){ - .takes_value = true, + .takes_value = .One, }, }); @@ -151,6 +158,6 @@ test "clap.comptime.ComptimeClap" { testing.expectEqualStrings("0", args.option("--cc").?); testing.expectEqual(@as(usize, 1), args.positionals().len); testing.expectEqualStrings("something", args.positionals()[0]); - testing.expectEqualStrings("a", args.option("-d").?); + testing.expectEqualSlices([]const u8, &[_][]const u8{ "a", "b" }, args.allOptions("-d")); testing.expectEqualSlices([]const u8, &[_][]const u8{ "a", "b" }, args.allOptions("--dd")); } diff --git a/clap/streaming.zig b/clap/streaming.zig index 95ee581..b843bff 100644 --- a/clap/streaming.zig +++ b/clap/streaming.zig @@ -75,7 +75,7 @@ pub fn StreamingClap(comptime Id: type, comptime ArgIterator: type) type { if (!mem.eql(u8, name, match)) continue; - if (!param.takes_value) { + if (param.takes_value == .None) { if (maybe_value != null) return error.DoesntTakeValue; @@ -128,7 +128,7 @@ pub fn StreamingClap(comptime Id: type, comptime ArgIterator: type) type { // Before we return, we have to set the new state of the clap defer { - if (arg.len <= next_index or param.takes_value) { + if (arg.len <= next_index or param.takes_value != .None) { parser.state = State.Normal; } else { parser.state = State{ @@ -140,7 +140,7 @@ pub fn StreamingClap(comptime Id: type, comptime ArgIterator: type) type { } } - if (!param.takes_value) + if (param.takes_value == .None) return Arg(Id){ .param = param }; if (arg.len <= next_index) { @@ -194,20 +194,26 @@ test "clap.streaming.StreamingClap: short params" { clap.Param(u8){ .id = 2, .names = clap.Names{ .short = 'c' }, - .takes_value = true, + .takes_value = .One, + }, + clap.Param(u8){ + .id = 3, + .names = clap.Names{ .short = 'd' }, + .takes_value = .Many, }, }; const a = ¶ms[0]; const b = ¶ms[1]; const c = ¶ms[2]; + const d = ¶ms[3]; testNoErr( ¶ms, &[_][]const u8{ "-a", "-b", "-ab", "-ba", "-c", "0", "-c=0", "-ac", - "0", "-ac=0", + "0", "-ac=0", "-d=0", }, &[_]Arg(u8){ Arg(u8){ .param = a }, @@ -222,6 +228,7 @@ test "clap.streaming.StreamingClap: short params" { Arg(u8){ .param = c, .value = "0" }, Arg(u8){ .param = a }, Arg(u8){ .param = c, .value = "0" }, + Arg(u8){ .param = d, .value = "0" }, }, ); } @@ -239,26 +246,33 @@ test "clap.streaming.StreamingClap: long params" { clap.Param(u8){ .id = 2, .names = clap.Names{ .long = "cc" }, - .takes_value = true, + .takes_value = .One, + }, + clap.Param(u8){ + .id = 3, + .names = clap.Names{ .long = "dd" }, + .takes_value = .Many, }, }; const aa = ¶ms[0]; const bb = ¶ms[1]; const cc = ¶ms[2]; + const dd = ¶ms[3]; testNoErr( ¶ms, &[_][]const u8{ "--aa", "--bb", "--cc", "0", - "--cc=0", + "--cc=0", "--dd=0", }, &[_]Arg(u8){ Arg(u8){ .param = aa }, Arg(u8){ .param = bb }, Arg(u8){ .param = cc, .value = "0" }, Arg(u8){ .param = cc, .value = "0" }, + Arg(u8){ .param = dd, .value = "0" }, }, ); } @@ -266,7 +280,7 @@ test "clap.streaming.StreamingClap: long params" { test "clap.streaming.StreamingClap: positional params" { const params = [_]clap.Param(u8){clap.Param(u8){ .id = 0, - .takes_value = true, + .takes_value = .One, }}; testNoErr( @@ -301,11 +315,11 @@ test "clap.streaming.StreamingClap: all params" { .short = 'c', .long = "cc", }, - .takes_value = true, + .takes_value = .One, }, clap.Param(u8){ .id = 3, - .takes_value = true, + .takes_value = .One, }, }; -- cgit v1.2.3 From a99dff8a3b1b3ef786fe6da8046ccde1a9500f03 Mon Sep 17 00:00:00 2001 From: Asherah Connor Date: Fri, 28 Aug 2020 10:47:14 +1000 Subject: separate options into single and multiple This avoids allocations if you never use multiple arguments. --- clap/comptime.zig | 60 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 35 insertions(+), 25 deletions(-) diff --git a/clap/comptime.zig b/clap/comptime.zig index cecfcb2..edbaafb 100644 --- a/clap/comptime.zig +++ b/clap/comptime.zig @@ -8,12 +8,17 @@ const debug = std.debug; pub fn ComptimeClap(comptime Id: type, comptime params: []const clap.Param(Id)) type { var flags: usize = 0; - var options: usize = 0; + var single_options: usize = 0; + var multi_options: usize = 0; var converted_params: []const clap.Param(usize) = &[_]clap.Param(usize){}; for (params) |param| { var index: usize = 0; if (param.names.long != null or param.names.short != null) { - const ptr = if (param.takes_value != .None) &options else &flags; + const ptr = switch (param.takes_value) { + .None => &flags, + .One => &single_options, + .Many => &multi_options, + }; index = ptr.*; ptr.* += 1; } @@ -27,22 +32,27 @@ pub fn ComptimeClap(comptime Id: type, comptime params: []const clap.Param(Id)) } return struct { - options: [options]std.ArrayList([]const u8), + single_options: [single_options]?[]const u8, + multi_options: [multi_options][]const []const u8, flags: [flags]bool, pos: []const []const u8, allocator: *mem.Allocator, pub fn parse(allocator: *mem.Allocator, comptime ArgIter: type, iter: *ArgIter) !@This() { + var multis = [_]std.ArrayList([]const u8){undefined} ** single_options; + for (multis) |*multi| { + multi.* = std.ArrayList([]const u8).init(allocator); + } + var pos = std.ArrayList([]const u8).init(allocator); + var res = @This(){ - .options = [_]std.ArrayList([]const u8){undefined} ** options, + .single_options = [_]?[]const u8{null} ** single_options, + .multi_options = [_][]const []const u8{undefined} ** multi_options, .flags = [_]bool{false} ** flags, .pos = undefined, .allocator = allocator, }; - for (res.options) |*init_opt| { - init_opt.* = std.ArrayList([]const u8).init(allocator); - } var stream = clap.StreamingClap(usize, ArgIter){ .params = converted_params, @@ -52,28 +62,29 @@ pub fn ComptimeClap(comptime Id: type, comptime params: []const clap.Param(Id)) const param = arg.param; if (param.names.long == null and param.names.short == null) { try pos.append(arg.value.?); - } else if (param.takes_value != .None) { - // If we don't have any optional parameters, then this code should - // never be reached. - debug.assert(res.options.len != 0); - - // Hack: Utilize Zigs lazy analyzis to avoid a compiler error - if (res.options.len != 0) - try res.options[param.id].append(arg.value.?); + } else if (param.takes_value == .One) { + debug.assert(res.single_options.len != 0); + res.single_options[param.id] = arg.value.?; + } else if (param.takes_value == .Many) { + debug.assert(res.multi_options.len != 0); + try multis[param.id].append(arg.value.?); } else { debug.assert(res.flags.len != 0); - if (res.flags.len != 0) - res.flags[param.id] = true; + res.flags[param.id] = true; } } + for (multis) |*multi, i| { + res.multi_options[i] = multi.toOwnedSlice(); + } res.pos = pos.toOwnedSlice(); + return res; } pub fn deinit(parser: *@This()) void { - for (parser.options) |o| - o.deinit(); + for (parser.multi_options) |o| + parser.allocator.free(o); parser.allocator.free(parser.pos); parser.* = undefined; } @@ -86,14 +97,14 @@ pub fn ComptimeClap(comptime Id: type, comptime params: []const clap.Param(Id)) return parser.flags[param.id]; } - pub fn allOptions(parser: @This(), comptime name: []const u8) [][]const u8 { + pub fn options(parser: @This(), comptime name: []const u8) []const []const u8 { const param = comptime findParam(name); if (param.takes_value == .None) @compileError(name ++ " is a flag and not an option."); if (param.takes_value == .One) @compileError(name ++ " takes one option, not multiple."); - return parser.options[param.id].items; + return parser.multi_options[param.id]; } pub fn option(parser: @This(), comptime name: []const u8) ?[]const u8 { @@ -102,8 +113,7 @@ pub fn ComptimeClap(comptime Id: type, comptime params: []const clap.Param(Id)) @compileError(name ++ " is a flag and not an option."); if (param.takes_value == .Many) @compileError(name ++ " takes many options, not one."); - const items = parser.options[param.id].items; - return if (items.len > 0) items[0] else null; + return parser.single_options[param.id]; } pub fn positionals(parser: @This()) []const []const u8 { @@ -158,6 +168,6 @@ test "clap.comptime.ComptimeClap" { testing.expectEqualStrings("0", args.option("--cc").?); testing.expectEqual(@as(usize, 1), args.positionals().len); testing.expectEqualStrings("something", args.positionals()[0]); - testing.expectEqualSlices([]const u8, &[_][]const u8{ "a", "b" }, args.allOptions("-d")); - testing.expectEqualSlices([]const u8, &[_][]const u8{ "a", "b" }, args.allOptions("--dd")); + testing.expectEqualSlices([]const u8, &[_][]const u8{ "a", "b" }, args.options("-d")); + testing.expectEqualSlices([]const u8, &[_][]const u8{ "a", "b" }, args.options("--dd")); } -- cgit v1.2.3 From 198b96d7a70deae180e8a70df8f1227594e0037f Mon Sep 17 00:00:00 2001 From: Asherah Connor Date: Fri, 28 Aug 2020 15:48:59 +1000 Subject: add documentation to Values enum --- clap.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/clap.zig b/clap.zig index 1a3659b..593191d 100644 --- a/clap.zig +++ b/clap.zig @@ -25,6 +25,7 @@ pub const Names = struct { long: ?[]const u8 = null, }; +/// Whether a param takes no value (a flag), one value, or can be specified multiple times. pub const Values = enum { None, One, -- cgit v1.2.3 From 41ac75b6db818ce9431d145d5a9e335cf38cc117 Mon Sep 17 00:00:00 2001 From: Asherah Connor Date: Fri, 28 Aug 2020 15:57:36 +1000 Subject: fix up --- clap.zig | 4 ++-- clap/comptime.zig | 13 ++++++++----- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/clap.zig b/clap.zig index 593191d..c312251 100644 --- a/clap.zig +++ b/clap.zig @@ -277,8 +277,8 @@ pub fn Args(comptime Id: type, comptime params: []const Param(Id)) type { return a.clap.option(name); } - pub fn allOptions(a: @This(), comptime name: []const u8) [][]const u8 { - return a.clap.allOptions(name); + pub fn options(a: @This(), comptime name: []const u8) []const []const u8 { + return a.clap.options(name); } pub fn positionals(a: @This()) []const []const u8 { diff --git a/clap/comptime.zig b/clap/comptime.zig index edbaafb..99f0be6 100644 --- a/clap/comptime.zig +++ b/clap/comptime.zig @@ -39,7 +39,7 @@ pub fn ComptimeClap(comptime Id: type, comptime params: []const clap.Param(Id)) allocator: *mem.Allocator, pub fn parse(allocator: *mem.Allocator, comptime ArgIter: type, iter: *ArgIter) !@This() { - var multis = [_]std.ArrayList([]const u8){undefined} ** single_options; + var multis = [_]std.ArrayList([]const u8){undefined} ** multi_options; for (multis) |*multi| { multi.* = std.ArrayList([]const u8).init(allocator); } @@ -64,13 +64,16 @@ pub fn ComptimeClap(comptime Id: type, comptime params: []const clap.Param(Id)) try pos.append(arg.value.?); } else if (param.takes_value == .One) { debug.assert(res.single_options.len != 0); - res.single_options[param.id] = arg.value.?; + if (res.single_options.len != 0) + res.single_options[param.id] = arg.value.?; } else if (param.takes_value == .Many) { - debug.assert(res.multi_options.len != 0); - try multis[param.id].append(arg.value.?); + debug.assert(multis.len != 0); + if (multis.len != 0) + try multis[param.id].append(arg.value.?); } else { debug.assert(res.flags.len != 0); - res.flags[param.id] = true; + if (res.flags.len != 0) + res.flags[param.id] = true; } } -- cgit v1.2.3 From 02644d62f291b19c1306c1f7ff596817cc4d08d9 Mon Sep 17 00:00:00 2001 From: Asherah Connor Date: Fri, 28 Aug 2020 16:19:20 +1000 Subject: reverse the order of these --- clap/comptime.zig | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/clap/comptime.zig b/clap/comptime.zig index 99f0be6..b0edb2a 100644 --- a/clap/comptime.zig +++ b/clap/comptime.zig @@ -100,23 +100,23 @@ pub fn ComptimeClap(comptime Id: type, comptime params: []const clap.Param(Id)) return parser.flags[param.id]; } - pub fn options(parser: @This(), comptime name: []const u8) []const []const u8 { + pub fn option(parser: @This(), comptime name: []const u8) ?[]const u8 { const param = comptime findParam(name); if (param.takes_value == .None) @compileError(name ++ " is a flag and not an option."); - if (param.takes_value == .One) - @compileError(name ++ " takes one option, not multiple."); - - return parser.multi_options[param.id]; + if (param.takes_value == .Many) + @compileError(name ++ " takes many options, not one."); + return parser.single_options[param.id]; } - pub fn option(parser: @This(), comptime name: []const u8) ?[]const u8 { + pub fn options(parser: @This(), comptime name: []const u8) []const []const u8 { const param = comptime findParam(name); if (param.takes_value == .None) @compileError(name ++ " is a flag and not an option."); - if (param.takes_value == .Many) - @compileError(name ++ " takes many options, not one."); - return parser.single_options[param.id]; + if (param.takes_value == .One) + @compileError(name ++ " takes one option, not multiple."); + + return parser.multi_options[param.id]; } pub fn positionals(parser: @This()) []const []const u8 { -- cgit v1.2.3 From 620b680c4d03cec7209d25e4019cf29a033dcb26 Mon Sep 17 00:00:00 2001 From: Asherah Connor Date: Fri, 28 Aug 2020 17:26:01 +1000 Subject: adjust examples, README template --- README.md | 26 +++++++++++++++++--------- clap/comptime.zig | 6 +++--- example/README.md.template | 4 +++- example/comptime-clap.zig | 9 ++++++--- example/simple.zig | 9 ++++++--- example/streaming-clap.zig | 4 ++-- 6 files changed, 37 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 06d29c5..2037b04 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ into master on every `zig` release. * Supports both passing values using spacing and `=` (`-a 100`, `-a=100`) * Short args also support passing values with no spacing or `=` (`-a100`) * This all works with chaining (`-ba 100`, `-ba=100`, `-ba100`) +* Supports options that can be specified multiple times (`-e 1 -e 2 -e 3`) * Print help message from parameter specification. * Parse help message to parameter specification. @@ -33,10 +34,11 @@ pub fn main() !void { // First we specify what parameters our program can take. // We can use `parseParam` to parse a string to a `Param(Help)` const params = comptime [_]clap.Param(clap.Help){ - clap.parseParam("-h, --help Display this help and exit. ") catch unreachable, - clap.parseParam("-n, --number An option parameter, which takes a value.") catch unreachable, + clap.parseParam("-h, --help Display this help and exit. ") catch unreachable, + clap.parseParam("-n, --number An option parameter, which takes a value.") catch unreachable, + clap.parseParam("-s, --string ... An option parameter which can be specified multiple times.") catch unreachable, clap.Param(clap.Help){ - .takes_value = true, + .takes_value = .One, }, }; @@ -47,6 +49,8 @@ pub fn main() !void { debug.warn("--help\n", .{}); if (args.option("--number")) |n| debug.warn("--number = {}\n", .{n}); + for (args.options("--string")) |s| + debug.warn("--string = {}\n", .{s}); for (args.positionals()) |pos| debug.warn("{}\n", .{pos}); } @@ -54,7 +58,8 @@ pub fn main() !void { ``` The data structure returned has lookup speed on par with array access (`arr[i]`) and validates -that the strings you pass to `option` and `flag` are actually parameters that the program can take: +that the strings you pass to `option`, `options` and `flag` are actually parameters that the +program can take: ```zig const std = @import("std"); @@ -106,10 +111,11 @@ pub fn main() !void { // First we specify what parameters our program can take. // We can use `parseParam` to parse a string to a `Param(Help)` const params = comptime [_]clap.Param(clap.Help){ - clap.parseParam("-h, --help Display this help and exit. ") catch unreachable, - clap.parseParam("-n, --number An option parameter, which takes a value.") catch unreachable, + clap.parseParam("-h, --help Display this help and exit. ") catch unreachable, + clap.parseParam("-n, --number An option parameter, which takes a value.") catch unreachable, + clap.parseParam("-s, --string ... An option parameter which can be specified multiple times.") catch unreachable, clap.Param(clap.Help){ - .takes_value = true, + .takes_value = .One, }, }; @@ -126,6 +132,8 @@ pub fn main() !void { debug.warn("--help\n", .{}); if (args.option("--number")) |n| debug.warn("--number = {}\n", .{n}); + for (args.options("--string")) |s| + debug.warn("--string = {}\n", .{s}); for (args.positionals()) |pos| debug.warn("{}\n", .{pos}); } @@ -155,11 +163,11 @@ pub fn main() !void { clap.Param(u8){ .id = 'n', .names = clap.Names{ .short = 'n', .long = "number" }, - .takes_value = true, + .takes_value = .One, }, clap.Param(u8){ .id = 'f', - .takes_value = true, + .takes_value = .One, }, }; diff --git a/clap/comptime.zig b/clap/comptime.zig index b0edb2a..28ec42b 100644 --- a/clap/comptime.zig +++ b/clap/comptime.zig @@ -167,10 +167,10 @@ test "clap.comptime.ComptimeClap" { testing.expect(args.flag("--aa")); testing.expect(!args.flag("-b")); testing.expect(!args.flag("--bb")); - testing.expectEqualStrings("0", args.option("-c").?); - testing.expectEqualStrings("0", args.option("--cc").?); + testing.expectEqualSlices(u8, "0", args.option("-c").?); + testing.expectEqualSlices(u8, "0", args.option("--cc").?); testing.expectEqual(@as(usize, 1), args.positionals().len); - testing.expectEqualStrings("something", args.positionals()[0]); + testing.expectEqualSlices(u8, "something", args.positionals()[0]); testing.expectEqualSlices([]const u8, &[_][]const u8{ "a", "b" }, args.options("-d")); testing.expectEqualSlices([]const u8, &[_][]const u8{ "a", "b" }, args.options("--dd")); } diff --git a/example/README.md.template b/example/README.md.template index 2afbe86..65b507d 100644 --- a/example/README.md.template +++ b/example/README.md.template @@ -14,6 +14,7 @@ into master on every `zig` release. * Supports both passing values using spacing and `=` (`-a 100`, `-a=100`) * Short args also support passing values with no spacing or `=` (`-a100`) * This all works with chaining (`-ba 100`, `-ba=100`, `-ba100`) +* Supports options that can be specified multiple times (`-e 1 -e 2 -e 3`) * Print help message from parameter specification. * Parse help message to parameter specification. @@ -28,7 +29,8 @@ The simplest way to use this library is to just call the `clap.parse` function. ``` The data structure returned has lookup speed on par with array access (`arr[i]`) and validates -that the strings you pass to `option` and `flag` are actually parameters that the program can take: +that the strings you pass to `option`, `options` and `flag` are actually parameters that the +program can take: ```zig {} diff --git a/example/comptime-clap.zig b/example/comptime-clap.zig index d5c84fe..d709e48 100644 --- a/example/comptime-clap.zig +++ b/example/comptime-clap.zig @@ -9,10 +9,11 @@ pub fn main() !void { // First we specify what parameters our program can take. // We can use `parseParam` to parse a string to a `Param(Help)` const params = comptime [_]clap.Param(clap.Help){ - clap.parseParam("-h, --help Display this help and exit. ") catch unreachable, - clap.parseParam("-n, --number An option parameter, which takes a value.") catch unreachable, + clap.parseParam("-h, --help Display this help and exit. ") catch unreachable, + clap.parseParam("-n, --number An option parameter, which takes a value.") catch unreachable, + clap.parseParam("-s, --string ... An option parameter which can be specified multiple times.") catch unreachable, clap.Param(clap.Help){ - .takes_value = true, + .takes_value = .One, }, }; @@ -29,6 +30,8 @@ pub fn main() !void { debug.warn("--help\n", .{}); if (args.option("--number")) |n| debug.warn("--number = {}\n", .{n}); + for (args.options("--string")) |s| + debug.warn("--string = {}\n", .{s}); for (args.positionals()) |pos| debug.warn("{}\n", .{pos}); } diff --git a/example/simple.zig b/example/simple.zig index 3510317..adea9f9 100644 --- a/example/simple.zig +++ b/example/simple.zig @@ -7,10 +7,11 @@ pub fn main() !void { // First we specify what parameters our program can take. // We can use `parseParam` to parse a string to a `Param(Help)` const params = comptime [_]clap.Param(clap.Help){ - clap.parseParam("-h, --help Display this help and exit. ") catch unreachable, - clap.parseParam("-n, --number An option parameter, which takes a value.") catch unreachable, + clap.parseParam("-h, --help Display this help and exit. ") catch unreachable, + clap.parseParam("-n, --number An option parameter, which takes a value.") catch unreachable, + clap.parseParam("-s, --string ... An option parameter which can be specified multiple times.") catch unreachable, clap.Param(clap.Help){ - .takes_value = true, + .takes_value = .One, }, }; @@ -21,6 +22,8 @@ pub fn main() !void { debug.warn("--help\n", .{}); if (args.option("--number")) |n| debug.warn("--number = {}\n", .{n}); + for (args.options("--string")) |s| + debug.warn("--string = {}\n", .{s}); for (args.positionals()) |pos| debug.warn("{}\n", .{pos}); } diff --git a/example/streaming-clap.zig b/example/streaming-clap.zig index faf388a..b92a9e6 100644 --- a/example/streaming-clap.zig +++ b/example/streaming-clap.zig @@ -15,11 +15,11 @@ pub fn main() !void { clap.Param(u8){ .id = 'n', .names = clap.Names{ .short = 'n', .long = "number" }, - .takes_value = true, + .takes_value = .One, }, clap.Param(u8){ .id = 'f', - .takes_value = true, + .takes_value = .One, }, }; -- cgit v1.2.3 From 093d29899b8fdf449b944e973b07b5992be2144a Mon Sep 17 00:00:00 2001 From: Komari Spaghetti Date: Mon, 2 Nov 2020 18:04:30 +0000 Subject: Report error context in Diagnostic (#26) --- README.md | 38 +++++++++++++++++---- clap.zig | 51 ++++++++++++++++++++++++++-- clap/comptime.zig | 14 +++++--- clap/streaming.zig | 84 +++++++++++++++++++++++++--------------------- example/comptime-clap.zig | 12 ++++++- example/simple-error.zig | 4 +-- example/simple.zig | 11 +++++- example/streaming-clap.zig | 11 +++++- 8 files changed, 167 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index 2037b04..fbb3e35 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,16 @@ pub fn main() !void { }, }; - var args = try clap.parse(clap.Help, ¶ms, std.heap.page_allocator); + // Initalize our diagnostics, which can be used for reporting useful errors. + // This is optional. You can also just pass `null` to `parser.next` if you + // don't care about the extra information `Diagnostics` provides. + var diag: clap.Diagnostic = undefined; + + var args = clap.parse(clap.Help, ¶ms, std.heap.page_allocator, &diag) catch |err| { + // Report useful error and exit + diag.report(std.io.getStdErr().outStream(), err) catch {}; + return err; + }; defer args.deinit(); if (args.flag("--help")) @@ -66,13 +75,11 @@ const std = @import("std"); const clap = @import("clap"); pub fn main() !void { - // First we specify what parameters our program can take. - // We can use `parseParam` to parse a string to a `Param(Help)` const params = comptime [_]clap.Param(clap.Help){ clap.parseParam("-h, --help Display this help and exit.") catch unreachable, }; - var args = try clap.parse(clap.Help, ¶ms, std.heap.direct_allocator); + var args = try clap.parse(clap.Help, ¶ms, std.heap.direct_allocator, null); defer args.deinit(); _ = args.flag("--helps"); @@ -118,14 +125,24 @@ pub fn main() !void { .takes_value = .One, }, }; + const Clap = clap.ComptimeClap(clap.Help, clap.args.OsIterator, ¶ms); // We then initialize an argument iterator. We will use the OsIterator as it nicely // wraps iterating over arguments the most efficient way on each os. var iter = try clap.args.OsIterator.init(allocator); defer iter.deinit(); + // Initalize our diagnostics, which can be used for reporting useful errors. + // This is optional. You can also just pass `null` to `parser.next` if you + // don't care about the extra information `Diagnostics` provides. + var diag: clap.Diagnostic = undefined; + // Parse the arguments - var args = try clap.ComptimeClap(clap.Help, ¶ms).parse(allocator, clap.args.OsIterator, &iter); + var args = Clap.parse(allocator, &iter, &diag) catch |err| { + // Report useful error and exit + diag.report(std.io.getStdErr().outStream(), err) catch {}; + return err; + }; defer args.deinit(); if (args.flag("--help")) @@ -182,8 +199,17 @@ pub fn main() !void { .iter = &iter, }; + // Initalize our diagnostics, which can be used for reporting useful errors. + // This is optional. You can also just pass `null` to `parser.next` if you + // don't care about the extra information `Diagnostics` provides. + var diag: clap.Diagnostic = undefined; + // Because we use a streaming parser, we have to consume each argument parsed individually. - while (try parser.next()) |arg| { + while (parser.next(&diag) catch |err| { + // Report useful error and exit + diag.report(std.io.getStdErr().outStream(), err) catch {}; + return err; + }) |arg| { // arg.param will point to the parameter which matched the argument. switch (arg.param.id) { 'h' => debug.warn("Help!\n", .{}), diff --git a/clap.zig b/clap.zig index c312251..016aea6 100644 --- a/clap.zig +++ b/clap.zig @@ -261,7 +261,7 @@ fn find(str: []const u8, f: []const u8) []const u8 { pub fn Args(comptime Id: type, comptime params: []const Param(Id)) type { return struct { arena: std.heap.ArenaAllocator, - clap: ComptimeClap(Id, params), + clap: ComptimeClap(Id, args.OsIterator, params), exe_arg: ?[]const u8, pub fn deinit(a: *@This()) void { @@ -287,15 +287,62 @@ pub fn Args(comptime Id: type, comptime params: []const Param(Id)) type { }; } +/// Optional diagnostics used for reporting useful errors +pub const Diagnostic = struct { + name: Names, + + /// Default diagnostics reporter when all you want is English with no colors. + /// Use this as a reference for implementing your own if needed. + pub fn report(diag: Diagnostic, stream: var, err: anyerror) !void { + const prefix = if (diag.name.short) |_| "-" else "--"; + const name = if (diag.name.short) |*c| @as(*const [1]u8, c)[0..] else diag.name.long.?; + + switch (err) { + error.DoesntTakeValue => try stream.print("The argument '{}{}' does not take a value\n", .{ prefix, name }), + error.MissingValue => try stream.print("The argument '{}{}' requires a value but none was supplied\n", .{ prefix, name }), + error.InvalidArgument => try stream.print("Invalid argument '{}{}'\n", .{ prefix, name }), + else => try stream.print("Error while parsing arguments: {}\n", .{@errorName(err)}), + } + } +}; + +fn testDiag(names: Names, err: anyerror, expected: []const u8) void { + var buf: [1024]u8 = undefined; + var slice_stream = io.fixedBufferStream(&buf); + (Diagnostic{ .name = names }).report(slice_stream.outStream(), err) catch unreachable; + + const actual = slice_stream.getWritten(); + if (!mem.eql(u8, actual, expected)) { + debug.warn("\n============ Expected ============\n", .{}); + debug.warn("{}", .{expected}); + debug.warn("============= Actual =============\n", .{}); + debug.warn("{}", .{actual}); + testing.expect(false); + } +} + +test "Diagnostic.report" { + testDiag(.{ .short = 'c' }, error.DoesntTakeValue, "The argument '-c' does not take a value\n"); + testDiag(.{ .long = "cc" }, error.DoesntTakeValue, "The argument '--cc' does not take a value\n"); + testDiag(.{ .short = 'c' }, error.MissingValue, "The argument '-c' requires a value but none was supplied\n"); + testDiag(.{ .long = "cc" }, error.MissingValue, "The argument '--cc' requires a value but none was supplied\n"); + testDiag(.{ .short = 'c' }, error.InvalidArgument, "Invalid argument '-c'\n"); + testDiag(.{ .long = "cc" }, error.InvalidArgument, "Invalid argument '--cc'\n"); + testDiag(.{ .short = 'c' }, error.SomethingElse, "Error while parsing arguments: SomethingElse\n"); + testDiag(.{ .long = "cc" }, error.SomethingElse, "Error while parsing arguments: SomethingElse\n"); +} + /// Parses the command line arguments passed into the program based on an /// array of `Param`s. pub fn parse( comptime Id: type, comptime params: []const Param(Id), allocator: *mem.Allocator, + diag: ?*Diagnostic, ) !Args(Id, params) { var iter = try args.OsIterator.init(allocator); - const clap = try ComptimeClap(Id, params).parse(allocator, args.OsIterator, &iter); + const Clap = ComptimeClap(Id, args.OsIterator, params); + const clap = try Clap.parse(allocator, &iter, diag); return Args(Id, params){ .arena = iter.arena, .clap = clap, diff --git a/clap/comptime.zig b/clap/comptime.zig index 28ec42b..6846770 100644 --- a/clap/comptime.zig +++ b/clap/comptime.zig @@ -6,7 +6,11 @@ const heap = std.heap; const mem = std.mem; const debug = std.debug; -pub fn ComptimeClap(comptime Id: type, comptime params: []const clap.Param(Id)) type { +pub fn ComptimeClap( + comptime Id: type, + comptime ArgIter: type, + comptime params: []const clap.Param(Id), +) type { var flags: usize = 0; var single_options: usize = 0; var multi_options: usize = 0; @@ -38,7 +42,7 @@ pub fn ComptimeClap(comptime Id: type, comptime params: []const clap.Param(Id)) pos: []const []const u8, allocator: *mem.Allocator, - pub fn parse(allocator: *mem.Allocator, comptime ArgIter: type, iter: *ArgIter) !@This() { + pub fn parse(allocator: *mem.Allocator, iter: *ArgIter, diag: ?*clap.Diagnostic) !@This() { var multis = [_]std.ArrayList([]const u8){undefined} ** multi_options; for (multis) |*multi| { multi.* = std.ArrayList([]const u8).init(allocator); @@ -58,7 +62,7 @@ pub fn ComptimeClap(comptime Id: type, comptime params: []const clap.Param(Id)) .params = converted_params, .iter = iter, }; - while (try stream.next()) |arg| { + while (try stream.next(diag)) |arg| { const param = arg.param; if (param.names.long == null and param.names.short == null) { try pos.append(arg.value.?); @@ -143,7 +147,7 @@ pub fn ComptimeClap(comptime Id: type, comptime params: []const clap.Param(Id)) } test "clap.comptime.ComptimeClap" { - const Clap = ComptimeClap(clap.Help, comptime &[_]clap.Param(clap.Help){ + const Clap = ComptimeClap(clap.Help, clap.args.SliceIterator, comptime &[_]clap.Param(clap.Help){ clap.parseParam("-a, --aa ") catch unreachable, clap.parseParam("-b, --bb ") catch unreachable, clap.parseParam("-c, --cc ") catch unreachable, @@ -160,7 +164,7 @@ test "clap.comptime.ComptimeClap" { "-a", "-c", "0", "something", "-d", "a", "--dd", "b", }, }; - var args = try Clap.parse(&fb_allocator.allocator, clap.args.SliceIterator, &iter); + var args = try Clap.parse(&fb_allocator.allocator, &iter, null); defer args.deinit(); testing.expect(args.flag("-a")); diff --git a/clap/streaming.zig b/clap/streaming.zig index b843bff..90c4e02 100644 --- a/clap/streaming.zig +++ b/clap/streaming.zig @@ -24,8 +24,8 @@ pub fn Arg(comptime Id: type) type { pub fn StreamingClap(comptime Id: type, comptime ArgIterator: type) type { return struct { const State = union(enum) { - Normal, - Chaining: Chaining, + normal, + chaining: Chaining, const Chaining = struct { arg: []const u8, @@ -35,49 +35,48 @@ pub fn StreamingClap(comptime Id: type, comptime ArgIterator: type) type { params: []const clap.Param(Id), iter: *ArgIterator, - state: State = State.Normal, + state: State = .normal, /// Get the next ::Arg that matches a ::Param. - pub fn next(parser: *@This()) !?Arg(Id) { + pub fn next(parser: *@This(), diag: ?*clap.Diagnostic) !?Arg(Id) { const ArgInfo = struct { - const Kind = enum { - Long, - Short, - Positional, - }; - arg: []const u8, - kind: Kind, + kind: enum { + long, + short, + positional, + }, }; switch (parser.state) { - .Normal => { + .normal => { const full_arg = (try parser.iter.next()) orelse return null; const arg_info = if (mem.eql(u8, full_arg, "--") or mem.eql(u8, full_arg, "-")) - ArgInfo{ .arg = full_arg, .kind = .Positional } + ArgInfo{ .arg = full_arg, .kind = .positional } else if (mem.startsWith(u8, full_arg, "--")) - ArgInfo{ .arg = full_arg[2..], .kind = .Long } + ArgInfo{ .arg = full_arg[2..], .kind = .long } else if (mem.startsWith(u8, full_arg, "-")) - ArgInfo{ .arg = full_arg[1..], .kind = .Short } + ArgInfo{ .arg = full_arg[1..], .kind = .short } else - ArgInfo{ .arg = full_arg, .kind = .Positional }; + ArgInfo{ .arg = full_arg, .kind = .positional }; const arg = arg_info.arg; const kind = arg_info.kind; - const eql_index = mem.indexOfScalar(u8, arg, '='); switch (kind) { - ArgInfo.Kind.Long => { + .long => { + const eql_index = mem.indexOfScalar(u8, arg, '='); + const name = if (eql_index) |i| arg[0..i] else arg; + const maybe_value = if (eql_index) |i| arg[i + 1 ..] else null; + for (parser.params) |*param| { const match = param.names.long orelse continue; - const name = if (eql_index) |i| arg[0..i] else arg; - const maybe_value = if (eql_index) |i| arg[i + 1 ..] else null; if (!mem.eql(u8, name, match)) continue; if (param.takes_value == .None) { if (maybe_value != null) - return error.DoesntTakeValue; + return err(diag, param.names, error.DoesntTakeValue); return Arg(Id){ .param = param }; } @@ -86,19 +85,18 @@ pub fn StreamingClap(comptime Id: type, comptime ArgIterator: type) type { if (maybe_value) |v| break :blk v; - break :blk (try parser.iter.next()) orelse return error.MissingValue; + break :blk (try parser.iter.next()) orelse + return err(diag, param.names, error.MissingValue); }; return Arg(Id){ .param = param, .value = value }; } }, - ArgInfo.Kind.Short => { - return try parser.chainging(State.Chaining{ - .arg = full_arg, - .index = (full_arg.len - arg.len), - }); - }, - ArgInfo.Kind.Positional => { + .short => return try parser.chainging(.{ + .arg = full_arg, + .index = full_arg.len - arg.len, + }, diag), + .positional => { for (parser.params) |*param| { if (param.names.long) |_| continue; @@ -110,13 +108,13 @@ pub fn StreamingClap(comptime Id: type, comptime ArgIterator: type) type { }, } - return error.InvalidArgument; + return err(diag, .{ .long = arg }, error.InvalidArgument); }, - .Chaining => |state| return try parser.chainging(state), + .chaining => |state| return try parser.chainging(state, diag), } } - fn chainging(parser: *@This(), state: State.Chaining) !?Arg(Id) { + fn chainging(parser: *@This(), state: State.Chaining, diag: ?*clap.Diagnostic) !?Arg(Id) { const arg = state.arg; const index = state.index; const next_index = index + 1; @@ -129,10 +127,10 @@ pub fn StreamingClap(comptime Id: type, comptime ArgIterator: type) type { // Before we return, we have to set the new state of the clap defer { if (arg.len <= next_index or param.takes_value != .None) { - parser.state = State.Normal; + parser.state = .normal; } else { - parser.state = State{ - .Chaining = State.Chaining{ + parser.state = .{ + .chaining = .{ .arg = arg, .index = next_index, }, @@ -144,7 +142,9 @@ pub fn StreamingClap(comptime Id: type, comptime ArgIterator: type) type { return Arg(Id){ .param = param }; if (arg.len <= next_index) { - const value = (try parser.iter.next()) orelse return error.MissingValue; + const value = (try parser.iter.next()) orelse + return err(diag, param.names, error.MissingValue); + return Arg(Id){ .param = param, .value = value }; } @@ -154,7 +154,13 @@ pub fn StreamingClap(comptime Id: type, comptime ArgIterator: type) type { return Arg(Id){ .param = param, .value = arg[next_index..] }; } - return error.InvalidArgument; + return err(diag, .{ .short = arg[index] }, error.InvalidArgument); + } + + fn err(diag: ?*clap.Diagnostic, names: clap.Names, _err: var) @TypeOf(_err) { + if (diag) |d| + d.name = names; + return _err; } }; } @@ -167,7 +173,7 @@ fn testNoErr(params: []const clap.Param(u8), args_strings: []const []const u8, r }; for (results) |res| { - const arg = (c.next() catch unreachable) orelse unreachable; + const arg = (c.next(null) catch unreachable) orelse unreachable; testing.expectEqual(res.param, arg.param); const expected_value = res.value orelse { testing.expectEqual(@as(@TypeOf(arg.value), null), arg.value); @@ -177,7 +183,7 @@ fn testNoErr(params: []const clap.Param(u8), args_strings: []const []const u8, r testing.expectEqualSlices(u8, expected_value, actual_value); } - if (c.next() catch unreachable) |_| + if (c.next(null) catch unreachable) |_| unreachable; } diff --git a/example/comptime-clap.zig b/example/comptime-clap.zig index d709e48..530c7e6 100644 --- a/example/comptime-clap.zig +++ b/example/comptime-clap.zig @@ -16,14 +16,24 @@ pub fn main() !void { .takes_value = .One, }, }; + const Clap = clap.ComptimeClap(clap.Help, clap.args.OsIterator, ¶ms); // We then initialize an argument iterator. We will use the OsIterator as it nicely // wraps iterating over arguments the most efficient way on each os. var iter = try clap.args.OsIterator.init(allocator); defer iter.deinit(); + // Initalize our diagnostics, which can be used for reporting useful errors. + // This is optional. You can also just pass `null` to `parser.next` if you + // don't care about the extra information `Diagnostics` provides. + var diag: clap.Diagnostic = undefined; + // Parse the arguments - var args = try clap.ComptimeClap(clap.Help, ¶ms).parse(allocator, clap.args.OsIterator, &iter); + var args = Clap.parse(allocator, &iter, &diag) catch |err| { + // Report useful error and exit + diag.report(std.io.getStdErr().outStream(), err) catch {}; + return err; + }; defer args.deinit(); if (args.flag("--help")) diff --git a/example/simple-error.zig b/example/simple-error.zig index 2c403fc..3c62f0e 100644 --- a/example/simple-error.zig +++ b/example/simple-error.zig @@ -2,13 +2,11 @@ const std = @import("std"); const clap = @import("clap"); pub fn main() !void { - // First we specify what parameters our program can take. - // We can use `parseParam` to parse a string to a `Param(Help)` const params = comptime [_]clap.Param(clap.Help){ clap.parseParam("-h, --help Display this help and exit.") catch unreachable, }; - var args = try clap.parse(clap.Help, ¶ms, std.heap.direct_allocator); + var args = try clap.parse(clap.Help, ¶ms, std.heap.direct_allocator, null); defer args.deinit(); _ = args.flag("--helps"); diff --git a/example/simple.zig b/example/simple.zig index adea9f9..f7b5953 100644 --- a/example/simple.zig +++ b/example/simple.zig @@ -15,7 +15,16 @@ pub fn main() !void { }, }; - var args = try clap.parse(clap.Help, ¶ms, std.heap.page_allocator); + // Initalize our diagnostics, which can be used for reporting useful errors. + // This is optional. You can also just pass `null` to `parser.next` if you + // don't care about the extra information `Diagnostics` provides. + var diag: clap.Diagnostic = undefined; + + var args = clap.parse(clap.Help, ¶ms, std.heap.page_allocator, &diag) catch |err| { + // Report useful error and exit + diag.report(std.io.getStdErr().outStream(), err) catch {}; + return err; + }; defer args.deinit(); if (args.flag("--help")) diff --git a/example/streaming-clap.zig b/example/streaming-clap.zig index b92a9e6..941070f 100644 --- a/example/streaming-clap.zig +++ b/example/streaming-clap.zig @@ -34,8 +34,17 @@ pub fn main() !void { .iter = &iter, }; + // Initalize our diagnostics, which can be used for reporting useful errors. + // This is optional. You can also just pass `null` to `parser.next` if you + // don't care about the extra information `Diagnostics` provides. + var diag: clap.Diagnostic = undefined; + // Because we use a streaming parser, we have to consume each argument parsed individually. - while (try parser.next()) |arg| { + while (parser.next(&diag) catch |err| { + // Report useful error and exit + diag.report(std.io.getStdErr().outStream(), err) catch {}; + return err; + }) |arg| { // arg.param will point to the parameter which matched the argument. switch (arg.param.id) { 'h' => debug.warn("Help!\n", .{}), -- cgit v1.2.3 From c78a5dd3d751bc12013b6441b02dd67cea4c9b82 Mon Sep 17 00:00:00 2001 From: Jimmi Holst Christensen Date: Tue, 3 Nov 2020 20:04:52 +0100 Subject: Improve Diagnostic error message reporting --- clap.zig | 43 +++++++++++++++---------- clap/args.zig | 2 +- clap/comptime.zig | 2 +- clap/streaming.zig | 92 +++++++++++++++++++++++++++++++++++++++++++++--------- 4 files changed, 107 insertions(+), 32 deletions(-) diff --git a/clap.zig b/clap.zig index 016aea6..9a18bac 100644 --- a/clap.zig +++ b/clap.zig @@ -289,27 +289,36 @@ pub fn Args(comptime Id: type, comptime params: []const Param(Id)) type { /// Optional diagnostics used for reporting useful errors pub const Diagnostic = struct { - name: Names, + arg: []const u8 = "", + name: Names = Names{}, /// Default diagnostics reporter when all you want is English with no colors. /// Use this as a reference for implementing your own if needed. pub fn report(diag: Diagnostic, stream: var, err: anyerror) !void { - const prefix = if (diag.name.short) |_| "-" else "--"; - const name = if (diag.name.short) |*c| @as(*const [1]u8, c)[0..] else diag.name.long.?; + const Arg = struct { + prefix: []const u8, + name: []const u8, + }; + const a = if (diag.name.short) |*c| + Arg{ .prefix = "-", .name = @as(*const [1]u8, c)[0..] } + else if (diag.name.long) |l| + Arg{ .prefix = "--", .name = l } + else + Arg{ .prefix = "", .name = diag.arg }; switch (err) { - error.DoesntTakeValue => try stream.print("The argument '{}{}' does not take a value\n", .{ prefix, name }), - error.MissingValue => try stream.print("The argument '{}{}' requires a value but none was supplied\n", .{ prefix, name }), - error.InvalidArgument => try stream.print("Invalid argument '{}{}'\n", .{ prefix, name }), + error.DoesntTakeValue => try stream.print("The argument '{}{}' does not take a value\n", .{ a.prefix, a.name }), + error.MissingValue => try stream.print("The argument '{}{}' requires a value but none was supplied\n", .{ a.prefix, a.name }), + error.InvalidArgument => try stream.print("Invalid argument '{}{}'\n", .{ a.prefix, a.name }), else => try stream.print("Error while parsing arguments: {}\n", .{@errorName(err)}), } } }; -fn testDiag(names: Names, err: anyerror, expected: []const u8) void { +fn testDiag(diag: Diagnostic, err: anyerror, expected: []const u8) void { var buf: [1024]u8 = undefined; var slice_stream = io.fixedBufferStream(&buf); - (Diagnostic{ .name = names }).report(slice_stream.outStream(), err) catch unreachable; + diag.report(slice_stream.outStream(), err) catch unreachable; const actual = slice_stream.getWritten(); if (!mem.eql(u8, actual, expected)) { @@ -322,14 +331,16 @@ fn testDiag(names: Names, err: anyerror, expected: []const u8) void { } test "Diagnostic.report" { - testDiag(.{ .short = 'c' }, error.DoesntTakeValue, "The argument '-c' does not take a value\n"); - testDiag(.{ .long = "cc" }, error.DoesntTakeValue, "The argument '--cc' does not take a value\n"); - testDiag(.{ .short = 'c' }, error.MissingValue, "The argument '-c' requires a value but none was supplied\n"); - testDiag(.{ .long = "cc" }, error.MissingValue, "The argument '--cc' requires a value but none was supplied\n"); - testDiag(.{ .short = 'c' }, error.InvalidArgument, "Invalid argument '-c'\n"); - testDiag(.{ .long = "cc" }, error.InvalidArgument, "Invalid argument '--cc'\n"); - testDiag(.{ .short = 'c' }, error.SomethingElse, "Error while parsing arguments: SomethingElse\n"); - testDiag(.{ .long = "cc" }, error.SomethingElse, "Error while parsing arguments: SomethingElse\n"); + testDiag(.{ .arg = "c" }, error.InvalidArgument, "Invalid argument 'c'\n"); + testDiag(.{ .name = .{ .long = "cc" } }, error.InvalidArgument, "Invalid argument '--cc'\n"); + testDiag(.{ .name = .{ .short = 'c' } }, error.DoesntTakeValue, "The argument '-c' does not take a value\n"); + testDiag(.{ .name = .{ .long = "cc" } }, error.DoesntTakeValue, "The argument '--cc' does not take a value\n"); + testDiag(.{ .name = .{ .short = 'c' } }, error.MissingValue, "The argument '-c' requires a value but none was supplied\n"); + testDiag(.{ .name = .{ .long = "cc" } }, error.MissingValue, "The argument '--cc' requires a value but none was supplied\n"); + testDiag(.{ .name = .{ .short = 'c' } }, error.InvalidArgument, "Invalid argument '-c'\n"); + testDiag(.{ .name = .{ .long = "cc" } }, error.InvalidArgument, "Invalid argument '--cc'\n"); + testDiag(.{ .name = .{ .short = 'c' } }, error.SomethingElse, "Error while parsing arguments: SomethingElse\n"); + testDiag(.{ .name = .{ .long = "cc" } }, error.SomethingElse, "Error while parsing arguments: SomethingElse\n"); } /// Parses the command line arguments passed into the program based on an diff --git a/clap/args.zig b/clap/args.zig index 0141d86..0105769 100644 --- a/clap/args.zig +++ b/clap/args.zig @@ -32,7 +32,7 @@ pub const SliceIterator = struct { } }; -test "clap.args.SliceIterator" { +test "SliceIterator" { const args = &[_][]const u8{ "A", "BB", "CCC" }; var iter = SliceIterator{ .args = args }; diff --git a/clap/comptime.zig b/clap/comptime.zig index 6846770..df5547a 100644 --- a/clap/comptime.zig +++ b/clap/comptime.zig @@ -146,7 +146,7 @@ pub fn ComptimeClap( }; } -test "clap.comptime.ComptimeClap" { +test "" { const Clap = ComptimeClap(clap.Help, clap.args.SliceIterator, comptime &[_]clap.Param(clap.Help){ clap.parseParam("-a, --aa ") catch unreachable, clap.parseParam("-b, --bb ") catch unreachable, diff --git a/clap/streaming.zig b/clap/streaming.zig index 90c4e02..8ab01f6 100644 --- a/clap/streaming.zig +++ b/clap/streaming.zig @@ -3,10 +3,12 @@ const clap = @import("../clap.zig"); const std = @import("std"); const args = clap.args; -const testing = std.testing; +const debug = std.debug; const heap = std.heap; +const io = std.io; const mem = std.mem; const os = std.os; +const testing = std.testing; /// The result returned from ::StreamingClap.next pub fn Arg(comptime Id: type) type { @@ -76,7 +78,7 @@ pub fn StreamingClap(comptime Id: type, comptime ArgIterator: type) type { continue; if (param.takes_value == .None) { if (maybe_value != null) - return err(diag, param.names, error.DoesntTakeValue); + return err(diag, arg, .{ .long = name }, error.DoesntTakeValue); return Arg(Id){ .param = param }; } @@ -86,11 +88,13 @@ pub fn StreamingClap(comptime Id: type, comptime ArgIterator: type) type { break :blk v; break :blk (try parser.iter.next()) orelse - return err(diag, param.names, error.MissingValue); + return err(diag, arg, .{ .long = name }, error.MissingValue); }; return Arg(Id){ .param = param, .value = value }; } + + return err(diag, arg, .{ .long = name }, error.InvalidArgument); }, .short => return try parser.chainging(.{ .arg = full_arg, @@ -105,10 +109,12 @@ pub fn StreamingClap(comptime Id: type, comptime ArgIterator: type) type { return Arg(Id){ .param = param, .value = arg }; } + + return err(diag, arg, .{}, error.InvalidArgument); }, } - return err(diag, .{ .long = arg }, error.InvalidArgument); + unreachable; }, .chaining => |state| return try parser.chainging(state, diag), } @@ -138,28 +144,32 @@ pub fn StreamingClap(comptime Id: type, comptime ArgIterator: type) type { } } - if (param.takes_value == .None) + const next_is_eql = if (next_index < arg.len) arg[next_index] == '=' else false; + if (param.takes_value == .None) { + if (next_is_eql) + return err(diag, arg, .{ .short = short }, error.DoesntTakeValue); return Arg(Id){ .param = param }; + } if (arg.len <= next_index) { const value = (try parser.iter.next()) orelse - return err(diag, param.names, error.MissingValue); + return err(diag, arg, .{ .short = short }, error.MissingValue); return Arg(Id){ .param = param, .value = value }; } - if (arg[next_index] == '=') + if (next_is_eql) return Arg(Id){ .param = param, .value = arg[next_index + 1 ..] }; return Arg(Id){ .param = param, .value = arg[next_index..] }; } - return err(diag, .{ .short = arg[index] }, error.InvalidArgument); + return err(diag, arg, .{ .short = arg[index] }, error.InvalidArgument); } - fn err(diag: ?*clap.Diagnostic, names: clap.Names, _err: var) @TypeOf(_err) { + fn err(diag: ?*clap.Diagnostic, arg: []const u8, names: clap.Names, _err: var) @TypeOf(_err) { if (diag) |d| - d.name = names; + d.* = .{ .arg = arg, .name = names }; return _err; } }; @@ -187,7 +197,33 @@ fn testNoErr(params: []const clap.Param(u8), args_strings: []const []const u8, r unreachable; } -test "clap.streaming.StreamingClap: short params" { +fn testErr(params: []const clap.Param(u8), args_strings: []const []const u8, expected: []const u8) void { + var diag: clap.Diagnostic = undefined; + var iter = args.SliceIterator{ .args = args_strings }; + var c = StreamingClap(u8, args.SliceIterator){ + .params = params, + .iter = &iter, + }; + while (c.next(&diag) catch |err| { + var buf: [1024]u8 = undefined; + var slice_stream = io.fixedBufferStream(&buf); + diag.report(slice_stream.outStream(), err) catch unreachable; + + const actual = slice_stream.getWritten(); + if (!mem.eql(u8, actual, expected)) { + debug.warn("\n============ Expected ============\n", .{}); + debug.warn("{}", .{expected}); + debug.warn("============= Actual =============\n", .{}); + debug.warn("{}", .{actual}); + testing.expect(false); + } + return; + }) |_| {} + + testing.expect(false); +} + +test "short params" { const params = [_]clap.Param(u8){ clap.Param(u8){ .id = 0, @@ -239,7 +275,7 @@ test "clap.streaming.StreamingClap: short params" { ); } -test "clap.streaming.StreamingClap: long params" { +test "long params" { const params = [_]clap.Param(u8){ clap.Param(u8){ .id = 0, @@ -283,7 +319,7 @@ test "clap.streaming.StreamingClap: long params" { ); } -test "clap.streaming.StreamingClap: positional params" { +test "positional params" { const params = [_]clap.Param(u8){clap.Param(u8){ .id = 0, .takes_value = .One, @@ -299,7 +335,7 @@ test "clap.streaming.StreamingClap: positional params" { ); } -test "clap.streaming.StreamingClap: all params" { +test "all params" { const params = [_]clap.Param(u8){ clap.Param(u8){ .id = 0, @@ -366,3 +402,31 @@ test "clap.streaming.StreamingClap: all params" { }, ); } + +test "errors" { + const params = [_]clap.Param(u8){ + clap.Param(u8){ + .id = 0, + .names = clap.Names{ + .short = 'a', + .long = "aa", + }, + }, + clap.Param(u8){ + .id = 1, + .names = clap.Names{ + .short = 'c', + .long = "cc", + }, + .takes_value = .One, + }, + }; + testErr(¶ms, &[_][]const u8{"q"}, "Invalid argument 'q'\n"); + testErr(¶ms, &[_][]const u8{"-q"}, "Invalid argument '-q'\n"); + testErr(¶ms, &[_][]const u8{"--q"}, "Invalid argument '--q'\n"); + testErr(¶ms, &[_][]const u8{"--q=1"}, "Invalid argument '--q'\n"); + testErr(¶ms, &[_][]const u8{"-a=1"}, "The argument '-a' does not take a value\n"); + testErr(¶ms, &[_][]const u8{"--aa=1"}, "The argument '--aa' does not take a value\n"); + testErr(¶ms, &[_][]const u8{"-c"}, "The argument '-c' requires a value but none was supplied\n"); + testErr(¶ms, &[_][]const u8{"--cc"}, "The argument '--cc' requires a value but none was supplied\n"); +} -- cgit v1.2.3 From 5efa886662ea22ce9f9b0166c5132d2234102bb7 Mon Sep 17 00:00:00 2001 From: Jimmi Holst Christensen Date: Tue, 10 Nov 2020 18:31:35 +0100 Subject: Better parseParam --- README.md | 8 +- clap.zig | 332 ++++++++++++++++++++-------------------------- clap/comptime.zig | 4 +- clap/streaming.zig | 10 +- example/comptime-clap.zig | 4 +- example/simple.zig | 4 +- 6 files changed, 150 insertions(+), 212 deletions(-) diff --git a/README.md b/README.md index fbb3e35..ebcebbb 100644 --- a/README.md +++ b/README.md @@ -37,9 +37,7 @@ pub fn main() !void { clap.parseParam("-h, --help Display this help and exit. ") catch unreachable, clap.parseParam("-n, --number An option parameter, which takes a value.") catch unreachable, clap.parseParam("-s, --string ... An option parameter which can be specified multiple times.") catch unreachable, - clap.Param(clap.Help){ - .takes_value = .One, - }, + clap.parseParam("...") catch unreachable, }; // Initalize our diagnostics, which can be used for reporting useful errors. @@ -121,9 +119,7 @@ pub fn main() !void { clap.parseParam("-h, --help Display this help and exit. ") catch unreachable, clap.parseParam("-n, --number An option parameter, which takes a value.") catch unreachable, clap.parseParam("-s, --string ... An option parameter which can be specified multiple times.") catch unreachable, - clap.Param(clap.Help){ - .takes_value = .One, - }, + clap.parseParam("...") catch unreachable, }; const Clap = clap.ComptimeClap(clap.Help, clap.args.OsIterator, ¶ms); diff --git a/clap.zig b/clap.zig index 9a18bac..8846d3b 100644 --- a/clap.zig +++ b/clap.zig @@ -8,9 +8,7 @@ const testing = std.testing; pub const args = @import("clap/args.zig"); test "clap" { - _ = args; - _ = ComptimeClap; - _ = StreamingClap; + testing.refAllDecls(@This()); } pub const ComptimeClap = @import("clap/comptime.zig").ComptimeClap; @@ -64,229 +62,196 @@ pub fn Param(comptime Id: type) type { /// Takes a string and parses it to a Param(Help). /// This is the reverse of 'help' but for at single parameter only. pub fn parseParam(line: []const u8) !Param(Help) { - var z: usize = 0; - var res = Param(Help){ - .id = Help{ - // For testing, i want to be able to easily compare slices just by pointer, - // so I slice by a runtime value here, so that zig does not optimize this - // out. Maybe I should write the test better, geeh. - .msg = line[z..z], - .value = line[z..z], - }, - }; - + var found_comma = false; var it = mem.tokenize(line, " \t"); var param_str = it.next() orelse return error.NoParamFound; - if (!mem.startsWith(u8, param_str, "--") and mem.startsWith(u8, param_str, "-")) { - const found_comma = param_str[param_str.len - 1] == ','; + + const short_name = if (!mem.startsWith(u8, param_str, "--") and + mem.startsWith(u8, param_str, "-")) + blk: { + found_comma = param_str[param_str.len - 1] == ','; if (found_comma) param_str = param_str[0 .. param_str.len - 1]; if (param_str.len != 2) return error.InvalidShortParam; - res.names.short = param_str[1]; + const short_name = param_str[1]; if (!found_comma) { - var help_msg = it.rest(); - if (it.next()) |next| blk: { - if (mem.startsWith(u8, next, "<")) { - const start = mem.indexOfScalar(u8, help_msg, '<').? + 1; - const len = mem.indexOfScalar(u8, help_msg[start..], '>') orelse break :blk; - res.id.value = help_msg[start..][0..len]; - if (mem.startsWith(u8, help_msg[start + len + 1 ..], "...")) { - res.takes_value = .Many; - help_msg = help_msg[start + len + 1 + 3 ..]; - } else { - res.takes_value = .One; - help_msg = help_msg[start + len + 1 ..]; - } - } - } - - res.id.msg = mem.trim(u8, help_msg, " \t"); + var res = parseParamRest(it.rest()); + res.names.short = short_name; return res; } param_str = it.next() orelse return error.NoParamFound; - } - - if (mem.startsWith(u8, param_str, "--")) { - res.names.long = param_str[2..]; + break :blk short_name; + } else null; + const long_name = if (mem.startsWith(u8, param_str, "--")) blk: { if (param_str[param_str.len - 1] == ',') return error.TrailingComma; - var help_msg = it.rest(); - if (it.next()) |next| blk: { - if (mem.startsWith(u8, next, "<")) { - const start = mem.indexOfScalar(u8, help_msg, '<').? + 1; - const len = mem.indexOfScalar(u8, help_msg[start..], '>') orelse break :blk; - res.id.value = help_msg[start..][0..len]; - if (mem.startsWith(u8, help_msg[start + len + 1 ..], "...")) { - res.takes_value = .Many; - help_msg = help_msg[start + len + 1 + 3 ..]; - } else { - res.takes_value = .One; - help_msg = help_msg[start + len + 1 ..]; - } - } - } + break :blk param_str[2..]; + } else if (found_comma) { + return error.TrailingComma; + } else if (short_name == null) { + return parseParamRest(mem.trimLeft(u8, line, " \t")); + } else null; + + var res = parseParamRest(it.rest()); + res.names.long = param_str[2..]; + res.names.short = short_name; + return res; +} - res.id.msg = mem.trim(u8, help_msg, " \t"); - return res; +fn parseParamRest(line: []const u8) Param(Help) { + if (mem.startsWith(u8, line, "<")) blk: { + const len = mem.indexOfScalar(u8, line, '>') orelse break :blk; + const takes_many = mem.startsWith(u8, line[len + 1 ..], "..."); + const help_start = len + 1 + @as(usize, 3) * @boolToInt(takes_many); + return Param(Help){ + .takes_value = if (takes_many) .Many else .One, + .id = .{ + .msg = mem.trim(u8, line[help_start..], " \t"), + .value = line[1..len], + }, + }; } - return error.NoParamFound; + return Param(Help){ .id = .{ .msg = mem.trim(u8, line, " \t") } }; +} + +fn expectParam(expect: Param(Help), actual: Param(Help)) void { + testing.expectEqualStrings(expect.id.msg, actual.id.msg); + testing.expectEqualStrings(expect.id.value, actual.id.value); + testing.expectEqual(expect.names.short, actual.names.short); + testing.expectEqual(expect.takes_value, actual.takes_value); + if (expect.names.long) |long| { + testing.expectEqualStrings(long, actual.names.long.?); + } else { + testing.expectEqual(@as(?[]const u8, null), actual.names.long); + } } test "parseParam" { - var z: usize = 0; - var text: []const u8 = "-s, --long Help text"; - testing.expectEqual(Param(Help){ + expectParam(Param(Help){ .id = Help{ - .msg = find(text, "Help text"), - .value = find(text, "value"), + .msg = "Help text", + .value = "value", }, .names = Names{ .short = 's', - .long = find(text, "long"), + .long = "long", }, .takes_value = .One, - }, try parseParam(text)); - - text = "-s, --long ... Help text"; - testing.expectEqual(Param(Help){ + }, try parseParam("-s, --long Help text")); + expectParam(Param(Help){ .id = Help{ - .msg = find(text, "Help text"), - .value = find(text, "value"), + .msg = "Help text", + .value = "value", }, .names = Names{ .short = 's', - .long = find(text, "long"), + .long = "long", }, .takes_value = .Many, - }, try parseParam(text)); - - text = "--long Help text"; - testing.expectEqual(Param(Help){ + }, try parseParam("-s, --long ... Help text")); + expectParam(Param(Help){ .id = Help{ - .msg = find(text, "Help text"), - .value = find(text, "value"), + .msg = "Help text", + .value = "value", }, .names = Names{ .short = null, - .long = find(text, "long"), + .long = "long", }, .takes_value = .One, - }, try parseParam(text)); - - text = "-s Help text"; - testing.expectEqual(Param(Help){ + }, try parseParam("--long Help text")); + expectParam(Param(Help){ .id = Help{ - .msg = find(text, "Help text"), - .value = find(text, "value"), + .msg = "Help text", + .value = "value", }, .names = Names{ .short = 's', .long = null, }, .takes_value = .One, - }, try parseParam(text)); - - text = "-s, --long Help text"; - testing.expectEqual(Param(Help){ + }, try parseParam("-s Help text")); + expectParam(Param(Help){ .id = Help{ - .msg = find(text, "Help text"), - .value = text[z..z], + .msg = "Help text", + .value = "", }, .names = Names{ .short = 's', - .long = find(text, "long"), + .long = "long", }, .takes_value = .None, - }, try parseParam(text)); - - text = "-s Help text"; - testing.expectEqual(Param(Help){ + }, try parseParam("-s, --long Help text")); + expectParam(Param(Help){ .id = Help{ - .msg = find(text, "Help text"), - .value = text[z..z], + .msg = "Help text", + .value = "", }, .names = Names{ .short = 's', .long = null, }, .takes_value = .None, - }, try parseParam(text)); - - text = "--long Help text"; - testing.expectEqual(Param(Help){ + }, try parseParam("-s Help text")); + expectParam(Param(Help){ .id = Help{ - .msg = find(text, "Help text"), - .value = text[z..z], + .msg = "Help text", + .value = "", }, .names = Names{ .short = null, - .long = find(text, "long"), + .long = "long", }, .takes_value = .None, - }, try parseParam(text)); - - text = "--long Help text"; - testing.expectEqual(Param(Help){ + }, try parseParam("--long Help text")); + expectParam(Param(Help){ .id = Help{ - .msg = find(text, "Help text"), - .value = find(text, "A | B"), + .msg = "Help text", + .value = "A | B", }, .names = Names{ .short = null, - .long = find(text, "long"), + .long = "long", }, .takes_value = .One, - }, try parseParam(text)); + }, try parseParam("--long Help text")); + expectParam(Param(Help){ + .id = Help{ + .msg = "Help text", + .value = "A", + }, + .names = Names{ + .short = null, + .long = null, + }, + .takes_value = .One, + }, try parseParam(" Help text")); + expectParam(Param(Help){ + .id = Help{ + .msg = "Help text", + .value = "A", + }, + .names = Names{ + .short = null, + .long = null, + }, + .takes_value = .Many, + }, try parseParam("... Help text")); - testing.expectError(error.NoParamFound, parseParam("Help")); testing.expectError(error.TrailingComma, parseParam("--long, Help")); - testing.expectError(error.NoParamFound, parseParam("-s, Help")); + testing.expectError(error.TrailingComma, parseParam("-s, Help")); testing.expectError(error.InvalidShortParam, parseParam("-ss Help")); testing.expectError(error.InvalidShortParam, parseParam("-ss Help")); testing.expectError(error.InvalidShortParam, parseParam("- Help")); } -fn find(str: []const u8, f: []const u8) []const u8 { - const i = mem.indexOf(u8, str, f).?; - return str[i..][0..f.len]; -} - -pub fn Args(comptime Id: type, comptime params: []const Param(Id)) type { - return struct { - arena: std.heap.ArenaAllocator, - clap: ComptimeClap(Id, args.OsIterator, params), - exe_arg: ?[]const u8, - - pub fn deinit(a: *@This()) void { - a.clap.deinit(); - a.arena.deinit(); - } - - pub fn flag(a: @This(), comptime name: []const u8) bool { - return a.clap.flag(name); - } - - pub fn option(a: @This(), comptime name: []const u8) ?[]const u8 { - return a.clap.option(name); - } - - pub fn options(a: @This(), comptime name: []const u8) []const []const u8 { - return a.clap.options(name); - } - - pub fn positionals(a: @This()) []const []const u8 { - return a.clap.positionals(); - } - }; -} - /// Optional diagnostics used for reporting useful errors pub const Diagnostic = struct { arg: []const u8 = "", @@ -319,15 +284,7 @@ fn testDiag(diag: Diagnostic, err: anyerror, expected: []const u8) void { var buf: [1024]u8 = undefined; var slice_stream = io.fixedBufferStream(&buf); diag.report(slice_stream.outStream(), err) catch unreachable; - - const actual = slice_stream.getWritten(); - if (!mem.eql(u8, actual, expected)) { - debug.warn("\n============ Expected ============\n", .{}); - debug.warn("{}", .{expected}); - debug.warn("============= Actual =============\n", .{}); - debug.warn("{}", .{actual}); - testing.expect(false); - } + testing.expectEqualStrings(expected, slice_stream.getWritten()); } test "Diagnostic.report" { @@ -343,6 +300,35 @@ test "Diagnostic.report" { testDiag(.{ .name = .{ .long = "cc" } }, error.SomethingElse, "Error while parsing arguments: SomethingElse\n"); } +pub fn Args(comptime Id: type, comptime params: []const Param(Id)) type { + return struct { + arena: std.heap.ArenaAllocator, + clap: ComptimeClap(Id, args.OsIterator, params), + exe_arg: ?[]const u8, + + pub fn deinit(a: *@This()) void { + a.clap.deinit(); + a.arena.deinit(); + } + + pub fn flag(a: @This(), comptime name: []const u8) bool { + return a.clap.flag(name); + } + + pub fn option(a: @This(), comptime name: []const u8) ?[]const u8 { + return a.clap.option(name); + } + + pub fn options(a: @This(), comptime name: []const u8) []const []const u8 { + return a.clap.options(name); + } + + pub fn positionals(a: @This()) []const []const u8 { + return a.clap.positionals(); + } + }; +} + /// Parses the command line arguments passed into the program based on an /// array of `Param`s. pub fn parse( @@ -422,6 +408,7 @@ fn printParam( try stream.print("--{}", .{l}); } + switch (param.takes_value) { .None => {}, .One => try stream.print(" <{}>", .{valueText(context, param)}), @@ -516,22 +503,7 @@ test "clap.help" { "\t-d, --dd \tBoth option.\n" ++ "\t-d, --dd ...\tBoth repeated option.\n"; - const actual = slice_stream.getWritten(); - if (!mem.eql(u8, actual, expected)) { - debug.warn("\n============ Expected ============\n", .{}); - debug.warn("{}", .{expected}); - debug.warn("============= Actual =============\n", .{}); - debug.warn("{}", .{actual}); - - var buffer: [1024 * 2]u8 = undefined; - var fba = std.heap.FixedBufferAllocator.init(&buffer); - - debug.warn("============ Expected (escaped) ============\n", .{}); - debug.warn("{x}\n", .{expected}); - debug.warn("============ Actual (escaped) ============\n", .{}); - debug.warn("{x}\n", .{actual}); - testing.expect(false); - } + testing.expectEqualStrings(expected, slice_stream.getWritten()); } /// Will print a usage message in the following format: @@ -629,23 +601,7 @@ fn testUsage(expected: []const u8, params: []const Param(Help)) !void { var buf: [1024]u8 = undefined; var fbs = io.fixedBufferStream(&buf); try usage(fbs.outStream(), params); - - const actual = fbs.getWritten(); - if (!mem.eql(u8, actual, expected)) { - debug.warn("\n============ Expected ============\n", .{}); - debug.warn("{}\n", .{expected}); - debug.warn("============= Actual =============\n", .{}); - debug.warn("{}\n", .{actual}); - - var buffer: [1024 * 2]u8 = undefined; - var fba = std.heap.FixedBufferAllocator.init(&buffer); - - debug.warn("============ Expected (escaped) ============\n", .{}); - debug.warn("{x}\n", .{expected}); - debug.warn("============ Actual (escaped) ============\n", .{}); - debug.warn("{x}\n", .{actual}); - testing.expect(false); - } + testing.expectEqualStrings(expected, fbs.getWritten()); } test "usage" { diff --git a/clap/comptime.zig b/clap/comptime.zig index df5547a..9ead41a 100644 --- a/clap/comptime.zig +++ b/clap/comptime.zig @@ -152,9 +152,7 @@ test "" { clap.parseParam("-b, --bb ") catch unreachable, clap.parseParam("-c, --cc ") catch unreachable, clap.parseParam("-d, --dd ...") catch unreachable, - clap.Param(clap.Help){ - .takes_value = .One, - }, + clap.parseParam("

") catch unreachable, }); var buf: [1024]u8 = undefined; diff --git a/clap/streaming.zig b/clap/streaming.zig index 8ab01f6..e0b808e 100644 --- a/clap/streaming.zig +++ b/clap/streaming.zig @@ -208,15 +208,7 @@ fn testErr(params: []const clap.Param(u8), args_strings: []const []const u8, exp var buf: [1024]u8 = undefined; var slice_stream = io.fixedBufferStream(&buf); diag.report(slice_stream.outStream(), err) catch unreachable; - - const actual = slice_stream.getWritten(); - if (!mem.eql(u8, actual, expected)) { - debug.warn("\n============ Expected ============\n", .{}); - debug.warn("{}", .{expected}); - debug.warn("============= Actual =============\n", .{}); - debug.warn("{}", .{actual}); - testing.expect(false); - } + testing.expectEqualStrings(expected, slice_stream.getWritten()); return; }) |_| {} diff --git a/example/comptime-clap.zig b/example/comptime-clap.zig index 530c7e6..e5d02ff 100644 --- a/example/comptime-clap.zig +++ b/example/comptime-clap.zig @@ -12,9 +12,7 @@ pub fn main() !void { clap.parseParam("-h, --help Display this help and exit. ") catch unreachable, clap.parseParam("-n, --number An option parameter, which takes a value.") catch unreachable, clap.parseParam("-s, --string ... An option parameter which can be specified multiple times.") catch unreachable, - clap.Param(clap.Help){ - .takes_value = .One, - }, + clap.parseParam("...") catch unreachable, }; const Clap = clap.ComptimeClap(clap.Help, clap.args.OsIterator, ¶ms); diff --git a/example/simple.zig b/example/simple.zig index f7b5953..270e344 100644 --- a/example/simple.zig +++ b/example/simple.zig @@ -10,9 +10,7 @@ pub fn main() !void { clap.parseParam("-h, --help Display this help and exit. ") catch unreachable, clap.parseParam("-n, --number An option parameter, which takes a value.") catch unreachable, clap.parseParam("-s, --string ... An option parameter which can be specified multiple times.") catch unreachable, - clap.Param(clap.Help){ - .takes_value = .One, - }, + clap.parseParam("...") catch unreachable, }; // Initalize our diagnostics, which can be used for reporting useful errors. -- cgit v1.2.3