From f4e53cd6149ed4dcbfd8b81a6427b1b652d0a472 Mon Sep 17 00:00:00 2001 From: Jimmi HC Date: Thu, 31 May 2018 16:00:44 +0200 Subject: Started work on the proper structure for the lib --- build.zig | 46 +++++ core.zig | 493 -------------------------------------------------- example.zig | 68 ------- examples/core.zig | 0 examples/extended.zig | 0 index.zig | 493 +------------------------------------------------- src/core.zig | 372 +++++++++++++++++++++++++++++++++++++ src/extended.zig | 264 +++++++++++++++++++++++++++ tests/core.zig | 134 ++++++++++++++ tests/extended.zig | 234 ++++++++++++++++++++++++ 10 files changed, 1052 insertions(+), 1052 deletions(-) create mode 100644 build.zig delete mode 100644 core.zig delete mode 100644 example.zig create mode 100644 examples/core.zig create mode 100644 examples/extended.zig create mode 100644 src/core.zig create mode 100644 src/extended.zig create mode 100644 tests/core.zig create mode 100644 tests/extended.zig diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..847a3a2 --- /dev/null +++ b/build.zig @@ -0,0 +1,46 @@ +const Builder = @import("std").build.Builder; + +pub fn build(b: &Builder) void { + const mode = b.standardReleaseOptions(); + + { + const example_step = b.step("examples", "Build all examples"); + const examples = [][]const u8 { + "core", + "extended", + }; + + b.default_step.dependOn(example_step); + inline for (examples) |example| { + comptime const path = "examples/" ++ example ++ ".zig"; + const exe = b.addExecutable(example, path); + exe.setBuildMode(mode); + exe.addPackagePath("clap", "index.zig"); + + const step = b.step("build-" ++ example, "Build '" ++ path ++ "'"); + step.dependOn(&exe.step); + example_step.dependOn(step); + } + } + + { + const test_step = b.step("tests", "Run all tests"); + const tests = [][]const u8 { + "core", + "extended", + }; + + b.default_step.dependOn(test_step); + inline for (tests) |test_name| { + comptime const path = "tests/" ++ test_name ++ ".zig"; + const t = b.addTest(path); + t.setBuildMode(mode); + //t.addPackagePath("clap", "index.zig"); + + const step = b.step("test-" ++ test_name, "Run test '" ++ test_name ++ "'"); + step.dependOn(&t.step); + test_step.dependOn(step); + } + + } +} diff --git a/core.zig b/core.zig deleted file mode 100644 index 306ff63..0000000 --- a/core.zig +++ /dev/null @@ -1,493 +0,0 @@ -const std = @import("std"); -const builtin = @import("builtin"); - -const os = std.os; -const heap = std.heap; -const mem = std.mem; -const debug = std.debug; - -/// The names a ::Param can have. -pub const Names = struct { - /// No prefix - bare: ?[]const u8, - - /// '-' prefix - short: ?u8, - - /// '--' prefix - long: ?[]const u8, - - /// Initializes no names - pub fn none() Names { - return Names{ - .bare = null, - .short = null, - .long = null, - }; - } - - /// Initializes a bare name - pub fn bare(b: []const u8) Names { - return Names{ - .bare = b, - .short = null, - .long = null, - }; - } - - /// Initializes a short name - pub fn short(s: u8) Names { - return Names{ - .bare = null, - .short = s, - .long = null, - }; - } - - /// Initializes a long name - pub fn long(l: []const u8) Names { - return Names{ - .bare = null, - .short = null, - .long = l, - }; - } - - /// Initializes a name with a prefix. - /// ::short is set to ::name[0], and ::long is set to ::name. - /// This function asserts that ::name.len != 0 - pub fn prefix(name: []const u8) Names { - debug.assert(name.len != 0); - - return Names{ - .bare = null, - .short = name[0], - .long = name, - }; - } -}; - -/// 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. -/// * They can take a value three different ways. -/// * "-a value" -/// * "-a=value" -/// * "-avalue" -/// * They chain if they don't take values: "-abc". -/// * The last given parameter can take a value in the same way that a single parameter can: -/// * "-abc value" -/// * "-abc=value" -/// * "-abcvalue" -/// * Long ("--long-param"): Should be used for less common parameters, or when no single character -/// can describe the paramter. -/// * They can take a value two different ways. -/// * "--long-param value" -/// * "--long-param=value" -/// * Bare ("bare"): Should be used as for sub-commands and other keywords. -/// * They can take a value two different ways. -/// * "command value" -/// * "command=value" -/// * Value ("value"): Should be used as the primary parameter of the program, like a filename or -/// an expression to parse. -/// * Value parameters must take a value. -pub fn Param(comptime Id: type) type { - return struct { - const Self = this; - - id: Id, - takes_value: bool, - names: Names, - - pub fn init(id: Id, takes_value: bool, names: &const Names) Self { - // Assert, that if the param have no name, then it has to take - // a value. - debug.assert( - names.bare != null or - names.long != null or - names.short != null or - takes_value - ); - - return Self{ - .id = id, - .takes_value = takes_value, - .names = names.*, - }; - } - }; -} - -/// The result returned from ::Clap.next -pub fn Arg(comptime Id: type) type { - return struct { - const Self = this; - - id: Id, - value: ?[]const u8, - - pub fn init(id: Id, value: ?[]const u8) Self { - return Self { - .id = id, - .value = value, - }; - } - }; -} - -/// A interface for iterating over command line arguments -pub const ArgIterator = struct { - const Error = error{OutOfMemory}; - - nextFn: fn(iter: &ArgIterator, allocator: &mem.Allocator) Error!?[]const u8, - - pub fn next(iter: &ArgIterator, allocator: &mem.Allocator) Error!?[]const u8 { - return iter.nextFn(iter, allocator); - } -}; - -/// An ::ArgIterator, which iterates over a slice of arguments. -/// This implementation does not allocate. -pub const ArgSliceIterator = struct { - args: []const []const u8, - index: usize, - iter: ArgIterator, - - pub fn init(args: []const []const u8) ArgSliceIterator { - return ArgSliceIterator { - .args = args, - .index = 0, - .iter = ArgIterator { - .nextFn = nextFn, - }, - }; - } - - fn nextFn(iter: &ArgIterator, allocator: &mem.Allocator) ArgIterator.Error!?[]const u8 { - const self = @fieldParentPtr(ArgSliceIterator, "iter", iter); - if (self.args.len <= self.index) - return null; - - defer self.index += 1; - return self.args[self.index]; - } -}; - -/// An ::ArgIterator, which wraps the ArgIterator in ::std. -/// On windows, this iterator allocates. -pub const OsArgIterator = struct { - args: os.ArgIterator, - iter: ArgIterator, - - pub fn init() OsArgIterator { - return OsArgIterator { - .args = os.args(), - .iter = ArgIterator { - .nextFn = nextFn, - }, - }; - } - - fn nextFn(iter: &ArgIterator, allocator: &mem.Allocator) ArgIterator.Error!?[]const u8 { - const self = @fieldParentPtr(OsArgIterator, "iter", iter); - if (builtin.os == builtin.Os.windows) { - return try self.args.next(allocator) ?? return null; - } else { - return self.args.nextPosix(); - } - } -}; - -/// A command line argument parser which, given an ::ArgIterator, will parse arguments according -/// to the ::params. ::Clap parses in an iterating manner, so you have to use a loop together with -/// ::Clap.next to parse all the arguments of your program. -pub fn Clap(comptime Id: type) type { - return struct { - const Self = this; - - const State = union(enum) { - Normal, - Chaining: Chaining, - - const Chaining = struct { - arg: []const u8, - index: usize, - }; - }; - - arena: heap.ArenaAllocator, - params: []const Param(Id), - inner: &ArgIterator, - state: State, - - pub fn init(params: []const Param(Id), inner: &ArgIterator, allocator: &mem.Allocator) Self { - var res = Self { - .arena = heap.ArenaAllocator.init(allocator), - .params = params, - .inner = inner, - .state = State.Normal, - }; - - return res; - } - - pub fn deinit(iter: &Self) void { - iter.arena.deinit(); - } - - /// Get the next ::Arg that matches a ::Param. - pub fn next(iter: &Self) !?Arg(Id) { - const ArgInfo = struct { - const Kind = enum { Long, Short, Bare }; - - arg: []const u8, - kind: Kind, - }; - - switch (iter.state) { - State.Normal => { - const full_arg = (try iter.innerNext()) ?? return null; - const arg_info = blk: { - var arg = full_arg; - var kind = ArgInfo.Kind.Bare; - - if (mem.startsWith(u8, arg, "--")) { - arg = arg[2..]; - kind = ArgInfo.Kind.Long; - } else if (mem.startsWith(u8, arg, "-")) { - arg = arg[1..]; - kind = ArgInfo.Kind.Short; - } - - if (arg.len == 0) - return error.ArgWithNoName; - - break :blk ArgInfo { .arg = arg, .kind = kind }; - }; - - const arg = arg_info.arg; - const kind = arg_info.kind; - const eql_index = mem.indexOfScalar(u8, arg, '='); - - switch (kind) { - ArgInfo.Kind.Bare, - ArgInfo.Kind.Long => { - for (iter.params) |*param| { - const match = switch (kind) { - ArgInfo.Kind.Bare => param.names.bare ?? continue, - ArgInfo.Kind.Long => param.names.long ?? continue, - else => unreachable, - }; - 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) { - if (maybe_value != null) - return error.DoesntTakeValue; - - return Arg(Id).init(param.id, null); - } - - const value = blk: { - if (maybe_value) |v| - break :blk v; - - break :blk (try iter.innerNext()) ?? return error.MissingValue; - }; - - return Arg(Id).init(param.id, value); - } - }, - ArgInfo.Kind.Short => { - return try iter.chainging(State.Chaining { - .arg = full_arg, - .index = (full_arg.len - arg.len), - }); - }, - } - - // We do a final pass to look for value parameters matches - if (kind == ArgInfo.Kind.Bare) { - for (iter.params) |*param| { - if (param.names.bare) |_| continue; - if (param.names.short) |_| continue; - if (param.names.long) |_| continue; - - return Arg(Id).init(param.id, arg); - } - } - - return error.InvalidArgument; - }, - @TagType(State).Chaining => |state| return try iter.chainging(state), - } - } - - fn chainging(iter: &Self, state: &const State.Chaining) !?Arg(Id) { - const arg = state.arg; - const index = state.index; - const next_index = index + 1; - - for (iter.params) |param| { - const short = param.names.short ?? continue; - if (short != arg[index]) - continue; - - // Before we return, we have to set the new state of the iterator - defer { - if (arg.len <= next_index or param.takes_value) { - iter.state = State.Normal; - } else { - iter.state = State { .Chaining = State.Chaining { - .arg = arg, - .index = next_index, - }}; - } - } - - if (!param.takes_value) - return Arg(Id).init(param.id, null); - - if (arg.len <= next_index) { - const value = (try iter.innerNext()) ?? return error.MissingValue; - return Arg(Id).init(param.id, value); - } - - if (arg[next_index] == '=') { - return Arg(Id).init(param.id, arg[next_index + 1..]); - } - - return Arg(Id).init(param.id, arg[next_index..]); - } - - return error.InvalidArgument; - } - - fn innerNext(iter: &Self) !?[]const u8 { - return try iter.inner.next(&iter.arena.allocator); - } - }; -} - -fn testNoErr(params: []const Param(u8), args: []const []const u8, ids: []const u8, values: []const ?[]const u8) void { - var arg_iter = ArgSliceIterator.init(args); - var iter = Clap(u8).init(params, &arg_iter.iter, debug.global_allocator); - - var i: usize = 0; - while (iter.next() catch unreachable) |arg| : (i += 1) { - debug.assert(ids[i] == arg.id); - const expected_value = values[i] ?? { - debug.assert(arg.value == null); - continue; - }; - const actual_value = arg.value ?? unreachable; - - debug.assert(mem.eql(u8, expected_value, actual_value)); - } -} - -test "clap.core: short" { - const params = []Param(u8) { - Param(u8).init(0, false, Names.short('a')), - Param(u8).init(1, false, Names.short('b')), - Param(u8).init(2, true, Names.short('c')), - }; - - testNoErr(params, [][]const u8 { "-a" }, []u8{0}, []?[]const u8{null}); - testNoErr(params, [][]const u8 { "-a", "-b" }, []u8{0,1}, []?[]const u8{null,null}); - testNoErr(params, [][]const u8 { "-ab" }, []u8{0,1}, []?[]const u8{null,null}); - testNoErr(params, [][]const u8 { "-c=100" }, []u8{2}, []?[]const u8{"100"}); - testNoErr(params, [][]const u8 { "-c100" }, []u8{2}, []?[]const u8{"100"}); - testNoErr(params, [][]const u8 { "-c", "100" }, []u8{2}, []?[]const u8{"100"}); - testNoErr(params, [][]const u8 { "-abc", "100" }, []u8{0,1,2}, []?[]const u8{null,null,"100"}); - testNoErr(params, [][]const u8 { "-abc=100" }, []u8{0,1,2}, []?[]const u8{null,null,"100"}); - testNoErr(params, [][]const u8 { "-abc100" }, []u8{0,1,2}, []?[]const u8{null,null,"100"}); -} - -test "clap.core: long" { - const params = []Param(u8) { - Param(u8).init(0, false, Names.long("aa")), - Param(u8).init(1, false, Names.long("bb")), - Param(u8).init(2, true, Names.long("cc")), - }; - - testNoErr(params, [][]const u8 { "--aa" }, []u8{0}, []?[]const u8{null}); - testNoErr(params, [][]const u8 { "--aa", "--bb" }, []u8{0,1}, []?[]const u8{null,null}); - testNoErr(params, [][]const u8 { "--cc=100" }, []u8{2}, []?[]const u8{"100"}); - testNoErr(params, [][]const u8 { "--cc", "100" }, []u8{2}, []?[]const u8{"100"}); -} - -test "clap.core: bare" { - const params = []Param(u8) { - Param(u8).init(0, false, Names.bare("aa")), - Param(u8).init(1, false, Names.bare("bb")), - Param(u8).init(2, true, Names.bare("cc")), - }; - - testNoErr(params, [][]const u8 { "aa" }, []u8{0}, []?[]const u8{null}); - testNoErr(params, [][]const u8 { "aa", "bb" }, []u8{0,1}, []?[]const u8{null,null}); - testNoErr(params, [][]const u8 { "cc=100" }, []u8{2}, []?[]const u8{"100"}); - testNoErr(params, [][]const u8 { "cc", "100" }, []u8{2}, []?[]const u8{"100"}); -} - -test "clap.core: none" { - const params = []Param(u8) { - Param(u8).init(0, true, Names.none()), - }; - - testNoErr(params, [][]const u8 { "aa" }, []u8{0}, []?[]const u8{"aa"}); -} - -test "clap.core: all" { - const params = []Param(u8) { - Param(u8).init( - 0, - false, - Names{ - .bare = "aa", - .short = 'a', - .long = "aa", - } - ), - Param(u8).init( - 1, - false, - Names{ - .bare = "bb", - .short = 'b', - .long = "bb", - } - ), - Param(u8).init( - 2, - true, - Names{ - .bare = "cc", - .short = 'c', - .long = "cc", - } - ), - Param(u8).init(3, true, Names.none()), - }; - - testNoErr(params, [][]const u8 { "-a" }, []u8{0}, []?[]const u8{null}); - testNoErr(params, [][]const u8 { "-a", "-b" }, []u8{0,1}, []?[]const u8{null,null}); - testNoErr(params, [][]const u8 { "-ab" }, []u8{0,1}, []?[]const u8{null,null}); - testNoErr(params, [][]const u8 { "-c=100" }, []u8{2}, []?[]const u8{"100"}); - testNoErr(params, [][]const u8 { "-c100" }, []u8{2}, []?[]const u8{"100"}); - testNoErr(params, [][]const u8 { "-c", "100" }, []u8{2}, []?[]const u8{"100"}); - testNoErr(params, [][]const u8 { "-abc", "100" }, []u8{0,1,2}, []?[]const u8{null,null,"100"}); - testNoErr(params, [][]const u8 { "-abc=100" }, []u8{0,1,2}, []?[]const u8{null,null,"100"}); - testNoErr(params, [][]const u8 { "-abc100" }, []u8{0,1,2}, []?[]const u8{null,null,"100"}); - testNoErr(params, [][]const u8 { "--aa" }, []u8{0}, []?[]const u8{null}); - testNoErr(params, [][]const u8 { "--aa", "--bb" }, []u8{0,1}, []?[]const u8{null,null}); - testNoErr(params, [][]const u8 { "--cc=100" }, []u8{2}, []?[]const u8{"100"}); - testNoErr(params, [][]const u8 { "--cc", "100" }, []u8{2}, []?[]const u8{"100"}); - testNoErr(params, [][]const u8 { "aa" }, []u8{0}, []?[]const u8{null}); - testNoErr(params, [][]const u8 { "aa", "bb" }, []u8{0,1}, []?[]const u8{null,null}); - testNoErr(params, [][]const u8 { "cc=100" }, []u8{2}, []?[]const u8{"100"}); - testNoErr(params, [][]const u8 { "cc", "100" }, []u8{2}, []?[]const u8{"100"}); - testNoErr(params, [][]const u8 { "dd" }, []u8{3}, []?[]const u8{"dd"}); -} diff --git a/example.zig b/example.zig deleted file mode 100644 index 4b3fa82..0000000 --- a/example.zig +++ /dev/null @@ -1,68 +0,0 @@ -const std = @import("std"); -const clap = @import("index.zig"); - -const debug = std.debug; -const os = std.os; - -const Options = struct { - print_values: bool, - a: i64, - b: u64, - c: u8, - d: []const u8, -}; - -// Output on windows: -// zig-clap> .\example.exe -a 1 -// zig-clap> .\example.exe -p -a 1 -// a = 1 -// zig-clap> .\example.exe -pa 1 -// a = 1 -// zig-clap> .\example.exe -pd V1 -// d = V1 -// zig-clap> .\example.exe -pd=V2 -// d = V2 -// zig-clap> .\example.exe -p -d=V3 -// d = V3 -// zig-clap> .\example.exe -pdV=4 -// d = V=4 -// zig-clap> .\example.exe -p -dV=5 -// d = V=5 - -pub fn main() !void { - const parser = comptime clap.Clap(Options) { - .defaults = Options { - .print_values = false, - .a = 0, - .b = 0, - .c = 0, - .d = "", - }, - .params = []clap.Param { - clap.Param.smart("a") - .with("takes_value", clap.Parser.int(i64, 10)), - clap.Param.smart("b") - .with("takes_value", clap.Parser.int(u64, 10)), - clap.Param.smart("c") - .with("takes_value", clap.Parser.int(u8, 10)), - clap.Param.smart("d") - .with("takes_value", clap.Parser.string), - clap.Param.smart("print_values") - .with("short", 'p') - .with("long", "print-values"), - } - }; - - var arg_iter = clap.core.OsArgIterator.init(); - const iter = &arg_iter.iter; - const command = iter.next(debug.global_allocator); - - const options = try parser.parse(debug.global_allocator, iter); - - if (options.print_values) { - if (options.a != 0) debug.warn("a = {}\n", options.a); - if (options.b != 0) debug.warn("b = {}\n", options.b); - if (options.c != 0) debug.warn("c = {}\n", options.c); - if (options.d.len != 0) debug.warn("d = {}\n", options.d); - } -} diff --git a/examples/core.zig b/examples/core.zig new file mode 100644 index 0000000..e69de29 diff --git a/examples/extended.zig b/examples/extended.zig new file mode 100644 index 0000000..e69de29 diff --git a/index.zig b/index.zig index 8226dfa..805d72f 100644 --- a/index.zig +++ b/index.zig @@ -1,491 +1,2 @@ -pub const core = @import("core.zig"); - -const builtin = @import("builtin"); -const std = @import("std"); - -const mem = std.mem; -const fmt = std.fmt; -const debug = std.debug; -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, - 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 Command = struct { - field: []const u8, - name: []const u8, - params: []const Param, - sub_commands: []const Command, - - Result: type, - defaults: &const Opaque, - parent: ?&const Command, - - pub fn init(name: []const u8, comptime Result: type, defaults: &const Result, params: []const Param, sub_commands: []const Command) Command { - return Command{ - .field = name, - .name = name, - .params = params, - .sub_commands = sub_commands, - .Result = Result, - .defaults = @ptrCast(&const Opaque, defaults), - .parent = null, - }; - } - - 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: &core.ArgIterator) !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: &core.ArgIterator) !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 iter = core.Clap(usize).init(core_params, arg_iter, allocator); - defer iter.deinit(); - - arg_loop: - while (try iter.next()) |arg| : (pos += 1) { - inline for(command.params) |param, i| { - comptime const field = "result." ++ param.field; - - if (arg.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.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 { - const UnsafeFunction = &const void; - - FieldType: type, - Errors: type, - func: UnsafeFunction, - - pub fn init(comptime FieldType: type, comptime Errors: type, func: parseFunc(FieldType, Errors)) Parser { - return Parser { - .FieldType = FieldType, - .Errors = Errors, - .func = @ptrCast(UnsafeFunction, func), - }; - } - - fn parse(comptime parser: Parser, field_ptr: TakePtr(parser.FieldType), arg: []const u8) parser.Errors!void { - return @ptrCast(parseFunc(parser.FieldType, parser.Errors), parser.func)(field_ptr, arg); - } - - // TODO: This is a workaround, since we don't have pointer reform yet. - fn TakePtr(comptime T: type) type { return &T; } - - fn parseFunc(comptime FieldType: type, comptime Errors: type) type { - return fn(&FieldType, []const u8) Errors!void; - } - - pub fn int(comptime Int: type, comptime radix: u8) Parser { - const func = struct { - fn i(field_ptr: &Int, arg: []const u8) !void { - field_ptr.* = try fmt.parseInt(Int, arg, radix); - } - }.i; - return Parser.init( - Int, - @typeOf(func).ReturnType.ErrorSet, - func - ); - } - - const string = Parser.init( - []const u8, - error{}, - struct { - fn s(field_ptr: &[]const u8, arg: []const u8) (error{}!void) { - field_ptr.* = arg; - } - }.s - ); -}; - - -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, - - pub fn with(op: &const SubOptions, comptime field: []const u8, value: var) SubOptions { - var res = op.*; - @field(res, field) = value; - return res; - } -}; - -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 = core.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 = core.ArgSliceIterator.init(args); - if (command.parse(debug.global_allocator, &arg_iter.iter)) |actual| { - unreachable; - } else |err| { - assert(err == expected); - } -} - -test "command.core" { - _ = core; -} - -test "command: 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)); -} - -test "command: 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{}, - ); - - testNoErr(command, [][]const u8 { "--cc" }, default.with("cc", true)); - testNoErr(command, [][]const u8 { "--int", "100" }, default.with("int", 100)); -} - -test "command: value bool" { - const command = comptime Command.init( - "", - Options, - default, - []Param { - Param.smart("a"), - }, - []Command{}, - ); - - testNoErr(command, [][]const u8 { "-a" }, default.with("a", true)); -} - -test "command: value str" { - const command = comptime Command.init( - "", - Options, - default, - []Param { - Param.smart("str").with("takes_value", Parser.string), - }, - []Command{}, - ); - - testNoErr(command, [][]const u8 { "--str", "Hello World!" }, default.with("str", "Hello World!")); -} - -test "command: 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 "command: position" { - const command = comptime Command.init( - "", - Options, - default, - []Param { - Param.smart("a").with("position", 0), - Param.smart("b").with("position", 1), - }, - []Command{}, - ); - - testNoErr(command, [][]const u8 { "-a", "-b" }, default.with("a", true).with("b", true)); - testErr(command, [][]const u8 { "-b", "-a" }, error.InvalidArgument); -} - -test "command: sub fields" { - const B = struct { - a: bool, - }; - const A = struct { - b: B, - }; - - const command = comptime Command.init( - "", - A, - A { .b = B { .a = false } }, - []Param { - Param.short('a') - .with("field", "b.a"), - }, - []Command{}, - ); - - var arg_iter = core.ArgSliceIterator.init([][]const u8{ "-a" }); - const res = command.parse(debug.global_allocator, &arg_iter.iter) catch unreachable; - debug.assert(res.b.a == true); -} - -test "command: 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{}, - ), - }, - ); - - debug.warn("{c}", ??command.params[0].short); - - 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); -} +pub const core = @import("src/core.zig"); +pub const extended = @import("src/extended.zig"); diff --git a/src/core.zig b/src/core.zig new file mode 100644 index 0000000..a3fb44c --- /dev/null +++ b/src/core.zig @@ -0,0 +1,372 @@ +const std = @import("std"); +const builtin = @import("builtin"); + +const os = std.os; +const heap = std.heap; +const mem = std.mem; +const debug = std.debug; + +/// The names a ::Param can have. +pub const Names = struct { + /// No prefix + bare: ?[]const u8, + + /// '-' prefix + short: ?u8, + + /// '--' prefix + long: ?[]const u8, + + /// Initializes no names + pub fn none() Names { + return Names{ + .bare = null, + .short = null, + .long = null, + }; + } + + /// Initializes a bare name + pub fn bare(b: []const u8) Names { + return Names{ + .bare = b, + .short = null, + .long = null, + }; + } + + /// Initializes a short name + pub fn short(s: u8) Names { + return Names{ + .bare = null, + .short = s, + .long = null, + }; + } + + /// Initializes a long name + pub fn long(l: []const u8) Names { + return Names{ + .bare = null, + .short = null, + .long = l, + }; + } + + /// Initializes a name with a prefix. + /// ::short is set to ::name[0], and ::long is set to ::name. + /// This function asserts that ::name.len != 0 + pub fn prefix(name: []const u8) Names { + debug.assert(name.len != 0); + + return Names{ + .bare = null, + .short = name[0], + .long = name, + }; + } +}; + +/// 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. +/// * They can take a value three different ways. +/// * "-a value" +/// * "-a=value" +/// * "-avalue" +/// * They chain if they don't take values: "-abc". +/// * The last given parameter can take a value in the same way that a single parameter can: +/// * "-abc value" +/// * "-abc=value" +/// * "-abcvalue" +/// * Long ("--long-param"): Should be used for less common parameters, or when no single character +/// can describe the paramter. +/// * They can take a value two different ways. +/// * "--long-param value" +/// * "--long-param=value" +/// * Bare ("bare"): Should be used as for sub-commands and other keywords. +/// * They can take a value two different ways. +/// * "command value" +/// * "command=value" +/// * Value ("value"): Should be used as the primary parameter of the program, like a filename or +/// an expression to parse. +/// * Value parameters must take a value. +pub fn Param(comptime Id: type) type { + return struct { + const Self = this; + + id: Id, + takes_value: bool, + names: Names, + + pub fn init(id: Id, takes_value: bool, names: &const Names) Self { + // Assert, that if the param have no name, then it has to take + // a value. + debug.assert( + names.bare != null or + names.long != null or + names.short != null or + takes_value + ); + + return Self{ + .id = id, + .takes_value = takes_value, + .names = names.*, + }; + } + }; +} + +/// The result returned from ::Clap.next +pub fn Arg(comptime Id: type) type { + return struct { + const Self = this; + + id: Id, + value: ?[]const u8, + + pub fn init(id: Id, value: ?[]const u8) Self { + return Self { + .id = id, + .value = value, + }; + } + }; +} + +/// A interface for iterating over command line arguments +pub const ArgIterator = struct { + const Error = error{OutOfMemory}; + + nextFn: fn(iter: &ArgIterator, allocator: &mem.Allocator) Error!?[]const u8, + + pub fn next(iter: &ArgIterator, allocator: &mem.Allocator) Error!?[]const u8 { + return iter.nextFn(iter, allocator); + } +}; + +/// An ::ArgIterator, which iterates over a slice of arguments. +/// This implementation does not allocate. +pub const ArgSliceIterator = struct { + args: []const []const u8, + index: usize, + iter: ArgIterator, + + pub fn init(args: []const []const u8) ArgSliceIterator { + return ArgSliceIterator { + .args = args, + .index = 0, + .iter = ArgIterator { + .nextFn = nextFn, + }, + }; + } + + fn nextFn(iter: &ArgIterator, allocator: &mem.Allocator) ArgIterator.Error!?[]const u8 { + const self = @fieldParentPtr(ArgSliceIterator, "iter", iter); + if (self.args.len <= self.index) + return null; + + defer self.index += 1; + return self.args[self.index]; + } +}; + +/// An ::ArgIterator, which wraps the ArgIterator in ::std. +/// On windows, this iterator allocates. +pub const OsArgIterator = struct { + args: os.ArgIterator, + iter: ArgIterator, + + pub fn init() OsArgIterator { + return OsArgIterator { + .args = os.args(), + .iter = ArgIterator { + .nextFn = nextFn, + }, + }; + } + + fn nextFn(iter: &ArgIterator, allocator: &mem.Allocator) ArgIterator.Error!?[]const u8 { + const self = @fieldParentPtr(OsArgIterator, "iter", iter); + if (builtin.os == builtin.Os.windows) { + return try self.args.next(allocator) ?? return null; + } else { + return self.args.nextPosix(); + } + } +}; + +/// A command line argument parser which, given an ::ArgIterator, will parse arguments according +/// to the ::params. ::Clap parses in an iterating manner, so you have to use a loop together with +/// ::Clap.next to parse all the arguments of your program. +pub fn Clap(comptime Id: type) type { + return struct { + const Self = this; + + const State = union(enum) { + Normal, + Chaining: Chaining, + + const Chaining = struct { + arg: []const u8, + index: usize, + }; + }; + + arena: heap.ArenaAllocator, + params: []const Param(Id), + inner: &ArgIterator, + state: State, + + pub fn init(params: []const Param(Id), inner: &ArgIterator, allocator: &mem.Allocator) Self { + var res = Self { + .arena = heap.ArenaAllocator.init(allocator), + .params = params, + .inner = inner, + .state = State.Normal, + }; + + return res; + } + + pub fn deinit(iter: &Self) void { + iter.arena.deinit(); + } + + /// Get the next ::Arg that matches a ::Param. + pub fn next(iter: &Self) !?Arg(Id) { + const ArgInfo = struct { + const Kind = enum { Long, Short, Bare }; + + arg: []const u8, + kind: Kind, + }; + + switch (iter.state) { + State.Normal => { + const full_arg = (try iter.innerNext()) ?? return null; + const arg_info = blk: { + var arg = full_arg; + var kind = ArgInfo.Kind.Bare; + + if (mem.startsWith(u8, arg, "--")) { + arg = arg[2..]; + kind = ArgInfo.Kind.Long; + } else if (mem.startsWith(u8, arg, "-")) { + arg = arg[1..]; + kind = ArgInfo.Kind.Short; + } + + if (arg.len == 0) + return error.ArgWithNoName; + + break :blk ArgInfo { .arg = arg, .kind = kind }; + }; + + const arg = arg_info.arg; + const kind = arg_info.kind; + const eql_index = mem.indexOfScalar(u8, arg, '='); + + switch (kind) { + ArgInfo.Kind.Bare, + ArgInfo.Kind.Long => { + for (iter.params) |*param| { + const match = switch (kind) { + ArgInfo.Kind.Bare => param.names.bare ?? continue, + ArgInfo.Kind.Long => param.names.long ?? continue, + else => unreachable, + }; + 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) { + if (maybe_value != null) + return error.DoesntTakeValue; + + return Arg(Id).init(param.id, null); + } + + const value = blk: { + if (maybe_value) |v| + break :blk v; + + break :blk (try iter.innerNext()) ?? return error.MissingValue; + }; + + return Arg(Id).init(param.id, value); + } + }, + ArgInfo.Kind.Short => { + return try iter.chainging(State.Chaining { + .arg = full_arg, + .index = (full_arg.len - arg.len), + }); + }, + } + + // We do a final pass to look for value parameters matches + if (kind == ArgInfo.Kind.Bare) { + for (iter.params) |*param| { + if (param.names.bare) |_| continue; + if (param.names.short) |_| continue; + if (param.names.long) |_| continue; + + return Arg(Id).init(param.id, arg); + } + } + + return error.InvalidArgument; + }, + @TagType(State).Chaining => |state| return try iter.chainging(state), + } + } + + fn chainging(iter: &Self, state: &const State.Chaining) !?Arg(Id) { + const arg = state.arg; + const index = state.index; + const next_index = index + 1; + + for (iter.params) |param| { + const short = param.names.short ?? continue; + if (short != arg[index]) + continue; + + // Before we return, we have to set the new state of the iterator + defer { + if (arg.len <= next_index or param.takes_value) { + iter.state = State.Normal; + } else { + iter.state = State { .Chaining = State.Chaining { + .arg = arg, + .index = next_index, + }}; + } + } + + if (!param.takes_value) + return Arg(Id).init(param.id, null); + + if (arg.len <= next_index) { + const value = (try iter.innerNext()) ?? return error.MissingValue; + return Arg(Id).init(param.id, value); + } + + if (arg[next_index] == '=') { + return Arg(Id).init(param.id, arg[next_index + 1..]); + } + + return Arg(Id).init(param.id, arg[next_index..]); + } + + return error.InvalidArgument; + } + + fn innerNext(iter: &Self) !?[]const u8 { + return try iter.inner.next(&iter.arena.allocator); + } + }; +} diff --git a/src/extended.zig b/src/extended.zig new file mode 100644 index 0000000..9427b83 --- /dev/null +++ b/src/extended.zig @@ -0,0 +1,264 @@ +pub const core = @import("core.zig"); + +const builtin = @import("builtin"); +const std = @import("std"); + +const mem = std.mem; +const fmt = std.fmt; +const debug = std.debug; +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, + 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 Command = struct { + field: []const u8, + name: []const u8, + params: []const Param, + sub_commands: []const Command, + + Result: type, + defaults: &const Opaque, + parent: ?&const Command, + + pub fn init(name: []const u8, comptime Result: type, defaults: &const Result, params: []const Param, sub_commands: []const Command) Command { + return Command{ + .field = name, + .name = name, + .params = params, + .sub_commands = sub_commands, + .Result = Result, + .defaults = @ptrCast(&const Opaque, defaults), + .parent = null, + }; + } + + 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: &core.ArgIterator) !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: &core.ArgIterator) !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 iter = core.Clap(usize).init(core_params, arg_iter, allocator); + defer iter.deinit(); + + arg_loop: + while (try iter.next()) |arg| : (pos += 1) { + inline for(command.params) |param, i| { + comptime const field = "result." ++ param.field; + + if (arg.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.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 { + const UnsafeFunction = &const void; + + FieldType: type, + Errors: type, + func: UnsafeFunction, + + pub fn init(comptime FieldType: type, comptime Errors: type, func: parseFunc(FieldType, Errors)) Parser { + return Parser { + .FieldType = FieldType, + .Errors = Errors, + .func = @ptrCast(UnsafeFunction, func), + }; + } + + fn parse(comptime parser: Parser, field_ptr: TakePtr(parser.FieldType), arg: []const u8) parser.Errors!void { + return @ptrCast(parseFunc(parser.FieldType, parser.Errors), parser.func)(field_ptr, arg); + } + + // TODO: This is a workaround, since we don't have pointer reform yet. + fn TakePtr(comptime T: type) type { return &T; } + + fn parseFunc(comptime FieldType: type, comptime Errors: type) type { + return fn(&FieldType, []const u8) Errors!void; + } + + pub fn int(comptime Int: type, comptime radix: u8) Parser { + const func = struct { + fn i(field_ptr: &Int, arg: []const u8) !void { + field_ptr.* = try fmt.parseInt(Int, arg, radix); + } + }.i; + return Parser.init( + Int, + @typeOf(func).ReturnType.ErrorSet, + func + ); + } + + const string = Parser.init( + []const u8, + error{}, + struct { + fn s(field_ptr: &[]const u8, arg: []const u8) (error{}!void) { + field_ptr.* = arg; + } + }.s + ); +}; diff --git a/tests/core.zig b/tests/core.zig new file mode 100644 index 0000000..a6a705e --- /dev/null +++ b/tests/core.zig @@ -0,0 +1,134 @@ +const std = @import("std"); +const clap = @import("../index.zig"); + +const debug = std.debug; +const mem = std.mem; +const core = clap.core; + +const assert = debug.assert; + +const ArgSliceIterator = core.ArgSliceIterator; +const Names = core.Names; +const Param = core.Param; +const Clap = core.Clap; + +fn testNoErr(params: []const Param(u8), args: []const []const u8, ids: []const u8, values: []const ?[]const u8) void { + var arg_iter = ArgSliceIterator.init(args); + var iter = Clap(u8).init(params, &arg_iter.iter, debug.global_allocator); + + var i: usize = 0; + while (iter.next() catch unreachable) |arg| : (i += 1) { + debug.assert(ids[i] == arg.id); + const expected_value = values[i] ?? { + debug.assert(arg.value == null); + continue; + }; + const actual_value = arg.value ?? unreachable; + + debug.assert(mem.eql(u8, expected_value, actual_value)); + } +} + +test "clap.core: short" { + const params = []Param(u8) { + Param(u8).init(0, false, Names.short('a')), + Param(u8).init(1, false, Names.short('b')), + Param(u8).init(2, true, Names.short('c')), + }; + + testNoErr(params, [][]const u8 { "-a" }, []u8{0}, []?[]const u8{null}); + testNoErr(params, [][]const u8 { "-a", "-b" }, []u8{0,1}, []?[]const u8{null,null}); + testNoErr(params, [][]const u8 { "-ab" }, []u8{0,1}, []?[]const u8{null,null}); + testNoErr(params, [][]const u8 { "-c=100" }, []u8{2}, []?[]const u8{"100"}); + testNoErr(params, [][]const u8 { "-c100" }, []u8{2}, []?[]const u8{"100"}); + testNoErr(params, [][]const u8 { "-c", "100" }, []u8{2}, []?[]const u8{"100"}); + testNoErr(params, [][]const u8 { "-abc", "100" }, []u8{0,1,2}, []?[]const u8{null,null,"100"}); + testNoErr(params, [][]const u8 { "-abc=100" }, []u8{0,1,2}, []?[]const u8{null,null,"100"}); + testNoErr(params, [][]const u8 { "-abc100" }, []u8{0,1,2}, []?[]const u8{null,null,"100"}); +} + +test "clap.core: long" { + const params = []Param(u8) { + Param(u8).init(0, false, Names.long("aa")), + Param(u8).init(1, false, Names.long("bb")), + Param(u8).init(2, true, Names.long("cc")), + }; + + testNoErr(params, [][]const u8 { "--aa" }, []u8{0}, []?[]const u8{null}); + testNoErr(params, [][]const u8 { "--aa", "--bb" }, []u8{0,1}, []?[]const u8{null,null}); + testNoErr(params, [][]const u8 { "--cc=100" }, []u8{2}, []?[]const u8{"100"}); + testNoErr(params, [][]const u8 { "--cc", "100" }, []u8{2}, []?[]const u8{"100"}); +} + +test "clap.core: bare" { + const params = []Param(u8) { + Param(u8).init(0, false, Names.bare("aa")), + Param(u8).init(1, false, Names.bare("bb")), + Param(u8).init(2, true, Names.bare("cc")), + }; + + testNoErr(params, [][]const u8 { "aa" }, []u8{0}, []?[]const u8{null}); + testNoErr(params, [][]const u8 { "aa", "bb" }, []u8{0,1}, []?[]const u8{null,null}); + testNoErr(params, [][]const u8 { "cc=100" }, []u8{2}, []?[]const u8{"100"}); + testNoErr(params, [][]const u8 { "cc", "100" }, []u8{2}, []?[]const u8{"100"}); +} + +test "clap.core: none" { + const params = []Param(u8) { + Param(u8).init(0, true, Names.none()), + }; + + testNoErr(params, [][]const u8 { "aa" }, []u8{0}, []?[]const u8{"aa"}); +} + +test "clap.core: all" { + const params = []Param(u8) { + Param(u8).init( + 0, + false, + Names{ + .bare = "aa", + .short = 'a', + .long = "aa", + } + ), + Param(u8).init( + 1, + false, + Names{ + .bare = "bb", + .short = 'b', + .long = "bb", + } + ), + Param(u8).init( + 2, + true, + Names{ + .bare = "cc", + .short = 'c', + .long = "cc", + } + ), + Param(u8).init(3, true, Names.none()), + }; + + testNoErr(params, [][]const u8 { "-a" }, []u8{0}, []?[]const u8{null}); + testNoErr(params, [][]const u8 { "-a", "-b" }, []u8{0,1}, []?[]const u8{null,null}); + testNoErr(params, [][]const u8 { "-ab" }, []u8{0,1}, []?[]const u8{null,null}); + testNoErr(params, [][]const u8 { "-c=100" }, []u8{2}, []?[]const u8{"100"}); + testNoErr(params, [][]const u8 { "-c100" }, []u8{2}, []?[]const u8{"100"}); + testNoErr(params, [][]const u8 { "-c", "100" }, []u8{2}, []?[]const u8{"100"}); + testNoErr(params, [][]const u8 { "-abc", "100" }, []u8{0,1,2}, []?[]const u8{null,null,"100"}); + testNoErr(params, [][]const u8 { "-abc=100" }, []u8{0,1,2}, []?[]const u8{null,null,"100"}); + testNoErr(params, [][]const u8 { "-abc100" }, []u8{0,1,2}, []?[]const u8{null,null,"100"}); + testNoErr(params, [][]const u8 { "--aa" }, []u8{0}, []?[]const u8{null}); + testNoErr(params, [][]const u8 { "--aa", "--bb" }, []u8{0,1}, []?[]const u8{null,null}); + testNoErr(params, [][]const u8 { "--cc=100" }, []u8{2}, []?[]const u8{"100"}); + testNoErr(params, [][]const u8 { "--cc", "100" }, []u8{2}, []?[]const u8{"100"}); + testNoErr(params, [][]const u8 { "aa" }, []u8{0}, []?[]const u8{null}); + testNoErr(params, [][]const u8 { "aa", "bb" }, []u8{0,1}, []?[]const u8{null,null}); + testNoErr(params, [][]const u8 { "cc=100" }, []u8{2}, []?[]const u8{"100"}); + testNoErr(params, [][]const u8 { "cc", "100" }, []u8{2}, []?[]const u8{"100"}); + testNoErr(params, [][]const u8 { "dd" }, []u8{3}, []?[]const u8{"dd"}); +} diff --git a/tests/extended.zig b/tests/extended.zig new file mode 100644 index 0000000..8f722e4 --- /dev/null +++ b/tests/extended.zig @@ -0,0 +1,234 @@ +const std = @import("std"); +const clap = @import("../index.zig"); + +const debug = std.debug; +const mem = std.mem; +const core = clap.core; +const extended = clap.extended; + +const assert = debug.assert; + +const ArgSliceIterator = core.ArgSliceIterator; +const Command = extended.Command; +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, + + pub fn with(op: &const SubOptions, comptime field: []const u8, value: var) SubOptions { + var res = op.*; + @field(res, field) = value; + return res; + } +}; + +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| { + unreachable; + } else |err| { + assert(err == expected); + } +} + +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)); +} + +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{}, + ); + + testNoErr(command, [][]const u8 { "--cc" }, default.with("cc", true)); + testNoErr(command, [][]const u8 { "--int", "100" }, default.with("int", 100)); +} + +test "clap.extended: value bool" { + const command = comptime Command.init( + "", + Options, + default, + []Param { + Param.smart("a"), + }, + []Command{}, + ); + + testNoErr(command, [][]const u8 { "-a" }, default.with("a", true)); +} + +test "clap.extended: value str" { + const command = comptime Command.init( + "", + Options, + default, + []Param { + Param.smart("str").with("takes_value", Parser.string), + }, + []Command{}, + ); + + testNoErr(command, [][]const u8 { "--str", "Hello World!" }, default.with("str", "Hello World!")); +} + +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: position" { + const command = comptime Command.init( + "", + Options, + default, + []Param { + Param.smart("a").with("position", 0), + Param.smart("b").with("position", 1), + }, + []Command{}, + ); + + testNoErr(command, [][]const u8 { "-a", "-b" }, default.with("a", true).with("b", true)); + testErr(command, [][]const u8 { "-b", "-a" }, error.InvalidArgument); +} + +test "clap.extended: sub fields" { + const B = struct { + a: bool, + }; + const A = struct { + b: B, + }; + + const command = comptime Command.init( + "", + A, + A { .b = B { .a = false } }, + []Param { + Param.short('a') + .with("field", "b.a"), + }, + []Command{}, + ); + + 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); +} + +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{}, + ), + }, + ); + + 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); +} -- cgit v1.2.3