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 --- .travis.yml | 5 +- README.md | 100 +++++++++++- build.zig | 48 ++++++ clap.zig | 376 --------------------------------------------- example/comptime-clap.zig | 47 ++++++ example/streaming-clap.zig | 54 +++++++ src/args.zig | 90 +++++++++++ src/comptime.zig | 142 +++++++++++++++++ src/index.zig | 105 +++++++++++++ src/streaming.zig | 338 ++++++++++++++++++++++++++++++++++++++++ test.zig | 245 ----------------------------- 11 files changed, 921 insertions(+), 629 deletions(-) create mode 100644 build.zig delete mode 100644 clap.zig create mode 100644 example/comptime-clap.zig create mode 100644 example/streaming-clap.zig create mode 100644 src/args.zig create mode 100644 src/comptime.zig create mode 100644 src/index.zig create mode 100644 src/streaming.zig delete mode 100644 test.zig diff --git a/.travis.yml b/.travis.yml index 48e7273..b4adebe 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,4 @@ git: depth: false script: - zig-linux-*/zig version - - zig-linux-*/zig test test.zig - - zig-linux-*/zig test test.zig --release-fast - - zig-linux-*/zig test test.zig --release-safe - - zig-linux-*/zig test test.zig --release-small + - zig-linux-*/zig build diff --git a/README.md b/README.md index f4c3766..92f53c2 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,109 @@ # zig-clap A simple and easy to use command line argument parser library for Zig. -It's ment as a thin layer of abstraction over parsing arguments which allows -for further abstraction on top, such as filling in a `HashMap`. ## Features -See [example](https://github.com/Hejsil/zig-clap/blob/38a51948069f405864ab327826b5975a6d0c93a8/test.zig#L200-L247). * Short arguments `-a` * Chaining `-abc` where `a` and `b` does not take values. * Long arguments `--long` -* Bare arguments `bare` * Supports both passing values using spacing and `=` (`-a 100`, `-a=100`) * Short args also support passing values with no spacing or `=` (`-a100`) * This all works with chaining (`-ba 100`, `-ba=100`, `-ba100`) +## Examples + +### `StreamingClap` + +The `StreamingClap` is base of all the other parsers. It's a streaming parser that uses an +`args.Iterator` to provide it with arguments lazily. + +```rust +const params = []clap.Param(u8){ + clap.Param(u8).init('h', false, clap.Names.prefix("help")), + clap.Param(u8).init('n', true, clap.Names.prefix("number")), + clap.Param(u8).init('f', true, clap.Names.positional()), +}; + +var os_iter = clap.args.OsIterator.init(allocator); +const iter = &os_iter.iter; +defer os_iter.deinit(); + +const exe = try iter.next(); + +var parser = clap.StreamingClap(u8, clap.args.OsIterator.Error).init(params, iter); + +while (try parser.next()) |arg| { + switch (arg.param.id) { + 'h' => debug.warn("Help!\n"), + 'n' => debug.warn("--number = {}\n", arg.value.?), + 'f' => debug.warn("{}\n", arg.value.?), + else => unreachable, + } + } +``` + +### `ComptimeClap` + +The `ComptimeClap` is a wrapper for `StreamingClap`, which parses all the arguments and makes +them available through three functions (`flag`, `option`, `positionals`). + +```rust +const params = comptime []clap.Param(void){ + clap.Param(void).init({}, false, clap.Names.prefix("help")), + clap.Param(void).init({}, true, clap.Names.prefix("number")), + clap.Param(void).init({}, true, clap.Names.positional()), +}; + +var os_iter = clap.args.OsIterator.init(allocator); +const iter = &os_iter.iter; +defer os_iter.deinit(); + +const exe = try iter.next(); + +var args = try clap.ComptimeClap(void, params).parse(allocator, clap.args.OsIterator.Error, iter); +defer args.deinit(); + +if (args.flag("--help")) + debug.warn("Help!\n"); +if (args.option("--number")) |n| + debug.warn("--number = {}\n", n); +for (args.positionals()) |pos| + debug.warn("{}\n", pos); +``` + +The data structure returned from this parser has lookup speed on par with array access (`arr[i]`) +and validates that the strings you pass to `option` and `flag` are actually parameters that the +program can take: + +```rust +const params = comptime []clap.Param(void){ + clap.Param(void).init({}, false, clap.Names.prefix("help")), +}; + +var os_iter = clap.args.OsIterator.init(allocator); +const iter = &os_iter.iter; +defer os_iter.deinit(); + +const exe = try iter.next(); + +var args = try clap.ComptimeClap(params).parse(allocator, clap.args.OsIterator.Error, iter); +defer args.deinit(); + +if (args.flag("--helps")) + debug.warn("Help!\n"); +``` + +``` +zig-clap/src/comptime.zig:103:17: error: --helps is not a parameter. + @compileError(name ++ " is not a parameter."); + ^ +zig-clap/src/comptime.zig:71:45: note: called from here + const param = comptime findParam(name); + ^ +zig-clap/example/comptime-clap.zig:41:18: note: called from here + if (args.flag("--helps")) + ^ +``` + +Ofc, this limits you to use only parameters that are comptime known. diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..06c012b --- /dev/null +++ b/build.zig @@ -0,0 +1,48 @@ +const builtin = @import("builtin"); +const std = @import("std"); + +const Mode = builtin.Mode; +const Builder = std.build.Builder; + +pub fn build(b: *Builder) void { + const mode = b.standardReleaseOptions(); + + const example_step = b.step("examples", "Build examples"); + inline for ([][]const u8{ + "comptime-clap", + "streaming-clap", + }) |example_name| { + const example = b.addExecutable(example_name, "example/" ++ example_name ++ ".zig"); + example.addPackagePath("clap", "src/index.zig"); + example.setBuildMode(mode); + example_step.dependOn(&example.step); + } + + const test_all_step = b.step("test", "Run all tests in all modes."); + inline for ([]Mode{ Mode.Debug, Mode.ReleaseFast, Mode.ReleaseSafe, Mode.ReleaseSmall }) |test_mode| { + const mode_str = comptime modeToString(test_mode); + + const tests = b.addTest("src/index.zig"); + tests.setBuildMode(test_mode); + tests.setNamePrefix(mode_str ++ " "); + + const test_step = b.step("test-" ++ mode_str, "Run all tests in " ++ mode_str ++ "."); + test_step.dependOn(&tests.step); + test_all_step.dependOn(test_step); + } + + const all_step = b.step("all", "Build everything and runs all tests"); + all_step.dependOn(test_all_step); + all_step.dependOn(example_step); + + b.default_step.dependOn(all_step); +} + +fn modeToString(mode: Mode) []const u8 { + return switch (mode) { + Mode.Debug => "debug", + Mode.ReleaseFast => "release-fast", + Mode.ReleaseSafe => "release-safe", + Mode.ReleaseSmall => "release-small", + }; +} diff --git a/clap.zig b/clap.zig deleted file mode 100644 index a2d30a5..0000000 --- a/clap.zig +++ /dev/null @@ -1,376 +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: 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 ::StreamingClap.next -pub fn Arg(comptime Id: type) type { - return struct { - const Self = @This(); - - param: *const Param(Id), - value: ?[]const u8, - - pub fn init(param: *const Param(Id), value: ?[]const u8) Self { - return Self{ - .param = param, - .value = value, - }; - } - }; -} - -/// A interface for iterating over command line arguments -pub fn ArgIterator(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 ArgSliceIterator = struct { - const Error = error{}; - - args: []const []const u8, - index: usize, - iter: ArgIterator(Error), - - pub fn init(args: []const []const u8) ArgSliceIterator { - return ArgSliceIterator{ - .args = args, - .index = 0, - .iter = ArgIterator(Error){ .nextFn = nextFn }, - }; - } - - fn nextFn(iter: *ArgIterator(Error)) 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 { - const Error = os.ArgIterator.NextError; - - arena: heap.ArenaAllocator, - args: os.ArgIterator, - iter: ArgIterator(Error), - - pub fn init(allocator: *mem.Allocator) OsArgIterator { - return OsArgIterator{ - .arena = heap.ArenaAllocator.init(allocator), - .args = os.args(), - .iter = ArgIterator(Error){ .nextFn = nextFn }, - }; - } - - pub fn deinit(iter: *OsArgIterator) void { - iter.arena.deinit(); - } - - fn nextFn(iter: *ArgIterator(Error)) Error!?[]const u8 { - const self = @fieldParentPtr(OsArgIterator, "iter", iter); - if (builtin.os == builtin.Os.windows) { - return try self.args.next(&self.arena.allocator) orelse return null; - } else { - return self.args.nextPosix(); - } - } -}; - -/// 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 Self = @This(); - - const State = union(enum) { - Normal, - Chaining: Chaining, - - const Chaining = struct { - arg: []const u8, - index: usize, - }; - }; - - params: []const Param(Id), - iter: *ArgIterator(ArgError), - state: State, - - pub fn init(params: []const Param(Id), iter: *ArgIterator(ArgError)) Self { - var res = Self{ - .params = params, - .iter = iter, - .state = State.Normal, - }; - - return res; - } - - /// Get the next ::Arg that matches a ::Param. - pub fn next(clap: *Self) !?Arg(Id) { - const ArgInfo = struct { - const Kind = enum { - Long, - Short, - Bare, - }; - - arg: []const u8, - kind: Kind, - }; - - switch (clap.state) { - State.Normal => { - const full_arg = (try clap.iter.next()) orelse 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; - } - - // 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.Bare, ArgInfo.Kind.Long => { - for (clap.params) |*param| { - const match = switch (kind) { - ArgInfo.Kind.Bare => param.names.bare orelse continue, - ArgInfo.Kind.Long => param.names.long orelse 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, null); - } - - const value = blk: { - if (maybe_value) |v| - break :blk v; - - break :blk (try clap.iter.next()) orelse return error.MissingValue; - }; - - return Arg(Id).init(param, value); - } - }, - ArgInfo.Kind.Short => { - return try clap.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 (clap.params) |*param| { - if (param.names.bare) |_| continue; - if (param.names.short) |_| continue; - if (param.names.long) |_| continue; - - return Arg(Id).init(param, arg); - } - } - - return error.InvalidArgument; - }, - @TagType(State).Chaining => |state| return try clap.chainging(state), - } - } - - fn chainging(clap: *Self, state: State.Chaining) !?Arg(Id) { - const arg = state.arg; - const index = state.index; - const next_index = index + 1; - - for (clap.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) { - clap.state = State.Normal; - } else { - clap.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 clap.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; - } - }; -} diff --git a/example/comptime-clap.zig b/example/comptime-clap.zig new file mode 100644 index 0000000..e44d9b1 --- /dev/null +++ b/example/comptime-clap.zig @@ -0,0 +1,47 @@ +const std = @import("std"); +const clap = @import("clap"); + +const debug = std.debug; + +pub fn main() !void { + var direct_allocator = std.heap.DirectAllocator.init(); + const allocator = &direct_allocator.allocator; + defer direct_allocator.deinit(); + + // First we specify what parameters our program can take. + const params = comptime []clap.Param(void){ + // Param.init takes 3 arguments. + // * An "id", which can be any type specified by the argument to Param. The + // ComptimeClap expects clap.Param(void) only. + // * A bool which determins wether the parameter takes a value. + // * A "Names" struct, which determins what names the parameter will have on the + // commandline. Names.prefix inits a "Names" struct that has the "short" name + // set to the first letter, and the "long" name set to the full name. + clap.Param(void).init({}, false, clap.Names.prefix("help")), + clap.Param(void).init({}, true, clap.Names.prefix("number")), + + // Names.positional returns a "Names" struct where neither the "short" or "long" + // name is set. + clap.Param(void).init({}, true, clap.Names.positional()), + }; + + // We then initialize an argument iterator. We will use the OsIterator as it nicely + // wraps iterating over arguments the most efficient way on each os. + var os_iter = clap.args.OsIterator.init(allocator); + const iter = &os_iter.iter; + defer os_iter.deinit(); + + // Consume the exe arg. + const exe = try iter.next(); + + // Finally we can parse the arguments + var args = try clap.ComptimeClap(void, params).parse(allocator, clap.args.OsIterator.Error, iter); + defer args.deinit(); + + if (args.flag("--help")) + debug.warn("Help!\n"); + if (args.option("--number")) |n| + debug.warn("--number = {}\n", n); + for (args.positionals()) |pos| + debug.warn("{}\n", pos); +} diff --git a/example/streaming-clap.zig b/example/streaming-clap.zig new file mode 100644 index 0000000..0dc2bf7 --- /dev/null +++ b/example/streaming-clap.zig @@ -0,0 +1,54 @@ +const std = @import("std"); +const clap = @import("clap"); + +const debug = std.debug; + +pub fn main() !void { + var direct_allocator = std.heap.DirectAllocator.init(); + const allocator = &direct_allocator.allocator; + defer direct_allocator.deinit(); + + // First we specify what parameters our program can take. + const params = []clap.Param(u8){ + // Param.init takes 3 arguments. + // * An "id", which can be any type specified by the argument to Param. Here, we + // use a "u8" as the "id" type. + // * A bool which determins wether the parameter takes a value. + // * A "Names" struct, which determins what names the parameter will have on the + // commandline. Names.prefix inits a "Names" struct that has the "short" name + // set to the first letter, and the "long" name set to the full name. + clap.Param(u8).init('h', false, clap.Names.prefix("help")), + clap.Param(u8).init('n', true, clap.Names.prefix("number")), + + // Names.positional returns a "Names" struct where neither the "short" or "long" + // name is set. + clap.Param(u8).init('f', true, clap.Names.positional()), + }; + + // We then initialize an argument iterator. We will use the OsIterator as it nicely + // wraps iterating over arguments the most efficient way on each os. + var os_iter = clap.args.OsIterator.init(allocator); + const iter = &os_iter.iter; + defer os_iter.deinit(); + + // Consume the exe arg. + const exe = try iter.next(); + + // Finally we initialize our streaming parser. + var parser = clap.StreamingClap(u8, clap.args.OsIterator.Error).init(params, iter); + + // Because we use a streaming parser, we have to consume each argument parsed individually. + while (try parser.next()) |arg| { + // arg.param will point to the parameter which matched the argument. + switch (arg.param.id) { + 'h' => debug.warn("Help!\n"), + 'n' => debug.warn("--number = {}\n", arg.value.?), + + // arg.value == null, if arg.param.takes_value == false. + // Otherwise, arg.value is the value passed with the argument, such as "-a=10" + // or "-a 10". + 'f' => debug.warn("{}\n", arg.value.?), + else => unreachable, + } + } +} 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"), + }, + ); +} diff --git a/test.zig b/test.zig deleted file mode 100644 index 27c93c2..0000000 --- a/test.zig +++ /dev/null @@ -1,245 +0,0 @@ -const std = @import("std"); -const clap = @import("clap.zig"); - -const debug = std.debug; -const mem = std.mem; - -const assert = debug.assert; - -const ArgSliceIterator = clap.ArgSliceIterator; -const Names = clap.Names; -const Param = clap.Param(u8); -const StreamingClap = clap.StreamingClap(u8, ArgSliceIterator.Error); -const Arg = clap.Arg(u8); - -fn testNoErr(params: []const Param, args: []const []const u8, results: []const Arg) void { - var arg_iter = ArgSliceIterator.init(args); - var c = StreamingClap.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: short" { - const params = []Param{ - Param.init(0, false, Names.short('a')), - Param.init(1, false, Names.short('b')), - Param.init(2, true, 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{ - Arg.init(a, null), - Arg.init(b, null), - Arg.init(a, null), - Arg.init(b, null), - Arg.init(b, null), - Arg.init(a, null), - Arg.init(c, "0"), - Arg.init(c, "0"), - Arg.init(a, null), - Arg.init(c, "0"), - Arg.init(a, null), - Arg.init(c, "0"), - }, - ); -} - -test "clap: long" { - const params = []Param{ - Param.init(0, false, Names.long("aa")), - Param.init(1, false, Names.long("bb")), - Param.init(2, true, 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{ - Arg.init(aa, null), - Arg.init(bb, null), - Arg.init(cc, "0"), - Arg.init(cc, "0"), - }, - ); -} - -test "clap: bare" { - const params = []Param{ - Param.init(0, false, Names.bare("aa")), - Param.init(1, false, Names.bare("bb")), - Param.init(2, true, Names.bare("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{ - Arg.init(aa, null), - Arg.init(bb, null), - Arg.init(cc, "0"), - Arg.init(cc, "0"), - }, - ); -} - -test "clap: none" { - const params = []Param{Param.init(0, true, Names.none())}; - - testNoErr( - params, - [][]const u8{ "aa", "bb" }, - []const Arg{ - Arg.init(¶ms[0], "aa"), - Arg.init(¶ms[0], "bb"), - }, - ); -} - -test "clap: all" { - const params = []Param{ - Param.init(0, false, Names{ - .bare = "aa", - .short = 'a', - .long = "aa", - }), - Param.init(1, false, Names{ - .bare = "bb", - .short = 'b', - .long = "bb", - }), - Param.init(2, true, Names{ - .bare = "cc", - .short = 'c', - .long = "cc", - }), - Param.init(3, true, Names.none()), - }; - - const aa = ¶ms[0]; - const bb = ¶ms[1]; - const cc = ¶ms[2]; - const bare = ¶ms[3]; - - testNoErr( - params, - [][]const u8{ - "-a", "-b", "-ab", "-ba", - "-c", "0", "-c=0", "-ac", - "0", "-ac=0", "--aa", "--bb", - "--cc", "0", "--cc=0", "aa", - "bb", "cc", "0", "cc=0", - "something", - }, - []const Arg{ - Arg.init(aa, null), - Arg.init(bb, null), - Arg.init(aa, null), - Arg.init(bb, null), - Arg.init(bb, null), - Arg.init(aa, null), - Arg.init(cc, "0"), - Arg.init(cc, "0"), - Arg.init(aa, null), - Arg.init(cc, "0"), - Arg.init(aa, null), - Arg.init(cc, "0"), - Arg.init(aa, null), - Arg.init(bb, null), - Arg.init(cc, "0"), - Arg.init(cc, "0"), - Arg.init(aa, null), - Arg.init(bb, null), - Arg.init(cc, "0"), - Arg.init(cc, "0"), - Arg.init(bare, "something"), - }, - ); -} - -test "clap.Example" { - // Fake program arguments. Don't mind them - const program_args = [][]const u8{ - "-h", "--help", - "-v", "--version", - "file.zig", - }; - - const warn = @import("std").debug.warn; - const c = @import("clap.zig"); - - // Initialize the parameters you want your program to take. - // `Param` has a type passed in, which will determin the type - // of `Param.id`. This field can then be used to identify the - // `Param`, or something else entirely. - // Example: You could have the `id` be a function, and then - // call it in the loop further down. - const params = []c.Param(u8){ - c.Param(u8).init('h', false, c.Names.prefix("help")), - c.Param(u8).init('v', false, c.Names.prefix("version")), - c.Param(u8).init('f', true, c.Names.none()), - }; - - // Here, we use an `ArgSliceIterator` which iterates over - // a slice of arguments. For real program, you would probably - // use `OsArgIterator`. - var iter = &c.ArgSliceIterator.init(program_args).iter; - var parser = c.StreamingClap(u8, c.ArgSliceIterator.Error).init(params, iter); - - // Iterate over all arguments passed to the program. - // In real code, you should probably handle the errors - // `parser.next` returns. - while (parser.next() catch unreachable) |arg| { - // `arg.param` is a pointer to its matching `Param` - // from the `params` array. - switch (arg.param.id) { - 'h' => warn("Help!\n"), - 'v' => warn("1.1.1\n"), - - // `arg.value` is `null`, if `arg.param.takes_value` - // is `false`. Otherwise, `arg.value` is the value - // passed with the argument, such as `-a=10` or - // `-a 10`. - 'f' => warn("{}\n", arg.value.?), - else => unreachable, - } - } -} -- cgit v1.2.3