Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Report error context in Diagnostic #26

Merged
merged 2 commits into from Nov 2, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
38 changes: 32 additions & 6 deletions README.md
Expand Up @@ -42,7 +42,16 @@ pub fn main() !void {
},
};

var args = try clap.parse(clap.Help, &params, 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, &params, 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"))
Expand All @@ -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, &params, std.heap.direct_allocator);
var args = try clap.parse(clap.Help, &params, std.heap.direct_allocator, null);
defer args.deinit();

_ = args.flag("--helps");
Expand Down Expand Up @@ -118,14 +125,24 @@ pub fn main() !void {
.takes_value = .One,
},
};
const Clap = clap.ComptimeClap(clap.Help, clap.args.OsIterator, &params);

// 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, &params).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"))
Expand Down Expand Up @@ -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", .{}),
Expand Down
51 changes: 49 additions & 2 deletions clap.zig
Expand Up @@ -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 {
Expand All @@ -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,
Expand Down
14 changes: 9 additions & 5 deletions clap/comptime.zig
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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.?);
Expand Down Expand Up @@ -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 <V>") catch unreachable,
Expand All @@ -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"));
Expand Down
84 changes: 45 additions & 39 deletions clap/streaming.zig
Expand Up @@ -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,
Expand All @@ -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 };
}
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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,
},
Expand All @@ -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 };
}

Expand All @@ -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;
}
};
}
Expand All @@ -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);
Expand All @@ -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;
}

Expand Down