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