From 5c7016f2bc10c0f964fce5c4d9e7210db5786da7 Mon Sep 17 00:00:00 2001 From: Jimmi HC Date: Fri, 1 Jun 2018 13:37:40 +0200 Subject: Reworked extended.zig again! --- src/core.zig | 6 +- src/extended.zig | 303 +++++++++++++------------------- tests/extended.zig | 494 +++++++++++++++++++++++++++++++++-------------------- 3 files changed, 429 insertions(+), 374 deletions(-) diff --git a/src/core.zig b/src/core.zig index 5e47714..f2e1fe0 100644 --- a/src/core.zig +++ b/src/core.zig @@ -266,8 +266,10 @@ pub fn Clap(comptime Id: type, comptime ArgError: type) type { kind = ArgInfo.Kind.Short; } - if (arg.len == 0) - return error.ArgWithNoName; + // We allow long arguments to go without a name. + // This allows the user to use "--" for something important + if (kind != ArgInfo.Kind.Long and arg.len == 0) + return error.InvalidArgument; break :blk ArgInfo { .arg = arg, .kind = kind }; }; diff --git a/src/extended.zig b/src/extended.zig index 31e6455..ffcce5b 100644 --- a/src/extended.zig +++ b/src/extended.zig @@ -10,206 +10,34 @@ const io = std.io; const assert = debug.assert; -const Opaque = @OpaqueType(); - pub const Param = struct { field: []const u8, - short: ?u8, - long: ?[]const u8, - takes_value: ?Parser, + names: core.Names, + kind: Kind, required: bool, position: ?usize, - pub fn short(s: u8) Param { - return Param{ - .field = []u8{s}, - .short = s, - .long = null, - .takes_value = null, - .required = false, - .position = null, - }; - } - - pub fn long(l: []const u8) Param { - return Param{ - .field = l, - .short = null, - .long = l, - .takes_value = null, - .required = false, - .position = null, - }; - } - - pub fn value(f: []const u8) Param { - return Param{ - .field = f, - .short = null, - .long = null, - .takes_value = null, - .required = false, - .position = null, - }; - } - - /// Initialize a ::Param. - /// If ::name.len == 0, then it's a value parameter: "value". - /// If ::name.len == 1, then it's a short parameter: "-s". - /// If ::name.len > 1, then it's a long parameter: "--long". - pub fn smart(name: []const u8) Param { - return Param{ - .field = name, - .short = if (name.len == 1) name[0] else null, - .long = if (name.len > 1) name else null, - .takes_value = null, - .required = false, - .position = null, - }; - } - - pub fn with(param: &const Param, comptime field_name: []const u8, v: var) Param { - var res = param.*; - @field(res, field_name) = v; - return res; - } + pub const Kind = union(enum) { + Flag, + Option: Parser, + SubCommand: Command, + }; }; +const Opaque = @OpaqueType(); pub const Command = struct { - field: []const u8, - name: []const u8, params: []const Param, - sub_commands: []const Command, Result: type, - defaults: &const Opaque, - parent: ?&const Command, + default: &const Opaque, - pub fn init(name: []const u8, comptime Result: type, defaults: &const Result, params: []const Param, sub_commands: []const Command) Command { + pub fn init(comptime Result: type, default: &const Result, params: []const Param) Command { return Command{ - .field = name, - .name = name, .params = params, - .sub_commands = sub_commands, .Result = Result, - .defaults = @ptrCast(&const Opaque, defaults), - .parent = null, + .default = @ptrCast(&const Opaque, default), }; } - - pub fn with(command: &const Command, comptime field_name: []const u8, v: var) Param { - var res = command.*; - @field(res, field_name) = v; - return res; - } - - pub fn parse(comptime command: &const Command, allocator: &mem.Allocator, arg_iter: var) !command.Result { - const Parent = struct {}; - var parent = Parent{}; - return command.parseHelper(&parent, allocator, arg_iter); - } - - fn parseHelper(comptime command: &const Command, parent: var, allocator: &mem.Allocator, arg_iter: var) !command.Result { - const Result = struct { - parent: @typeOf(parent), - result: command.Result, - }; - - var result = Result{ - .parent = parent, - .result = @ptrCast(&const command.Result, command.defaults).*, - }; - - // In order for us to wrap the core api, we have to translate clap.Param into core.Param. - const core_params = comptime blk: { - var res: [command.params.len + command.sub_commands.len]core.Param(usize) = undefined; - - for (command.params) |p, i| { - const id = i; - res[id] = core.Param(usize) { - .id = id, - .takes_value = p.takes_value != null, - .names = core.Names{ - .bare = null, - .short = p.short, - .long = p.long, - }, - }; - } - - for (command.sub_commands) |c, i| { - const id = i + command.params.len; - res[id] = core.Param(usize) { - .id = id, - .takes_value = false, - .names = core.Names.bare(c.name), - }; - } - - break :blk res; - }; - - var handled = comptime blk: { - var res: [command.params.len]bool = undefined; - for (command.params) |p, i| { - res[i] = !p.required; - } - - break :blk res; - }; - - var pos: usize = 0; - var clap = core.Clap(usize, @typeOf(arg_iter.*).Error).init(core_params, arg_iter); - - arg_loop: - while (try clap.next()) |arg| : (pos += 1) { - inline for(command.params) |param, i| { - comptime const field = "result." ++ param.field; - - if (arg.param.id == i and (param.position ?? pos) == pos) { - if (param.takes_value) |parser| { - try parser.parse(getFieldPtr(&result, field), ??arg.value); - } else { - getFieldPtr(&result, field).* = true; - } - handled[i] = true; - continue :arg_loop; - } - } - - inline for(command.sub_commands) |c, i| { - comptime const field = "result." ++ c.field; - comptime var sub_command = c; - sub_command.parent = command; - - if (arg.param.id == i + command.params.len) { - getFieldPtr(&result, field).* = try sub_command.parseHelper(&result, allocator, arg_iter); - continue :arg_loop; - } - } - - return error.InvalidArgument; - } - - return result.result; - } - - fn GetFieldPtrReturn(comptime Struct: type, comptime field: []const u8) type { - var inst: Struct = undefined; - const dot_index = comptime mem.indexOfScalar(u8, field, '.') ?? { - return @typeOf(&@field(inst, field)); - }; - - return GetFieldPtrReturn(@typeOf(@field(inst, field[0..dot_index])), field[dot_index + 1..]); - } - - fn getFieldPtr(curr: var, comptime field: []const u8) GetFieldPtrReturn(@typeOf(curr).Child, field) { - const dot_index = comptime mem.indexOfScalar(u8, field, '.') ?? { - return &@field(curr, field); - }; - - return getFieldPtr(&@field(curr, field[0..dot_index]), field[dot_index + 1..]); - } }; pub const Parser = struct { @@ -261,3 +89,112 @@ pub const Parser = struct { }.s ); }; + +pub fn Clap(comptime Result: type) type { + return struct { + const Self = this; + + default: Result, + params: []const Param, + + pub fn parse( + comptime clap: &const Self, + comptime Error: type, + iter: &core.ArgIterator(Error), + ) !Result { + // We initialize the core.Clap without any params, and fill them out in parseHelper. + var c = core.Clap(usize, Error).init([]core.Param(usize){}, iter); + + const top_level_command = comptime Command.init(Result, &clap.default, clap.params); + return try parseHelper(top_level_command, Error, &c); + } + + fn parseHelper( + comptime command: &const Command, + comptime Error: type, + clap: &core.Clap(usize, Error), + ) !command.Result { + var result = @ptrCast(&const command.Result, command.default).*; + + var handled = comptime blk: { + var res: [command.params.len]bool = undefined; + for (command.params) |p, i| { + res[i] = !p.required; + } + + break :blk res; + }; + + // We replace the current clap with the commands parameters, so that we preserve the that + // claps state. This is important, as core.Clap could be in a Chaining state, and + // constructing a new core.Clap would skip the last chaining arguments. + clap.params = comptime blk: { + var res: [command.params.len]core.Param(usize) = undefined; + + for (command.params) |p, i| { + const id = i; + res[id] = core.Param(usize) { + .id = id, + .takes_value = p.kind == Param.Kind.Option, + .names = p.names, + }; + } + + break :blk res; + }; + + var pos: usize = 0; + + arg_loop: + while (try clap.next()) |arg| : (pos += 1) { + inline for(command.params) |param, i| { + if (arg.param.id == i and (param.position ?? pos) == pos) { + handled[i] = true; + + switch (param.kind) { + Param.Kind.Flag => { + getFieldPtr(&result, param.field).* = true; + }, + Param.Kind.Option => |parser| { + try parser.parse(getFieldPtr(&result, param.field), ??arg.value); + }, + Param.Kind.SubCommand => |sub_command| { + getFieldPtr(&result, param.field).* = try sub_command.parseHelper(Error, clap); + + // After parsing a subcommand, there should be no arguments left. + break :arg_loop; + }, + } + continue :arg_loop; + } + } + + return error.InvalidArgument; + } + + for (handled) |h| { + if (!h) + return error.ParamNotHandled; + } + + return result; + } + + fn GetFieldPtrReturn(comptime Struct: type, comptime field: []const u8) type { + var inst: Struct = undefined; + const dot_index = comptime mem.indexOfScalar(u8, field, '.') ?? { + return @typeOf(&@field(inst, field)); + }; + + return GetFieldPtrReturn(@typeOf(@field(inst, field[0..dot_index])), field[dot_index + 1..]); + } + + fn getFieldPtr(curr: var, comptime field: []const u8) GetFieldPtrReturn(@typeOf(curr).Child, field) { + const dot_index = comptime mem.indexOfScalar(u8, field, '.') ?? { + return &@field(curr, field); + }; + + return getFieldPtr(&@field(curr, field[0..dot_index]), field[dot_index + 1..]); + } + }; +} diff --git a/tests/extended.zig b/tests/extended.zig index 8f722e4..140c822 100644 --- a/tests/extended.zig +++ b/tests/extended.zig @@ -9,226 +9,342 @@ const extended = clap.extended; const assert = debug.assert; const ArgSliceIterator = core.ArgSliceIterator; -const Command = extended.Command; +const Names = core.Names; +const Clap = extended.Clap; const Param = extended.Param; const Parser = extended.Parser; -const Options = struct { - str: []const u8, - int: i64, - uint: u64, - a: bool, - b: bool, - cc: bool, - sub: &const SubOptions, - - pub fn with(op: &const Options, comptime field: []const u8, value: var) Options { - var res = op.*; - @field(res, field) = value; - return res; - } -}; - -const SubOptions = struct { - a: bool, - b: u64, - qq: bool, +fn success(comptime parser: var, expect: var, args: []const []const u8) void { + var iter = ArgSliceIterator.init(args); + const actual = parser.parse(ArgSliceIterator.Error, &iter.iter) catch unreachable; - pub fn with(op: &const SubOptions, comptime field: []const u8, value: var) SubOptions { - var res = op.*; - @field(res, field) = value; - return res; + const T = @typeOf(expect).Child; + inline for (@typeInfo(T).Struct.fields) |field| { + assert(@field(expect, field.name) == @field(actual, field.name)); } -}; - -const default = Options { - .str = "", - .int = 0, - .uint = 0, - .a = false, - .b = false, - .cc = false, - .sub = SubOptions{ - .a = false, - .b = 0, - .qq = false, - }, -}; - -fn testNoErr(comptime command: &const Command, args: []const []const u8, expected: &const command.Result) void { - var arg_iter = ArgSliceIterator.init(args); - const actual = command.parse(debug.global_allocator, &arg_iter.iter) catch unreachable; - assert(mem.eql(u8, expected.str, actual.str)); - assert(expected.int == actual.int); - assert(expected.uint == actual.uint); - assert(expected.a == actual.a); - assert(expected.b == actual.b); - assert(expected.cc == actual.cc); - assert(expected.sub.a == actual.sub.a); - assert(expected.sub.b == actual.sub.b); } -fn testErr(comptime command: &const Command, args: []const []const u8, expected: error) void { - var arg_iter = ArgSliceIterator.init(args); - if (command.parse(debug.global_allocator, &arg_iter.iter)) |actual| { +fn fail(comptime parser: var, expect: error, args: []const []const u8) void { + var iter = ArgSliceIterator.init(args); + if (parser.parse(ArgSliceIterator.Error, &iter.iter)) |_| { unreachable; - } else |err| { - assert(err == expected); + } else |actual| { + assert(expect == actual); } } -test "clap.extended: short" { - const command = comptime Command.init( - "", - Options, - default, - []Param { - Param.smart("a"), - Param.smart("b"), - Param.smart("int") - .with("short", 'i') - .with("takes_value", Parser.int(i64, 10)), - }, - []Command{}, - ); - - testNoErr(command, [][]const u8 { "-a" }, default.with("a", true)); - testNoErr(command, [][]const u8 { "-a", "-b" }, default.with("a", true).with("b", true)); - testNoErr(command, [][]const u8 { "-i=100" }, default.with("int", 100)); - testNoErr(command, [][]const u8 { "-i100" }, default.with("int", 100)); - testNoErr(command, [][]const u8 { "-i", "100" }, default.with("int", 100)); - testNoErr(command, [][]const u8 { "-ab" }, default.with("a", true).with("b", true)); - testNoErr(command, [][]const u8 { "-abi", "100" }, default.with("a", true).with("b", true).with("int", 100)); - testNoErr(command, [][]const u8 { "-abi=100" }, default.with("a", true).with("b", true).with("int", 100)); - testNoErr(command, [][]const u8 { "-abi100" }, default.with("a", true).with("b", true).with("int", 100)); -} +pub fn Test(comptime Expect: type) type { + return struct { + const Self = this; -test "clap.extended: long" { - const command = comptime Command.init( - "", - Options, - default, - []Param { - Param.smart("cc"), - Param.smart("int").with("takes_value", Parser.int(i64, 10)), - Param.smart("uint").with("takes_value", Parser.int(u64, 10)), - Param.smart("str").with("takes_value", Parser.string), - }, - []Command{}, - ); + args: []const []const u8, + kind: Kind, - testNoErr(command, [][]const u8 { "--cc" }, default.with("cc", true)); - testNoErr(command, [][]const u8 { "--int", "100" }, default.with("int", 100)); -} + const Kind = union(enum) { + Success: Expect, + Fail: error, + }; -test "clap.extended: value bool" { - const command = comptime Command.init( - "", - Options, - default, - []Param { - Param.smart("a"), - }, - []Command{}, - ); + pub fn success(args: []const []const u8, expected: &const Expect) Self { + return Self{ + .args = args, + .kind = Kind{ + .Success = expected.*, + }, + }; + } - testNoErr(command, [][]const u8 { "-a" }, default.with("a", true)); -} + pub fn fail(args: []const []const u8, err: error) Self { + return Self{ + .args = args, + .kind = Kind{ + .Fail = err, + }, + }; + } -test "clap.extended: value str" { - const command = comptime Command.init( - "", - Options, - default, - []Param { - Param.smart("str").with("takes_value", Parser.string), - }, - []Command{}, - ); + pub fn run(t: &const Self, comptime parser: var) void { + var iter = ArgSliceIterator.init(t.args); + const actual = parser.parse(ArgSliceIterator.Error, &iter.iter); - testNoErr(command, [][]const u8 { "--str", "Hello World!" }, default.with("str", "Hello World!")); + switch (t.kind) { + Kind.Success => |expected| { + const actual_value = actual catch unreachable; + inline for (@typeInfo(Expect).Struct.fields) |field| { + assert(@field(expected, field.name) == @field(actual_value, field.name)); + } + }, + Kind.Fail => |expected| { + if (actual) |_| { + unreachable; + } else |actual_err| { + assert(actual_err == expected); + } + }, + } + } + }; } -test "clap.extended: value int" { - const command = comptime Command.init( - "", - Options, - default, - []Param { - Param.smart("int").with("takes_value", Parser.int(i64, 10)), - }, - []Command{}, - ); - - testNoErr(command, [][]const u8 { "--int", "100" }, default.with("int", 100)); -} +test "clap.extended: short" { + const S = struct { + a: bool, + b: u8, + }; -test "clap.extended: position" { - const command = comptime Command.init( - "", - Options, - default, - []Param { - Param.smart("a").with("position", 0), - Param.smart("b").with("position", 1), + const parser = comptime Clap(S){ + .default = S{ + .a = false, + .b = 0, }, - []Command{}, - ); + .params = []Param{ + Param{ + .field = "a", + .names = Names.short('a'), + .kind = Param.Kind.Flag, + .required = true, + .position = 0, + }, + Param{ + .field = "b", + .names = Names.short('b'), + .kind = Param.Kind{ .Option = Parser.int(u8, 10) }, + .required = false, + .position = null, + }, + } + }; + + const T = Test(S); + const tests = []T{ + T.success( + [][]const u8 { "-a" }, + S{ + .a = true, + .b = 0, + }, + ), + T.success( + [][]const u8 { "-a", "-b", "100" }, + S{ + .a = true, + .b = 100, + }, + ), + T.success( + [][]const u8 { "-a", "-b=100" }, + S{ + .a = true, + .b = 100, + }, + ), + T.success( + [][]const u8 { "-a", "-b100" }, + S{ + .a = true, + .b = 100, + }, + ), + T.success( + [][]const u8 { "-ab", "100" }, + S{ + .a = true, + .b = 100, + }, + ), + T.success( + [][]const u8 { "-ab=100" }, + S{ + .a = true, + .b = 100, + }, + ), + T.success( + [][]const u8 { "-ab100" }, + S{ + .a = true, + .b = 100, + }, + ), + T.fail( + [][]const u8 { "-q" }, + error.InvalidArgument, + ), + T.fail( + [][]const u8 { "--a" }, + error.InvalidArgument, + ), + T.fail( + [][]const u8 { "-b=100" }, + error.ParamNotHandled, + ), + T.fail( + [][]const u8 { "-b=100", "-a" }, + error.InvalidArgument, + ), + }; - testNoErr(command, [][]const u8 { "-a", "-b" }, default.with("a", true).with("b", true)); - testErr(command, [][]const u8 { "-b", "-a" }, error.InvalidArgument); + for (tests) |t| { + t.run(parser); + } } -test "clap.extended: sub fields" { - const B = struct { +test "clap.extended: long" { + const S = struct { a: bool, - }; - const A = struct { - b: B, + b: u8, }; - const command = comptime Command.init( - "", - A, - A { .b = B { .a = false } }, - []Param { - Param.short('a') - .with("field", "b.a"), + const parser = comptime Clap(S){ + .default = S{ + .a = false, + .b = 0, }, - []Command{}, - ); + .params = []Param{ + Param{ + .field = "a", + .names = Names.long("a"), + .kind = Param.Kind.Flag, + .required = true, + .position = 0, + }, + Param{ + .field = "b", + .names = Names.long("b"), + .kind = Param.Kind{ .Option = Parser.int(u8, 10) }, + .required = false, + .position = null, + }, + } + }; - var arg_iter = ArgSliceIterator.init([][]const u8{ "-a" }); - const res = command.parse(debug.global_allocator, &arg_iter.iter) catch unreachable; - debug.assert(res.b.a == true); + const T = Test(S); + const tests = []T{ + T.success( + [][]const u8 { "--a" }, + S{ + .a = true, + .b = 0, + }, + ), + T.success( + [][]const u8 { "--a", "--b", "100" }, + S{ + .a = true, + .b = 100, + }, + ), + T.success( + [][]const u8 { "--a", "--b=100" }, + S{ + .a = true, + .b = 100, + }, + ), + T.fail( + [][]const u8 { "--a=100" }, + error.DoesntTakeValue, + ), + T.fail( + [][]const u8 { "--q" }, + error.InvalidArgument, + ), + T.fail( + [][]const u8 { "-a" }, + error.InvalidArgument, + ), + T.fail( + [][]const u8 { "--b=100" }, + error.ParamNotHandled, + ), + T.fail( + [][]const u8 { "--b=100", "--a" }, + error.InvalidArgument, + ), + }; + + for (tests) |t| { + t.run(parser); + } } -test "clap.extended: sub commands" { - const command = comptime Command.init( - "", - Options, - default, - []Param { - Param.smart("a"), - Param.smart("b"), - }, - []Command{ - Command.init( - "sub", - SubOptions, - default.sub, - []Param { - Param.smart("a"), - Param.smart("b") - .with("takes_value", Parser.int(u64, 10)), - }, - []Command{}, - ), +test "clap.extended: bare" { + const S = struct { + a: bool, + b: u8, + }; + + const parser = comptime Clap(S){ + .default = S{ + .a = false, + .b = 0, }, - ); + .params = []Param{ + Param{ + .field = "a", + .names = Names.bare("a"), + .kind = Param.Kind.Flag, + .required = true, + .position = 0, + }, + Param{ + .field = "b", + .names = Names.bare("b"), + .kind = Param.Kind{ .Option = Parser.int(u8, 10) }, + .required = false, + .position = null, + }, + } + }; + + const T = Test(S); + const tests = []T{ + T.success( + [][]const u8 { "a" }, + S{ + .a = true, + .b = 0, + }, + ), + T.success( + [][]const u8 { "a", "b", "100" }, + S{ + .a = true, + .b = 100, + }, + ), + T.success( + [][]const u8 { "a", "b=100" }, + S{ + .a = true, + .b = 100, + }, + ), + T.fail( + [][]const u8 { "a=100" }, + error.DoesntTakeValue, + ), + T.fail( + [][]const u8 { "--a" }, + error.InvalidArgument, + ), + T.fail( + [][]const u8 { "-a" }, + error.InvalidArgument, + ), + T.fail( + [][]const u8 { "b=100" }, + error.ParamNotHandled, + ), + T.fail( + [][]const u8 { "b=100", "--a" }, + error.InvalidArgument, + ), + }; - testNoErr(command, [][]const u8 { "sub", "-a" }, default.with("sub", default.sub.with("a", true))); - testNoErr(command, [][]const u8 { "sub", "-b", "100" }, default.with("sub", default.sub.with("b", 100))); - testNoErr(command, [][]const u8 { "-a", "sub", "-a" }, default.with("a", true).with("sub", default.sub.with("a", true))); - testErr(command, [][]const u8 { "-qq", "sub" }, error.InvalidArgument); + for (tests) |t| { + t.run(parser); + } } + +// TODO: Test sub commands and sub field access -- cgit v1.2.3