From c564b168785e740c37f47da4942825b25cb8b4ec Mon Sep 17 00:00:00 2001 From: Jimmi Holst Christensen Date: Wed, 14 Nov 2018 14:06:20 +0100 Subject: Restructured and make StreamingClap simpler * Also added a ComptimeClap --- src/args.zig | 90 +++++++++++++++ src/comptime.zig | 142 +++++++++++++++++++++++ src/index.zig | 105 +++++++++++++++++ src/streaming.zig | 338 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 675 insertions(+) create mode 100644 src/args.zig create mode 100644 src/comptime.zig create mode 100644 src/index.zig create mode 100644 src/streaming.zig (limited to 'src') diff --git a/src/args.zig b/src/args.zig new file mode 100644 index 0000000..304596c --- /dev/null +++ b/src/args.zig @@ -0,0 +1,90 @@ +const builtin = @import("builtin"); +const std = @import("std"); + +const debug = std.debug; +const heap = std.heap; +const mem = std.mem; +const os = std.os; + +/// A interface for iterating over command line arguments +pub fn Iterator(comptime E: type) type { + return struct { + const Self = @This(); + const Error = E; + + nextFn: fn (iter: *Self) Error!?[]const u8, + + pub fn next(iter: *Self) Error!?[]const u8 { + return iter.nextFn(iter); + } + }; +} + +/// An ::ArgIterator, which iterates over a slice of arguments. +/// This implementation does not allocate. +pub const SliceIterator = struct { + const Error = error{}; + + args: []const []const u8, + index: usize, + iter: Iterator(Error), + + pub fn init(args: []const []const u8) SliceIterator { + return SliceIterator{ + .args = args, + .index = 0, + .iter = Iterator(Error){ .nextFn = nextFn }, + }; + } + + fn nextFn(iter: *Iterator(Error)) Error!?[]const u8 { + const self = @fieldParentPtr(SliceIterator, "iter", iter); + if (self.args.len <= self.index) + return null; + + defer self.index += 1; + return self.args[self.index]; + } +}; + +test "clap.args.SliceIterator" { + const args = [][]const u8{ "A", "BB", "CCC" }; + var slice_iter = SliceIterator.init(args); + const iter = &slice_iter.iter; + + for (args) |a| { + const b = try iter.next(); + debug.assert(mem.eql(u8, a, b.?)); + } +} + +/// An ::ArgIterator, which wraps the ArgIterator in ::std. +/// On windows, this iterator allocates. +pub const OsIterator = struct { + const Error = os.ArgIterator.NextError; + + arena: heap.ArenaAllocator, + args: os.ArgIterator, + iter: Iterator(Error), + + pub fn init(allocator: *mem.Allocator) OsIterator { + return OsIterator{ + .arena = heap.ArenaAllocator.init(allocator), + .args = os.args(), + .iter = Iterator(Error){ .nextFn = nextFn }, + }; + } + + pub fn deinit(iter: *OsIterator) void { + iter.arena.deinit(); + } + + fn nextFn(iter: *Iterator(Error)) Error!?[]const u8 { + const self = @fieldParentPtr(OsIterator, "iter", iter); + if (builtin.os == builtin.Os.windows) { + return try self.args.next(&self.arena.allocator) orelse return null; + } else { + return self.args.nextPosix(); + } + } +}; diff --git a/src/comptime.zig b/src/comptime.zig new file mode 100644 index 0000000..b11dccc --- /dev/null +++ b/src/comptime.zig @@ -0,0 +1,142 @@ +const clap = @import("index.zig"); +const std = @import("std"); + +const debug = std.debug; +const heap = std.heap; +const mem = std.mem; + +pub fn ComptimeClap(comptime Id: type, comptime params: []const clap.Param(Id)) type { + var flags: usize = 0; + var options: usize = 0; + var converted_params: []const clap.Param(usize) = []clap.Param(usize){}; + for (params) |param| { + const index = blk: { + if (param.names.long == null and param.names.short == null) + break :blk 0; + if (param.takes_value) { + const res = options; + options += 1; + break :blk res; + } + + const res = flags; + flags += 1; + break :blk res; + }; + + converted_params = converted_params ++ []clap.Param(usize){ + clap.Param(usize).init(index, param.takes_value, param.names), + }; + } + + return struct { + options: [options]?[]const u8, + flags: [flags]bool, + pos: []const []const u8, + allocator: *mem.Allocator, + + pub fn parse(allocator: *mem.Allocator, comptime ArgError: type, iter: *clap.args.Iterator(ArgError)) !@This() { + var pos = std.ArrayList([]const u8).init(allocator); + var res = @This(){ + .options = []?[]const u8{null} ** options, + .flags = []bool{false} ** flags, + .pos = undefined, + .allocator = allocator, + }; + + var stream = clap.StreamingClap(usize, ArgError).init(converted_params, iter); + while (try stream.next()) |arg| { + const param = arg.param; + if (param.names.long == null and param.names.short == null) { + try pos.append(arg.value.?); + } else if (param.takes_value) { + // We slice before access to avoid false positive access out of bound + // compile error. + res.options[0..][param.id] = arg.value.?; + } else { + res.flags[0..][param.id] = true; + } + } + + res.pos = pos.toOwnedSlice(); + return res; + } + + pub fn deinit(parser: *@This()) void { + parser.allocator.free(parser.pos); + parser.* = undefined; + } + + pub fn flag(parser: @This(), comptime name: []const u8) bool { + const param = comptime findParam(name); + if (param.takes_value) + @compileError(name ++ " is an option and not a flag."); + + return parser.flags[param.id]; + } + + pub fn option(parser: @This(), comptime name: []const u8) ?[]const u8 { + const param = comptime findParam(name); + if (!param.takes_value) + @compileError(name ++ " is a flag and not an option."); + + return parser.options[param.id]; + } + + pub fn positionals(parser: @This()) []const []const u8 { + return parser.pos; + } + + fn findParam(comptime name: []const u8) clap.Param(usize) { + comptime { + for (converted_params) |param| { + if (param.names.short) |s| { + if (mem.eql(u8, name, "-" ++ []u8{s})) + return param; + } + if (param.names.long) |l| { + if (mem.eql(u8, name, "--" ++ l)) + return param; + } + } + + @compileError(name ++ " is not a parameter."); + } + } + }; +} + +test "clap.comptime.ComptimeClap" { + const Clap = ComptimeClap(void, comptime []clap.Param(void){ + clap.Param(void).init({}, false, clap.Names{ + .short = 'a', + .long = "aa", + }), + clap.Param(void).init({}, false, clap.Names{ + .short = 'b', + .long = "bb", + }), + clap.Param(void).init({}, true, clap.Names{ + .short = 'c', + .long = "cc", + }), + clap.Param(void).init({}, true, clap.Names.positional()), + }); + + var buf: [1024]u8 = undefined; + var fb_allocator = heap.FixedBufferAllocator.init(buf[0..]); + var arg_iter = clap.args.SliceIterator.init([][]const u8{ + "-a", "-c", "0", "something", + }); + var args = try Clap.parse(&fb_allocator.allocator, clap.args.SliceIterator.Error, &arg_iter.iter); + defer args.deinit(); + + debug.assert(args.flag("-a")); + debug.assert(args.flag("--aa")); + debug.assert(!args.flag("-b")); + debug.assert(!args.flag("--bb")); + debug.assert(mem.eql(u8, args.option("-c").?, "0")); + debug.assert(mem.eql(u8, args.option("--cc").?, "0")); + debug.assert(args.positionals().len == 1); + debug.assert(mem.eql(u8, args.positionals()[0], "something")); +} diff --git a/src/index.zig b/src/index.zig new file mode 100644 index 0000000..dde4748 --- /dev/null +++ b/src/index.zig @@ -0,0 +1,105 @@ +const std = @import("std"); + +const debug = std.debug; + +pub const @"comptime" = @import("comptime.zig"); +pub const args = @import("args.zig"); +pub const streaming = @import("streaming.zig"); + +test "clap" { + _ = @"comptime"; + _ = args; + _ = streaming; +} + +pub const ComptimeClap = @"comptime".ComptimeClap; +pub const StreamingClap = streaming.StreamingClap; + +/// The names a ::Param can have. +pub const Names = struct { + /// '-' prefix + short: ?u8, + + /// '--' prefix + long: ?[]const u8, + + /// Initializes no names + pub fn positional() Names { + return Names{ + .short = null, + .long = null, + }; + } + + /// Initializes a short name + pub fn short(s: u8) Names { + return Names{ + .short = s, + .long = null, + }; + } + + /// Initializes a long name + pub fn long(l: []const u8) Names { + return Names{ + .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{ + .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" +/// * Positional: Should be used as the primary parameter of the program, like a filename or +/// an expression to parse. +/// * Positional parameters have both names.long and names.short == null. +/// * Positional parameters must take a value. +pub fn Param(comptime Id: type) type { + return struct { + id: Id, + takes_value: bool, + names: Names, + + pub fn init(id: Id, takes_value: bool, names: Names) @This() { + // Assert, that if the param have no name, then it has to take + // a value. + debug.assert( + names.long != null or + names.short != null or + takes_value); + + return @This(){ + .id = id, + .takes_value = takes_value, + .names = names, + }; + } + }; +} diff --git a/src/streaming.zig b/src/streaming.zig new file mode 100644 index 0000000..bfb4045 --- /dev/null +++ b/src/streaming.zig @@ -0,0 +1,338 @@ +const builtin = @import("builtin"); +const clap = @import("index.zig"); +const std = @import("std"); + +const args = clap.args; +const debug = std.debug; +const heap = std.heap; +const mem = std.mem; +const os = std.os; + +/// The result returned from ::StreamingClap.next +pub fn Arg(comptime Id: type) type { + return struct { + const Self = @This(); + + param: *const clap.Param(Id), + value: ?[]const u8, + + pub fn init(param: *const clap.Param(Id), value: ?[]const u8) Self { + return Self{ + .param = param, + .value = value, + }; + } + }; +} + +/// A command line argument parser which, given an ::ArgIterator, will parse arguments according +/// to the ::params. ::StreamingClap parses in an iterating manner, so you have to use a loop together with +/// ::StreamingClap.next to parse all the arguments of your program. +pub fn StreamingClap(comptime Id: type, comptime ArgError: type) type { + return struct { + + const State = union(enum) { + Normal, + Chaining: Chaining, + + const Chaining = struct { + arg: []const u8, + index: usize, + }; + }; + + params: []const clap.Param(Id), + iter: *args.Iterator(ArgError), + state: State, + + pub fn init(params: []const clap.Param(Id), iter: *args.Iterator(ArgError)) @This() { + var res = @This(){ + .params = params, + .iter = iter, + .state = State.Normal, + }; + + return res; + } + + /// Get the next ::Arg that matches a ::Param. + pub fn next(parser: *@This()) !?Arg(Id) { + const ArgInfo = struct { + const Kind = enum { + Long, + Short, + Positional, + }; + + arg: []const u8, + kind: Kind, + }; + + switch (parser.state) { + State.Normal => { + const full_arg = (try parser.iter.next()) orelse return null; + const arg_info = blk: { + var arg = full_arg; + var kind = ArgInfo.Kind.Positional; + + 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; + } + + // 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 }; + }; + + const arg = arg_info.arg; + const kind = arg_info.kind; + const eql_index = mem.indexOfScalar(u8, arg, '='); + + switch (kind) { + ArgInfo.Kind.Long => { + 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) { + if (maybe_value != null) + return error.DoesntTakeValue; + + return Arg(Id).init(param, null); + } + + const value = blk: { + if (maybe_value) |v| + break :blk v; + + break :blk (try parser.iter.next()) orelse return error.MissingValue; + }; + + return Arg(Id).init(param, value); + } + }, + ArgInfo.Kind.Short => { + return try parser.chainging(State.Chaining{ + .arg = full_arg, + .index = (full_arg.len - arg.len), + }); + }, + ArgInfo.Kind.Positional => { + for (parser.params) |*param| { + if (param.names.long) |_| + continue; + if (param.names.short) |_| + continue; + + return Arg(Id).init(param, arg); + } + }, + } + + return error.InvalidArgument; + }, + @TagType(State).Chaining => |state| return try parser.chainging(state), + } + } + + fn chainging(parser: *@This(), state: State.Chaining) !?Arg(Id) { + const arg = state.arg; + const index = state.index; + const next_index = index + 1; + + for (parser.params) |*param| { + const short = param.names.short orelse continue; + if (short != arg[index]) + continue; + + // Before we return, we have to set the new state of the clap + defer { + if (arg.len <= next_index or param.takes_value) { + parser.state = State.Normal; + } else { + parser.state = State{ + .Chaining = State.Chaining{ + .arg = arg, + .index = next_index, + }, + }; + } + } + + if (!param.takes_value) + return Arg(Id).init(param, null); + + if (arg.len <= next_index) { + const value = (try parser.iter.next()) orelse return error.MissingValue; + return Arg(Id).init(param, value); + } + + if (arg[next_index] == '=') { + return Arg(Id).init(param, arg[next_index + 1 ..]); + } + + return Arg(Id).init(param, arg[next_index..]); + } + + return error.InvalidArgument; + } + }; +} + + +fn testNoErr(params: []const clap.Param(u8), args_strings: []const []const u8, results: []const Arg(u8)) void { + var arg_iter = args.SliceIterator.init(args_strings); + var c = StreamingClap(u8, args.SliceIterator.Error).init(params, &arg_iter.iter); + + for (results) |res| { + const arg = (c.next() catch unreachable) orelse unreachable; + debug.assert(res.param == arg.param); + const expected_value = res.value orelse { + debug.assert(arg.value == null); + continue; + }; + const actual_value = arg.value orelse unreachable; + debug.assert(mem.eql(u8, expected_value, actual_value)); + } + + if (c.next() catch unreachable) |_| { + unreachable; + } +} + +test "clap.streaming.StreamingClap: short params" { + const params = []clap.Param(u8){ + clap.Param(u8).init(0, false, clap.Names.short('a')), + clap.Param(u8).init(1, false, clap.Names.short('b')), + clap.Param(u8).init(2, true, clap.Names.short('c')), + }; + + const a = ¶ms[0]; + const b = ¶ms[1]; + const c = ¶ms[2]; + + testNoErr( + params, + [][]const u8{ + "-a", "-b", "-ab", "-ba", + "-c", "0", "-c=0", "-ac", + "0", "-ac=0", + }, + []const Arg(u8){ + Arg(u8).init(a, null), + Arg(u8).init(b, null), + Arg(u8).init(a, null), + Arg(u8).init(b, null), + Arg(u8).init(b, null), + Arg(u8).init(a, null), + Arg(u8).init(c, "0"), + Arg(u8).init(c, "0"), + Arg(u8).init(a, null), + Arg(u8).init(c, "0"), + Arg(u8).init(a, null), + Arg(u8).init(c, "0"), + }, + ); +} + +test "clap.streaming.StreamingClap: long params" { + const params = []clap.Param(u8){ + clap.Param(u8).init(0, false, clap.Names.long("aa")), + clap.Param(u8).init(1, false, clap.Names.long("bb")), + clap.Param(u8).init(2, true, clap.Names.long("cc")), + }; + + const aa = ¶ms[0]; + const bb = ¶ms[1]; + const cc = ¶ms[2]; + + testNoErr( + params, + [][]const u8{ + "--aa", "--bb", + "--cc", "0", + "--cc=0", + }, + []const Arg(u8){ + Arg(u8).init(aa, null), + Arg(u8).init(bb, null), + Arg(u8).init(cc, "0"), + Arg(u8).init(cc, "0"), + }, + ); +} + +test "clap.streaming.StreamingClap: positional params" { + const params = []clap.Param(u8){clap.Param(u8).init(0, true, clap.Names.positional())}; + + testNoErr( + params, + [][]const u8{ "aa", "bb" }, + []const Arg(u8){ + Arg(u8).init(¶ms[0], "aa"), + Arg(u8).init(¶ms[0], "bb"), + }, + ); +} + +test "clap.streaming.StreamingClap: all params" { + const params = []clap.Param(u8){ + clap.Param(u8).init(0, false, clap.Names{ + .short = 'a', + .long = "aa", + }), + clap.Param(u8).init(1, false, clap.Names{ + .short = 'b', + .long = "bb", + }), + clap.Param(u8).init(2, true, clap.Names{ + .short = 'c', + .long = "cc", + }), + clap.Param(u8).init(3, true, clap.Names.positional()), + }; + + const aa = ¶ms[0]; + const bb = ¶ms[1]; + const cc = ¶ms[2]; + const positional = ¶ms[3]; + + testNoErr( + params, + [][]const u8{ + "-a", "-b", "-ab", "-ba", + "-c", "0", "-c=0", "-ac", + "0", "-ac=0", "--aa", "--bb", + "--cc", "0", "--cc=0", "something", + }, + []const Arg(u8){ + Arg(u8).init(aa, null), + Arg(u8).init(bb, null), + Arg(u8).init(aa, null), + Arg(u8).init(bb, null), + Arg(u8).init(bb, null), + Arg(u8).init(aa, null), + Arg(u8).init(cc, "0"), + Arg(u8).init(cc, "0"), + Arg(u8).init(aa, null), + Arg(u8).init(cc, "0"), + Arg(u8).init(aa, null), + Arg(u8).init(cc, "0"), + Arg(u8).init(aa, null), + Arg(u8).init(bb, null), + Arg(u8).init(cc, "0"), + Arg(u8).init(cc, "0"), + Arg(u8).init(positional, "something"), + }, + ); +} -- cgit v1.2.3