From c06c93608cb3befe77c78ba25c70b14db6f7b319 Mon Sep 17 00:00:00 2001 From: Jimmi Holst Christensen Date: Fri, 25 Feb 2022 19:44:52 +0100 Subject: Revert "Change clap into generating a struct" This reverts commit cfaac64c404fb1c2e892880410aa3b7dd881ea58. --- README.md | 48 +++--- clap.zig | 265 ++++++++++++++++++++++++------ clap/comptime.zig | 237 +++++++++++++++++++++++++++ clap/streaming.zig | 12 +- clap/untyped.zig | 394 --------------------------------------------- example/README.md.template | 4 +- example/help.zig | 10 +- example/simple-ex.zig | 20 +-- example/simple.zig | 20 +-- example/streaming-clap.zig | 2 +- example/usage.zig | 12 +- 11 files changed, 518 insertions(+), 506 deletions(-) create mode 100644 clap/comptime.zig delete mode 100644 clap/untyped.zig diff --git a/README.md b/README.md index 347250e..75b4b19 100644 --- a/README.md +++ b/README.md @@ -34,30 +34,30 @@ pub fn main() !void { // First we specify what parameters our program can take. // We can use `parseParam` to parse a string to a `Param(Help)` const params = comptime [_]clap.Param(clap.Help){ - clap.untyped.parseParam("-h, --help Display this help and exit.") catch unreachable, - clap.untyped.parseParam("-n, --number An option parameter, which takes a value.") catch unreachable, - clap.untyped.parseParam("-s, --string ... An option parameter which can be specified multiple times.") catch unreachable, - clap.untyped.parseParam("...") catch unreachable, + clap.parseParam("-h, --help Display this help and exit. ") catch unreachable, + clap.parseParam("-n, --number An option parameter, which takes a value.") catch unreachable, + clap.parseParam("-s, --string ... An option parameter which can be specified multiple times.") catch unreachable, + clap.parseParam("...") catch unreachable, }; // Initalize our diagnostics, which can be used for reporting useful errors. // This is optional. You can also pass `.{}` to `clap.parse` if you don't // care about the extra information `Diagnostics` provides. var diag = clap.Diagnostic{}; - var res = clap.untyped.parse(clap.Help, ¶ms, .{ .diagnostic = &diag }) catch |err| { + var args = clap.parse(clap.Help, ¶ms, .{ .diagnostic = &diag }) catch |err| { // Report useful error and exit diag.report(io.getStdErr().writer(), err) catch {}; return err; }; - defer res.deinit(); + defer args.deinit(); - if (res.args.help) + if (args.flag("--help")) debug.print("--help\n", .{}); - if (res.args.number) |n| + if (args.option("--number")) |n| debug.print("--number = {s}\n", .{n}); - for (res.args.string) |s| + for (args.options("--string")) |s| debug.print("--string = {s}\n", .{s}); - for (res.positionals) |pos| + for (args.positionals()) |pos| debug.print("{s}\n", .{pos}); } @@ -100,9 +100,9 @@ zig-clap/example/simple-error.zig:16:18: note: called from here There is also a `parseEx` variant that takes an argument iterator. -### `streaming.Clap` +### `StreamingClap` -The `streaming.Clap` is the base of all the other parsers. It's a streaming parser that uses an +The `StreamingClap` is the base of all the other parsers. It's a streaming parser that uses an `args.Iterator` to provide it with arguments lazily. ```zig @@ -140,7 +140,7 @@ pub fn main() !void { // This is optional. You can also leave the `diagnostic` field unset if you // don't care about the extra information `Diagnostic` provides. var diag = clap.Diagnostic{}; - var parser = clap.streaming.Clap(u8, process.ArgIterator){ + var parser = clap.StreamingClap(u8, process.ArgIterator){ .params = ¶ms, .iter = &iter, .diagnostic = &diag, @@ -182,17 +182,17 @@ const std = @import("std"); pub fn main() !void { const params = comptime [_]clap.Param(clap.Help){ - clap.untyped.parseParam("-h, --help Display this help and exit. ") catch unreachable, - clap.untyped.parseParam("-v, --version Output version information and exit.") catch unreachable, + clap.parseParam("-h, --help Display this help and exit. ") catch unreachable, + clap.parseParam("-v, --version Output version information and exit.") catch unreachable, }; - var res = try clap.untyped.parse(clap.Help, ¶ms, .{}); - defer res.deinit(); + var args = try clap.parse(clap.Help, ¶ms, .{}); + defer args.deinit(); // clap.help is a function that can print a simple help message, given a // slice of Param(Help). There is also a helpEx, which can print a // help message for any Param, but it is more verbose to call. - if (res.args.help) + if (args.flag("--help")) return clap.help(std.io.getStdErr().writer(), ¶ms); } @@ -224,18 +224,18 @@ const std = @import("std"); pub fn main() !void { const params = comptime [_]clap.Param(clap.Help){ - clap.untyped.parseParam("-h, --help Display this help and exit.") catch unreachable, - clap.untyped.parseParam("-v, --version Output version information and exit.") catch unreachable, - clap.untyped.parseParam(" --value An option parameter, which takes a value.") catch unreachable, + clap.parseParam("-h, --help Display this help and exit. ") catch unreachable, + clap.parseParam("-v, --version Output version information and exit. ") catch unreachable, + clap.parseParam(" --value An option parameter, which takes a value.") catch unreachable, }; - var res = try clap.untyped.parse(clap.Help, ¶ms, .{}); - defer res.deinit(); + var args = try clap.parse(clap.Help, ¶ms, .{}); + defer args.deinit(); // clap.usage is a function that can print a simple usage message, given a // slice of Param(Help). There is also a usageEx, which can print a // usage message for any Param, but it is more verbose to call. - if (res.args.help) + if (args.flag("--help")) return clap.usage(std.io.getStdErr().writer(), ¶ms); } diff --git a/clap.zig b/clap.zig index f10b61b..39bbef2 100644 --- a/clap.zig +++ b/clap.zig @@ -13,8 +13,8 @@ test "clap" { testing.refAllDecls(@This()); } -pub const streaming = @import("clap/streaming.zig"); -pub const untyped = @import("clap/untyped.zig"); +pub const ComptimeClap = @import("clap/comptime.zig").ComptimeClap; +pub const StreamingClap = @import("clap/streaming.zig").StreamingClap; /// The names a ::Param can have. pub const Names = struct { @@ -23,23 +23,6 @@ pub const Names = struct { /// '--' prefix long: ?[]const u8 = null, - - pub fn longest(names: *const Names) Longest { - if (names.long) |long| - return .{ .kind = .long, .name = long }; - if (names.short) |*short| { - // TODO: Zig cannot figure out @as(*const [1]u8, short) in the ano literal - const casted: *const [1]u8 = short; - return .{ .kind = .short, .name = casted }; - } - - return .{ .kind = .positinal, .name = "" }; - } - - pub const Longest = struct { - kind: enum { long, short, positinal }, - name: []const u8, - }; }; /// Whether a param takes no value (a flag), one value, or can be specified multiple times. @@ -78,6 +61,155 @@ pub fn Param(comptime Id: type) type { }; } +/// Takes a string and parses it to a Param(Help). +/// This is the reverse of 'help' but for at single parameter only. +pub fn parseParam(line: []const u8) !Param(Help) { + // This function become a lot less ergonomic to use once you hit the eval branch quota. To + // avoid this we pick a sane default. Sadly, the only sane default is the biggest possible + // value. If we pick something a lot smaller and a user hits the quota after that, they have + // no way of overriding it, since we set it here. + // We can recosider this again if: + // * We get parseParams: https://github.com/Hejsil/zig-clap/issues/39 + // * We get a larger default branch quota in the zig compiler (stage 2). + // * Someone points out how this is a really bad idea. + @setEvalBranchQuota(std.math.maxInt(u32)); + + var found_comma = false; + var it = mem.tokenize(u8, line, " \t"); + var param_str = it.next() orelse return error.NoParamFound; + + const short_name = if (!mem.startsWith(u8, param_str, "--") and + mem.startsWith(u8, param_str, "-")) + blk: { + found_comma = param_str[param_str.len - 1] == ','; + if (found_comma) + param_str = param_str[0 .. param_str.len - 1]; + + if (param_str.len != 2) + return error.InvalidShortParam; + + const short_name = param_str[1]; + if (!found_comma) { + var res = parseParamRest(it.rest()); + res.names.short = short_name; + return res; + } + + param_str = it.next() orelse return error.NoParamFound; + break :blk short_name; + } else null; + + const long_name = if (mem.startsWith(u8, param_str, "--")) blk: { + if (param_str[param_str.len - 1] == ',') + return error.TrailingComma; + + break :blk param_str[2..]; + } else if (found_comma) { + return error.TrailingComma; + } else if (short_name == null) { + return parseParamRest(mem.trimLeft(u8, line, " \t")); + } else null; + + var res = parseParamRest(it.rest()); + res.names.long = long_name; + res.names.short = short_name; + return res; +} + +fn parseParamRest(line: []const u8) Param(Help) { + if (mem.startsWith(u8, line, "<")) blk: { + const len = mem.indexOfScalar(u8, line, '>') orelse break :blk; + const takes_many = mem.startsWith(u8, line[len + 1 ..], "..."); + const help_start = len + 1 + @as(usize, 3) * @boolToInt(takes_many); + return .{ + .takes_value = if (takes_many) .many else .one, + .id = .{ + .msg = mem.trim(u8, line[help_start..], " \t"), + .value = line[1..len], + }, + }; + } + + return .{ .id = .{ .msg = mem.trim(u8, line, " \t") } }; +} + +fn expectParam(expect: Param(Help), actual: Param(Help)) !void { + try testing.expectEqualStrings(expect.id.msg, actual.id.msg); + try testing.expectEqualStrings(expect.id.value, actual.id.value); + try testing.expectEqual(expect.names.short, actual.names.short); + try testing.expectEqual(expect.takes_value, actual.takes_value); + if (expect.names.long) |long| { + try testing.expectEqualStrings(long, actual.names.long.?); + } else { + try testing.expectEqual(@as(?[]const u8, null), actual.names.long); + } +} + +test "parseParam" { + try expectParam(Param(Help){ + .id = .{ .msg = "Help text", .value = "value" }, + .names = .{ .short = 's', .long = "long" }, + .takes_value = .one, + }, try parseParam("-s, --long Help text")); + + try expectParam(Param(Help){ + .id = .{ .msg = "Help text", .value = "value" }, + .names = .{ .short = 's', .long = "long" }, + .takes_value = .many, + }, try parseParam("-s, --long ... Help text")); + + try expectParam(Param(Help){ + .id = .{ .msg = "Help text", .value = "value" }, + .names = .{ .long = "long" }, + .takes_value = .one, + }, try parseParam("--long Help text")); + + try expectParam(Param(Help){ + .id = .{ .msg = "Help text", .value = "value" }, + .names = .{ .short = 's' }, + .takes_value = .one, + }, try parseParam("-s Help text")); + + try expectParam(Param(Help){ + .id = .{ .msg = "Help text" }, + .names = .{ .short = 's', .long = "long" }, + }, try parseParam("-s, --long Help text")); + + try expectParam(Param(Help){ + .id = .{ .msg = "Help text" }, + .names = .{ .short = 's' }, + }, try parseParam("-s Help text")); + + try expectParam(Param(Help){ + .id = .{ .msg = "Help text" }, + .names = .{ .long = "long" }, + }, try parseParam("--long Help text")); + + try expectParam(Param(Help){ + .id = .{ .msg = "Help text", .value = "A | B" }, + .names = .{ .long = "long" }, + .takes_value = .one, + }, try parseParam("--long Help text")); + + try expectParam(Param(Help){ + .id = .{ .msg = "Help text", .value = "A" }, + .names = .{}, + .takes_value = .one, + }, try parseParam(" Help text")); + + try expectParam(Param(Help){ + .id = .{ .msg = "Help text", .value = "A" }, + .names = .{}, + .takes_value = .many, + }, try parseParam("... Help text")); + + try testing.expectError(error.TrailingComma, parseParam("--long, Help")); + try testing.expectError(error.TrailingComma, parseParam("-s, Help")); + try testing.expectError(error.InvalidShortParam, parseParam("-ss Help")); + try testing.expectError(error.InvalidShortParam, parseParam("-ss Help")); + try testing.expectError(error.InvalidShortParam, parseParam("- Help")); +} + /// Optional diagnostics used for reporting useful errors pub const Diagnostic = struct { arg: []const u8 = "", @@ -174,7 +306,7 @@ test "Diagnostic.report" { pub fn Args(comptime Id: type, comptime params: []const Param(Id)) type { return struct { arena: std.heap.ArenaAllocator, - clap: untyped.Clap(Id, params), + clap: ComptimeClap(Id, params), exe_arg: ?[]const u8, pub fn deinit(a: *@This()) void { @@ -210,6 +342,43 @@ pub const ParseOptions = struct { diagnostic: ?*Diagnostic = null, }; +/// Same as `parseEx` but uses the `args.OsIterator` by default. +pub fn parse( + comptime Id: type, + comptime params: []const Param(Id), + opt: ParseOptions, +) !Args(Id, params) { + var arena = heap.ArenaAllocator.init(opt.allocator); + errdefer arena.deinit(); + + var iter = try process.ArgIterator.initWithAllocator(arena.allocator()); + const exe_arg = iter.next(); + + const clap = try parseEx(Id, params, &iter, .{ + // Let's reuse the arena from the `OSIterator` since we already have it. + .allocator = arena.allocator(), + .diagnostic = opt.diagnostic, + }); + + return Args(Id, params){ + .exe_arg = exe_arg, + .arena = arena, + .clap = clap, + }; +} + +/// Parses the command line arguments passed into the program based on an +/// array of `Param`s. +pub fn parseEx( + comptime Id: type, + comptime params: []const Param(Id), + iter: anytype, + opt: ParseOptions, +) !ComptimeClap(Id, params) { + const Clap = ComptimeClap(Id, params); + return try Clap.parse(iter, opt); +} + /// Will print a help message in the following format: /// -s, --long helpText /// -s, helpText @@ -357,15 +526,15 @@ test "clap.help" { try help( slice_stream.writer(), comptime &.{ - untyped.parseParam("-a Short flag.") catch unreachable, - untyped.parseParam("-b Short option.") catch unreachable, - untyped.parseParam("--aa Long flag.") catch unreachable, - untyped.parseParam("--bb Long option.") catch unreachable, - untyped.parseParam("-c, --cc Both flag.") catch unreachable, - untyped.parseParam("--complicate Flag with a complicated and\nvery long description that\nspans multiple lines.") catch unreachable, - untyped.parseParam("-d, --dd Both option.") catch unreachable, - untyped.parseParam("-d, --dd ... Both repeated option.") catch unreachable, - untyped.parseParam( + parseParam("-a Short flag.") catch unreachable, + parseParam("-b Short option.") catch unreachable, + parseParam("--aa Long flag.") catch unreachable, + parseParam("--bb Long option.") catch unreachable, + parseParam("-c, --cc Both flag.") catch unreachable, + parseParam("--complicate Flag with a complicated and\nvery long description that\nspans multiple lines.") catch unreachable, + parseParam("-d, --dd Both option.") catch unreachable, + parseParam("-d, --dd ... Both repeated option.") catch unreachable, + parseParam( "

Positional. This should not appear in the help message.", ) catch unreachable, }, @@ -498,37 +667,37 @@ fn testUsage(expected: []const u8, params: []const Param(Help)) !void { test "usage" { @setEvalBranchQuota(100000); try testUsage("[-ab]", &.{ - try untyped.parseParam("-a"), - try untyped.parseParam("-b"), + try parseParam("-a"), + try parseParam("-b"), }); try testUsage("[-a ] [-b ]", &.{ - try untyped.parseParam("-a "), - try untyped.parseParam("-b "), + try parseParam("-a "), + try parseParam("-b "), }); try testUsage("[--a] [--b]", &.{ - try untyped.parseParam("--a"), - try untyped.parseParam("--b"), + try parseParam("--a"), + try parseParam("--b"), }); try testUsage("[--a ] [--b ]", &.{ - try untyped.parseParam("--a "), - try untyped.parseParam("--b "), + try parseParam("--a "), + try parseParam("--b "), }); try testUsage("", &.{ - try untyped.parseParam(""), + try parseParam(""), }); try testUsage( "[-ab] [-c ] [-d ] [--e] [--f] [--g ] [--h ] [-i ...] ", &.{ - try untyped.parseParam("-a"), - try untyped.parseParam("-b"), - try untyped.parseParam("-c "), - try untyped.parseParam("-d "), - try untyped.parseParam("--e"), - try untyped.parseParam("--f"), - try untyped.parseParam("--g "), - try untyped.parseParam("--h "), - try untyped.parseParam("-i ..."), - try untyped.parseParam(""), + try parseParam("-a"), + try parseParam("-b"), + try parseParam("-c "), + try parseParam("-d "), + try parseParam("--e"), + try parseParam("--f"), + try parseParam("--g "), + try parseParam("--h "), + try parseParam("-i ..."), + try parseParam(""), }, ); } diff --git a/clap/comptime.zig b/clap/comptime.zig new file mode 100644 index 0000000..b440004 --- /dev/null +++ b/clap/comptime.zig @@ -0,0 +1,237 @@ +const clap = @import("../clap.zig"); +const std = @import("std"); + +const debug = std.debug; +const heap = std.heap; +const io = std.io; +const mem = std.mem; +const testing = std.testing; + +/// Deprecated: Use `parseEx` instead +pub fn ComptimeClap( + comptime Id: type, + comptime params: []const clap.Param(Id), +) type { + comptime var flags: usize = 0; + comptime var single_options: usize = 0; + comptime var multi_options: usize = 0; + comptime var converted_params: []const clap.Param(usize) = &.{}; + for (params) |param| { + var index: usize = 0; + if (param.names.long != null or param.names.short != null) { + const ptr = switch (param.takes_value) { + .none => &flags, + .one => &single_options, + .many => &multi_options, + }; + index = ptr.*; + ptr.* += 1; + } + + converted_params = converted_params ++ [_]clap.Param(usize){.{ + .id = index, + .names = param.names, + .takes_value = param.takes_value, + }}; + } + + return struct { + multi_options: [multi_options][]const []const u8, + single_options: [single_options][]const u8, + single_options_is_set: std.PackedIntArray(u1, single_options), + flags: std.PackedIntArray(u1, flags), + pos: []const []const u8, + allocator: mem.Allocator, + + pub fn parse(iter: anytype, opt: clap.ParseOptions) !@This() { + const allocator = opt.allocator; + var multis = [_]std.ArrayList([]const u8){undefined} ** multi_options; + for (multis) |*multi| + multi.* = std.ArrayList([]const u8).init(allocator); + + var pos = std.ArrayList([]const u8).init(allocator); + + var res = @This(){ + .multi_options = .{undefined} ** multi_options, + .single_options = .{undefined} ** single_options, + .single_options_is_set = std.PackedIntArray(u1, single_options).init( + .{0} ** single_options, + ), + .flags = std.PackedIntArray(u1, flags).init(.{0} ** flags), + .pos = undefined, + .allocator = allocator, + }; + + var stream = clap.StreamingClap(usize, @typeInfo(@TypeOf(iter)).Pointer.child){ + .params = converted_params, + .iter = iter, + .diagnostic = opt.diagnostic, + }; + 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 == .one) { + debug.assert(res.single_options.len != 0); + if (res.single_options.len != 0) { + res.single_options[param.id] = arg.value.?; + res.single_options_is_set.set(param.id, 1); + } + } else if (param.takes_value == .many) { + debug.assert(multis.len != 0); + if (multis.len != 0) + try multis[param.id].append(arg.value.?); + } else { + debug.assert(res.flags.len != 0); + if (res.flags.len != 0) + res.flags.set(param.id, 1); + } + } + + for (multis) |*multi, i| + res.multi_options[i] = multi.toOwnedSlice(); + res.pos = pos.toOwnedSlice(); + + return res; + } + + pub fn deinit(parser: @This()) void { + for (parser.multi_options) |o| + parser.allocator.free(o); + parser.allocator.free(parser.pos); + } + + pub fn flag(parser: @This(), comptime name: []const u8) bool { + const param = comptime findParam(name); + if (param.takes_value != .none) + @compileError(name ++ " is an option and not a flag."); + + return parser.flags.get(param.id) != 0; + } + + pub fn option(parser: @This(), comptime name: []const u8) ?[]const u8 { + const param = comptime findParam(name); + if (param.takes_value == .none) + @compileError(name ++ " is a flag and not an option."); + if (param.takes_value == .many) + @compileError(name ++ " takes many options, not one."); + if (parser.single_options_is_set.get(param.id) == 0) + return null; + return parser.single_options[param.id]; + } + + pub fn options(parser: @This(), comptime name: []const u8) []const []const u8 { + const param = comptime findParam(name); + if (param.takes_value == .none) + @compileError(name ++ " is a flag and not an option."); + if (param.takes_value == .one) + @compileError(name ++ " takes one option, not multiple."); + + return parser.multi_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 "" { + const params = comptime &.{ + clap.parseParam("-a, --aa") catch unreachable, + clap.parseParam("-b, --bb") catch unreachable, + clap.parseParam("-c, --cc ") catch unreachable, + clap.parseParam("-d, --dd ...") catch unreachable, + clap.parseParam("

") catch unreachable, + }; + + var iter = clap.args.SliceIterator{ + .args = &.{ + "-a", "-c", "0", "something", "-d", "a", "--dd", "b", + }, + }; + var args = try clap.parseEx(clap.Help, params, &iter, .{ .allocator = testing.allocator }); + defer args.deinit(); + + try testing.expect(args.flag("-a")); + try testing.expect(args.flag("--aa")); + try testing.expect(!args.flag("-b")); + try testing.expect(!args.flag("--bb")); + try testing.expectEqualStrings("0", args.option("-c").?); + try testing.expectEqualStrings("0", args.option("--cc").?); + try testing.expectEqual(@as(usize, 1), args.positionals().len); + try testing.expectEqualStrings("something", args.positionals()[0]); + try testing.expectEqualSlices([]const u8, &.{ "a", "b" }, args.options("-d")); + try testing.expectEqualSlices([]const u8, &.{ "a", "b" }, args.options("--dd")); +} + +test "empty" { + var iter = clap.args.SliceIterator{ .args = &.{} }; + var args = try clap.parseEx(u8, &.{}, &iter, .{ .allocator = testing.allocator }); + defer args.deinit(); +} + +fn testErr( + comptime params: []const clap.Param(u8), + args_strings: []const []const u8, + expected: []const u8, +) !void { + var diag = clap.Diagnostic{}; + var iter = clap.args.SliceIterator{ .args = args_strings }; + _ = clap.parseEx(u8, params, &iter, .{ + .allocator = testing.allocator, + .diagnostic = &diag, + }) catch |err| { + var buf: [1024]u8 = undefined; + var fbs = io.fixedBufferStream(&buf); + diag.report(fbs.writer(), err) catch return error.TestFailed; + try testing.expectEqualStrings(expected, fbs.getWritten()); + return; + }; + + try testing.expect(false); +} + +test "errors" { + const params = [_]clap.Param(u8){ + .{ + .id = 0, + .names = .{ .short = 'a', .long = "aa" }, + }, + .{ + .id = 1, + .names = .{ .short = 'c', .long = "cc" }, + .takes_value = .one, + }, + }; + + try testErr(¶ms, &.{"q"}, "Invalid argument 'q'\n"); + try testErr(¶ms, &.{"-q"}, "Invalid argument '-q'\n"); + try testErr(¶ms, &.{"--q"}, "Invalid argument '--q'\n"); + try testErr(¶ms, &.{"--q=1"}, "Invalid argument '--q'\n"); + try testErr(¶ms, &.{"-a=1"}, "The argument '-a' does not take a value\n"); + try testErr(¶ms, &.{"--aa=1"}, "The argument '--aa' does not take a value\n"); + try testErr(¶ms, &.{"-c"}, "The argument '-c' requires a value but none was supplied\n"); + try testErr( + ¶ms, + &.{"--cc"}, + "The argument '--cc' requires a value but none was supplied\n", + ); +} diff --git a/clap/streaming.zig b/clap/streaming.zig index 42b1912..8eca51a 100644 --- a/clap/streaming.zig +++ b/clap/streaming.zig @@ -10,7 +10,7 @@ const mem = std.mem; const os = std.os; const testing = std.testing; -/// The result returned from Clap.next +/// The result returned from StreamingClap.next pub fn Arg(comptime Id: type) type { return struct { const Self = @This(); @@ -21,9 +21,9 @@ pub fn Arg(comptime Id: type) type { } /// 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, comptime ArgIterator: type) type { +/// 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 ArgIterator: type) type { return struct { const State = union(enum) { normal, @@ -209,7 +209,7 @@ fn testNoErr( results: []const Arg(u8), ) !void { var iter = args.SliceIterator{ .args = args_strings }; - var c = Clap(u8, args.SliceIterator){ + var c = StreamingClap(u8, args.SliceIterator){ .params = params, .iter = &iter, }; @@ -236,7 +236,7 @@ fn testErr( ) !void { var diag: clap.Diagnostic = undefined; var iter = args.SliceIterator{ .args = args_strings }; - var c = Clap(u8, args.SliceIterator){ + var c = StreamingClap(u8, args.SliceIterator){ .params = params, .iter = &iter, .diagnostic = &diag, diff --git a/clap/untyped.zig b/clap/untyped.zig deleted file mode 100644 index c9b6621..0000000 --- a/clap/untyped.zig +++ /dev/null @@ -1,394 +0,0 @@ -const clap = @import("../clap.zig"); -const std = @import("std"); - -const builtin = std.builtin; -const debug = std.debug; -const heap = std.heap; -const io = std.io; -const mem = std.mem; -const process = std.process; -const testing = std.testing; - -/// Same as `parseEx` but uses the `args.OsIterator` by default. -pub fn parse( - comptime Id: type, - comptime params: []const clap.Param(Id), - opt: clap.ParseOptions, -) !Result(Arguments(Id, params, []const []const u8, &[_][]const u8{})) { - var arena = heap.ArenaAllocator.init(opt.allocator); - errdefer arena.deinit(); - - var iter = try process.ArgIterator.initWithAllocator(arena.allocator()); - const exe_arg = iter.next(); - - const result = try parseEx(Id, params, &iter, .{ - // Let's reuse the arena from the `OSIterator` since we already have it. - .allocator = arena.allocator(), - .diagnostic = opt.diagnostic, - }); - - return Result(Arguments(Id, params, []const []const u8, &.{})){ - .args = result.args, - .positionals = result.positionals, - .exe_arg = exe_arg, - .arena = arena, - }; -} - -pub fn Result(comptime Args: type) type { - return struct { - args: Args, - positionals: []const []const u8, - exe_arg: ?[]const u8, - arena: std.heap.ArenaAllocator, - - pub fn deinit(result: @This()) void { - result.arena.deinit(); - } - }; -} - -/// Parses the command line arguments passed into the program based on an -/// array of `Param`s. -pub fn parseEx( - comptime Id: type, - comptime params: []const clap.Param(Id), - iter: anytype, - opt: clap.ParseOptions, -) !ResultEx(Arguments(Id, params, []const []const u8, &.{})) { - const allocator = opt.allocator; - var positionals = std.ArrayList([]const u8).init(allocator); - var args = Arguments(Id, params, std.ArrayListUnmanaged([]const u8), .{}){}; - errdefer deinitArgs(allocator, &args); - - var stream = clap.streaming.Clap(Id, @typeInfo(@TypeOf(iter)).Pointer.child){ - .params = params, - .iter = iter, - .diagnostic = opt.diagnostic, - }; - while (try stream.next()) |arg| { - inline for (params) |*param| { - if (param == arg.param) { - const longest = comptime param.names.longest(); - switch (longest.kind) { - .short, .long => switch (param.takes_value) { - .none => @field(args, longest.name) = true, - .one => @field(args, longest.name) = arg.value.?, - .many => try @field(args, longest.name).append(allocator, arg.value.?), - }, - .positinal => try positionals.append(arg.value.?), - } - } - } - } - - var result_args = Arguments(Id, params, []const []const u8, &.{}){}; - inline for (@typeInfo(@TypeOf(args)).Struct.fields) |field| { - if (field.field_type == std.ArrayListUnmanaged([]const u8)) { - const slice = @field(args, field.name).toOwnedSlice(allocator); - @field(result_args, field.name) = slice; - } else { - @field(result_args, field.name) = @field(args, field.name); - } - } - - return ResultEx(@TypeOf(result_args)){ - .args = result_args, - .positionals = positionals.toOwnedSlice(), - .allocator = allocator, - }; -} - -pub fn ResultEx(comptime Args: type) type { - return struct { - args: Args, - positionals: []const []const u8, - allocator: mem.Allocator, - - pub fn deinit(result: *@This()) void { - deinitArgs(result.allocator, &result.args); - result.allocator.free(result.positionals); - } - }; -} - -fn deinitArgs(allocator: mem.Allocator, args: anytype) void { - const Args = @TypeOf(args.*); - inline for (@typeInfo(Args).Struct.fields) |field| { - if (field.field_type == []const []const u8) - allocator.free(@field(args, field.name)); - if (field.field_type == std.ArrayListUnmanaged([]const u8)) - @field(args, field.name).deinit(allocator); - } -} - -fn Arguments( - comptime Id: type, - comptime params: []const clap.Param(Id), - comptime MultiArgsType: type, - comptime multi_args_default: MultiArgsType, -) type { - var fields: [params.len]builtin.TypeInfo.StructField = undefined; - - var i: usize = 0; - for (params) |param| { - const longest = param.names.longest(); - if (longest.kind == .positinal) - continue; - - const field_type = switch (param.takes_value) { - .none => bool, - .one => ?[]const u8, - .many => MultiArgsType, - }; - fields[i] = .{ - .name = longest.name, - .field_type = field_type, - .default_value = switch (param.takes_value) { - .none => &false, - .one => &@as(?[]const u8, null), - .many => &multi_args_default, - }, - .is_comptime = false, - .alignment = @alignOf(field_type), - }; - i += 1; - } - - return @Type(.{ .Struct = .{ - .layout = .Auto, - .fields = fields[0..i], - .decls = &.{}, - .is_tuple = false, - } }); -} - -test "" { - const params = comptime &.{ - parseParam("-a, --aa") catch unreachable, - parseParam("-b, --bb") catch unreachable, - parseParam("-c, --cc ") catch unreachable, - parseParam("-d, --dd ...") catch unreachable, - parseParam("

") catch unreachable, - }; - - var iter = clap.args.SliceIterator{ - .args = &.{ - "-a", "-c", "0", "something", "-d", "a", "--dd", "b", - }, - }; - var res = try clap.untyped.parseEx(clap.Help, params, &iter, .{ - .allocator = testing.allocator, - }); - defer res.deinit(); - - try testing.expect(res.args.aa); - try testing.expect(!res.args.bb); - try testing.expectEqualStrings("0", res.args.cc.?); - try testing.expectEqual(@as(usize, 1), res.positionals.len); - try testing.expectEqualStrings("something", res.positionals[0]); - try testing.expectEqualSlices([]const u8, &.{ "a", "b" }, res.args.dd); -} - -test "empty" { - var iter = clap.args.SliceIterator{ .args = &.{} }; - var res = try clap.untyped.parseEx(u8, &.{}, &iter, .{ .allocator = testing.allocator }); - defer res.deinit(); -} - -fn testErr( - comptime params: []const clap.Param(u8), - args_strings: []const []const u8, - expected: []const u8, -) !void { - var diag = clap.Diagnostic{}; - var iter = clap.args.SliceIterator{ .args = args_strings }; - _ = clap.untyped.parseEx(u8, params, &iter, .{ - .allocator = testing.allocator, - .diagnostic = &diag, - }) catch |err| { - var buf: [1024]u8 = undefined; - var fbs = io.fixedBufferStream(&buf); - diag.report(fbs.writer(), err) catch return error.TestFailed; - try testing.expectEqualStrings(expected, fbs.getWritten()); - return; - }; - - try testing.expect(false); -} - -test "errors" { - const params = [_]clap.Param(u8){ - .{ - .id = 0, - .names = .{ .short = 'a', .long = "aa" }, - }, - .{ - .id = 1, - .names = .{ .short = 'c', .long = "cc" }, - .takes_value = .one, - }, - }; - - try testErr(¶ms, &.{"q"}, "Invalid argument 'q'\n"); - try testErr(¶ms, &.{"-q"}, "Invalid argument '-q'\n"); - try testErr(¶ms, &.{"--q"}, "Invalid argument '--q'\n"); - try testErr(¶ms, &.{"--q=1"}, "Invalid argument '--q'\n"); - try testErr(¶ms, &.{"-a=1"}, "The argument '-a' does not take a value\n"); - try testErr(¶ms, &.{"--aa=1"}, "The argument '--aa' does not take a value\n"); - try testErr(¶ms, &.{"-c"}, "The argument '-c' requires a value but none was supplied\n"); - try testErr( - ¶ms, - &.{"--cc"}, - "The argument '--cc' requires a value but none was supplied\n", - ); -} - -/// Takes a string and parses it to a Param(clap.Help). -/// This is the reverse of 'help' but for at single parameter only. -pub fn parseParam(line: []const u8) !clap.Param(clap.Help) { - // This function become a lot less ergonomic to use once you hit the eval branch quota. To - // avoid this we pick a sane default. Sadly, the only sane default is the biggest possible - // value. If we pick something a lot smaller and a user hits the quota after that, they have - // no way of overriding it, since we set it here. - // We can recosider this again if: - // * We get parseParams: https://github.com/Hejsil/zig-clap/issues/39 - // * We get a larger default branch quota in the zig compiler (stage 2). - // * Someone points out how this is a really bad idea. - @setEvalBranchQuota(std.math.maxInt(u32)); - - var found_comma = false; - var it = mem.tokenize(u8, line, " \t"); - var param_str = it.next() orelse return error.NoParamFound; - - const short_name = if (!mem.startsWith(u8, param_str, "--") and - mem.startsWith(u8, param_str, "-")) - blk: { - found_comma = param_str[param_str.len - 1] == ','; - if (found_comma) - param_str = param_str[0 .. param_str.len - 1]; - - if (param_str.len != 2) - return error.InvalidShortParam; - - const short_name = param_str[1]; - if (!found_comma) { - var res = parseParamRest(it.rest()); - res.names.short = short_name; - return res; - } - - param_str = it.next() orelse return error.NoParamFound; - break :blk short_name; - } else null; - - const long_name = if (mem.startsWith(u8, param_str, "--")) blk: { - if (param_str[param_str.len - 1] == ',') - return error.TrailingComma; - - break :blk param_str[2..]; - } else if (found_comma) { - return error.TrailingComma; - } else if (short_name == null) { - return parseParamRest(mem.trimLeft(u8, line, " \t")); - } else null; - - var res = parseParamRest(it.rest()); - res.names.long = long_name; - res.names.short = short_name; - return res; -} - -fn parseParamRest(line: []const u8) clap.Param(clap.Help) { - if (mem.startsWith(u8, line, "<")) blk: { - const len = mem.indexOfScalar(u8, line, '>') orelse break :blk; - const takes_many = mem.startsWith(u8, line[len + 1 ..], "..."); - const help_start = len + 1 + @as(usize, 3) * @boolToInt(takes_many); - return .{ - .takes_value = if (takes_many) .many else .one, - .id = .{ - .msg = mem.trim(u8, line[help_start..], " \t"), - .value = line[1..len], - }, - }; - } - - return .{ .id = .{ .msg = mem.trim(u8, line, " \t") } }; -} - -fn expectParam(expect: clap.Param(clap.Help), actual: clap.Param(clap.Help)) !void { - try testing.expectEqualStrings(expect.id.msg, actual.id.msg); - try testing.expectEqualStrings(expect.id.value, actual.id.value); - try testing.expectEqual(expect.names.short, actual.names.short); - try testing.expectEqual(expect.takes_value, actual.takes_value); - if (expect.names.long) |long| { - try testing.expectEqualStrings(long, actual.names.long.?); - } else { - try testing.expectEqual(@as(?[]const u8, null), actual.names.long); - } -} - -test "parseParam" { - try expectParam(clap.Param(clap.Help){ - .id = .{ .msg = "Help text", .value = "value" }, - .names = .{ .short = 's', .long = "long" }, - .takes_value = .one, - }, try parseParam("-s, --long Help text")); - - try expectParam(clap.Param(clap.Help){ - .id = .{ .msg = "Help text", .value = "value" }, - .names = .{ .short = 's', .long = "long" }, - .takes_value = .many, - }, try parseParam("-s, --long ... Help text")); - - try expectParam(clap.Param(clap.Help){ - .id = .{ .msg = "Help text", .value = "value" }, - .names = .{ .long = "long" }, - .takes_value = .one, - }, try parseParam("--long Help text")); - - try expectParam(clap.Param(clap.Help){ - .id = .{ .msg = "Help text", .value = "value" }, - .names = .{ .short = 's' }, - .takes_value = .one, - }, try parseParam("-s Help text")); - - try expectParam(clap.Param(clap.Help){ - .id = .{ .msg = "Help text" }, - .names = .{ .short = 's', .long = "long" }, - }, try parseParam("-s, --long Help text")); - - try expectParam(clap.Param(clap.Help){ - .id = .{ .msg = "Help text" }, - .names = .{ .short = 's' }, - }, try parseParam("-s Help text")); - - try expectParam(clap.Param(clap.Help){ - .id = .{ .msg = "Help text" }, - .names = .{ .long = "long" }, - }, try parseParam("--long Help text")); - - try expectParam(clap.Param(clap.Help){ - .id = .{ .msg = "Help text", .value = "A | B" }, - .names = .{ .long = "long" }, - .takes_value = .one, - }, try parseParam("--long Help text")); - - try expectParam(clap.Param(clap.Help){ - .id = .{ .msg = "Help text", .value = "A" }, - .names = .{}, - .takes_value = .one, - }, try parseParam(" Help text")); - - try expectParam(clap.Param(clap.Help){ - .id = .{ .msg = "Help text", .value = "A" }, - .names = .{}, - .takes_value = .many, - }, try parseParam("... Help text")); - - try testing.expectError(error.TrailingComma, parseParam("--long, Help")); - try testing.expectError(error.TrailingComma, parseParam("-s, Help")); - try testing.expectError(error.InvalidShortParam, parseParam("-ss Help")); - try testing.expectError(error.InvalidShortParam, parseParam("-ss Help")); - try testing.expectError(error.InvalidShortParam, parseParam("- Help")); -} diff --git a/example/README.md.template b/example/README.md.template index d792152..7f5c545 100644 --- a/example/README.md.template +++ b/example/README.md.template @@ -51,9 +51,9 @@ zig-clap/example/simple-error.zig:16:18: note: called from here There is also a `parseEx` variant that takes an argument iterator. -### `streaming.Clap` +### `StreamingClap` -The `streaming.Clap` is the base of all the other parsers. It's a streaming parser that uses an +The `StreamingClap` is the base of all the other parsers. It's a streaming parser that uses an `args.Iterator` to provide it with arguments lazily. ```zig diff --git a/example/help.zig b/example/help.zig index 2e7ca2b..de3b707 100644 --- a/example/help.zig +++ b/example/help.zig @@ -3,16 +3,16 @@ const std = @import("std"); pub fn main() !void { const params = comptime [_]clap.Param(clap.Help){ - clap.untyped.parseParam("-h, --help Display this help and exit. ") catch unreachable, - clap.untyped.parseParam("-v, --version Output version information and exit.") catch unreachable, + clap.parseParam("-h, --help Display this help and exit. ") catch unreachable, + clap.parseParam("-v, --version Output version information and exit.") catch unreachable, }; - var res = try clap.untyped.parse(clap.Help, ¶ms, .{}); - defer res.deinit(); + var args = try clap.parse(clap.Help, ¶ms, .{}); + defer args.deinit(); // clap.help is a function that can print a simple help message, given a // slice of Param(Help). There is also a helpEx, which can print a // help message for any Param, but it is more verbose to call. - if (res.args.help) + if (args.flag("--help")) return clap.help(std.io.getStdErr().writer(), ¶ms); } diff --git a/example/simple-ex.zig b/example/simple-ex.zig index f1a958d..d2dc77e 100644 --- a/example/simple-ex.zig +++ b/example/simple-ex.zig @@ -11,10 +11,10 @@ pub fn main() !void { // First we specify what parameters our program can take. // We can use `parseParam` to parse a string to a `Param(Help)` const params = comptime [_]clap.Param(clap.Help){ - clap.untyped.parseParam("-h, --help Display this help and exit.") catch unreachable, - clap.untyped.parseParam("-n, --number An option parameter, which takes a value.") catch unreachable, - clap.untyped.parseParam("-s, --string ... An option parameter which can be specified multiple times.") catch unreachable, - clap.untyped.parseParam("...") catch unreachable, + clap.parseParam("-h, --help Display this help and exit. ") catch unreachable, + clap.parseParam("-n, --number An option parameter, which takes a value.") catch unreachable, + clap.parseParam("-s, --string ... An option parameter which can be specified multiple times.") catch unreachable, + clap.parseParam("...") catch unreachable, }; var iter = try process.ArgIterator.initWithAllocator(allocator); @@ -27,7 +27,7 @@ pub fn main() !void { // This is optional. You can also pass `.{}` to `clap.parse` if you don't // care about the extra information `Diagnostics` provides. var diag = clap.Diagnostic{}; - var res = clap.untyped.parseEx(clap.Help, ¶ms, &iter, .{ + var args = clap.parseEx(clap.Help, ¶ms, &iter, .{ .allocator = allocator, .diagnostic = &diag, }) catch |err| { @@ -35,14 +35,14 @@ pub fn main() !void { diag.report(io.getStdErr().writer(), err) catch {}; return err; }; - defer res.deinit(); + defer args.deinit(); - if (res.args.help) + if (args.flag("--help")) debug.print("--help\n", .{}); - if (res.args.number) |n| + if (args.option("--number")) |n| debug.print("--number = {s}\n", .{n}); - for (res.args.string) |s| + for (args.options("--string")) |s| debug.print("--string = {s}\n", .{s}); - for (res.positionals) |pos| + for (args.positionals()) |pos| debug.print("{s}\n", .{pos}); } diff --git a/example/simple.zig b/example/simple.zig index c37e896..ff6d301 100644 --- a/example/simple.zig +++ b/example/simple.zig @@ -8,29 +8,29 @@ pub fn main() !void { // First we specify what parameters our program can take. // We can use `parseParam` to parse a string to a `Param(Help)` const params = comptime [_]clap.Param(clap.Help){ - clap.untyped.parseParam("-h, --help Display this help and exit.") catch unreachable, - clap.untyped.parseParam("-n, --number An option parameter, which takes a value.") catch unreachable, - clap.untyped.parseParam("-s, --string ... An option parameter which can be specified multiple times.") catch unreachable, - clap.untyped.parseParam("...") catch unreachable, + clap.parseParam("-h, --help Display this help and exit. ") catch unreachable, + clap.parseParam("-n, --number An option parameter, which takes a value.") catch unreachable, + clap.parseParam("-s, --string ... An option parameter which can be specified multiple times.") catch unreachable, + clap.parseParam("...") catch unreachable, }; // Initalize our diagnostics, which can be used for reporting useful errors. // This is optional. You can also pass `.{}` to `clap.parse` if you don't // care about the extra information `Diagnostics` provides. var diag = clap.Diagnostic{}; - var res = clap.untyped.parse(clap.Help, ¶ms, .{ .diagnostic = &diag }) catch |err| { + var args = clap.parse(clap.Help, ¶ms, .{ .diagnostic = &diag }) catch |err| { // Report useful error and exit diag.report(io.getStdErr().writer(), err) catch {}; return err; }; - defer res.deinit(); + defer args.deinit(); - if (res.args.help) + if (args.flag("--help")) debug.print("--help\n", .{}); - if (res.args.number) |n| + if (args.option("--number")) |n| debug.print("--number = {s}\n", .{n}); - for (res.args.string) |s| + for (args.options("--string")) |s| debug.print("--string = {s}\n", .{s}); - for (res.positionals) |pos| + for (args.positionals()) |pos| debug.print("{s}\n", .{pos}); } diff --git a/example/streaming-clap.zig b/example/streaming-clap.zig index cacda56..a7ab7d8 100644 --- a/example/streaming-clap.zig +++ b/example/streaming-clap.zig @@ -32,7 +32,7 @@ pub fn main() !void { // This is optional. You can also leave the `diagnostic` field unset if you // don't care about the extra information `Diagnostic` provides. var diag = clap.Diagnostic{}; - var parser = clap.streaming.Clap(u8, process.ArgIterator){ + var parser = clap.StreamingClap(u8, process.ArgIterator){ .params = ¶ms, .iter = &iter, .diagnostic = &diag, diff --git a/example/usage.zig b/example/usage.zig index 04fedba..368a6b3 100644 --- a/example/usage.zig +++ b/example/usage.zig @@ -3,17 +3,17 @@ const std = @import("std"); pub fn main() !void { const params = comptime [_]clap.Param(clap.Help){ - clap.untyped.parseParam("-h, --help Display this help and exit.") catch unreachable, - clap.untyped.parseParam("-v, --version Output version information and exit.") catch unreachable, - clap.untyped.parseParam(" --value An option parameter, which takes a value.") catch unreachable, + clap.parseParam("-h, --help Display this help and exit. ") catch unreachable, + clap.parseParam("-v, --version Output version information and exit. ") catch unreachable, + clap.parseParam(" --value An option parameter, which takes a value.") catch unreachable, }; - var res = try clap.untyped.parse(clap.Help, ¶ms, .{}); - defer res.deinit(); + var args = try clap.parse(clap.Help, ¶ms, .{}); + defer args.deinit(); // clap.usage is a function that can print a simple usage message, given a // slice of Param(Help). There is also a usageEx, which can print a // usage message for any Param, but it is more verbose to call. - if (res.args.help) + if (args.flag("--help")) return clap.usage(std.io.getStdErr().writer(), ¶ms); } -- cgit v1.2.3