zig-clap/clap/streaming.zig
2020-09-06 17:31:27 +02:00

363 lines
12 KiB
Zig

const builtin = @import("builtin");
const clap = @import("../clap.zig");
const std = @import("std");
const args = clap.args;
const testing = std.testing;
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 = null,
};
}
/// 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 ArgIterator: 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: *ArgIterator,
state: State = State.Normal,
/// 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) {
.Normal => {
const full_arg = (try parser.iter.next()) orelse return null;
const arg_info = if (mem.eql(u8, full_arg, "--") or mem.eql(u8, full_arg, "-"))
ArgInfo{ .arg = full_arg, .kind = .Positional }
else if (mem.startsWith(u8, full_arg, "--"))
ArgInfo{ .arg = full_arg[2..], .kind = .Long }
else if (mem.startsWith(u8, full_arg, "-"))
ArgInfo{ .arg = full_arg[1..], .kind = .Short }
else
ArgInfo{ .arg = full_arg, .kind = .Positional };
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 == .None) {
if (maybe_value != null)
return error.DoesntTakeValue;
return Arg(Id){ .param = param };
}
const value = blk: {
if (maybe_value) |v|
break :blk v;
break :blk (try parser.iter.next()) orelse return error.MissingValue;
};
return Arg(Id){ .param = param, .value = 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){ .param = param, .value = arg };
}
},
}
return error.InvalidArgument;
},
.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 != .None) {
parser.state = State.Normal;
} else {
parser.state = State{
.Chaining = State.Chaining{
.arg = arg,
.index = next_index,
},
};
}
}
if (param.takes_value == .None)
return Arg(Id){ .param = param };
if (arg.len <= next_index) {
const value = (try parser.iter.next()) orelse return error.MissingValue;
return Arg(Id){ .param = param, .value = value };
}
if (arg[next_index] == '=')
return Arg(Id){ .param = param, .value = arg[next_index + 1 ..] };
return Arg(Id){ .param = param, .value = arg[next_index..] };
}
return error.InvalidArgument;
}
};
}
fn testNoErr(params: []const clap.Param(u8), args_strings: []const []const u8, results: []const Arg(u8)) void {
var iter = args.SliceIterator{ .args = args_strings };
var c = StreamingClap(u8, args.SliceIterator){
.params = params,
.iter = &iter,
};
for (results) |res| {
const arg = (c.next() catch unreachable) orelse unreachable;
testing.expectEqual(res.param, arg.param);
const expected_value = res.value orelse {
testing.expectEqual(@as(@TypeOf(arg.value), null), arg.value);
continue;
};
const actual_value = arg.value orelse unreachable;
testing.expectEqualSlices(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){
.id = 0,
.names = clap.Names{ .short = 'a' },
},
clap.Param(u8){
.id = 1,
.names = clap.Names{ .short = 'b' },
},
clap.Param(u8){
.id = 2,
.names = clap.Names{ .short = 'c' },
.takes_value = .One,
},
clap.Param(u8){
.id = 3,
.names = clap.Names{ .short = 'd' },
.takes_value = .Many,
},
};
const a = &params[0];
const b = &params[1];
const c = &params[2];
const d = &params[3];
testNoErr(
&params,
&[_][]const u8{
"-a", "-b", "-ab", "-ba",
"-c", "0", "-c=0", "-ac",
"0", "-ac=0", "-d=0",
},
&[_]Arg(u8){
Arg(u8){ .param = a },
Arg(u8){ .param = b },
Arg(u8){ .param = a },
Arg(u8){ .param = b },
Arg(u8){ .param = b },
Arg(u8){ .param = a },
Arg(u8){ .param = c, .value = "0" },
Arg(u8){ .param = c, .value = "0" },
Arg(u8){ .param = a },
Arg(u8){ .param = c, .value = "0" },
Arg(u8){ .param = a },
Arg(u8){ .param = c, .value = "0" },
Arg(u8){ .param = d, .value = "0" },
},
);
}
test "clap.streaming.StreamingClap: long params" {
const params = [_]clap.Param(u8){
clap.Param(u8){
.id = 0,
.names = clap.Names{ .long = "aa" },
},
clap.Param(u8){
.id = 1,
.names = clap.Names{ .long = "bb" },
},
clap.Param(u8){
.id = 2,
.names = clap.Names{ .long = "cc" },
.takes_value = .One,
},
clap.Param(u8){
.id = 3,
.names = clap.Names{ .long = "dd" },
.takes_value = .Many,
},
};
const aa = &params[0];
const bb = &params[1];
const cc = &params[2];
const dd = &params[3];
testNoErr(
&params,
&[_][]const u8{
"--aa", "--bb",
"--cc", "0",
"--cc=0", "--dd=0",
},
&[_]Arg(u8){
Arg(u8){ .param = aa },
Arg(u8){ .param = bb },
Arg(u8){ .param = cc, .value = "0" },
Arg(u8){ .param = cc, .value = "0" },
Arg(u8){ .param = dd, .value = "0" },
},
);
}
test "clap.streaming.StreamingClap: positional params" {
const params = [_]clap.Param(u8){clap.Param(u8){
.id = 0,
.takes_value = .One,
}};
testNoErr(
&params,
&[_][]const u8{ "aa", "bb" },
&[_]Arg(u8){
Arg(u8){ .param = &params[0], .value = "aa" },
Arg(u8){ .param = &params[0], .value = "bb" },
},
);
}
test "clap.streaming.StreamingClap: all params" {
const params = [_]clap.Param(u8){
clap.Param(u8){
.id = 0,
.names = clap.Names{
.short = 'a',
.long = "aa",
},
},
clap.Param(u8){
.id = 1,
.names = clap.Names{
.short = 'b',
.long = "bb",
},
},
clap.Param(u8){
.id = 2,
.names = clap.Names{
.short = 'c',
.long = "cc",
},
.takes_value = .One,
},
clap.Param(u8){
.id = 3,
.takes_value = .One,
},
};
const aa = &params[0];
const bb = &params[1];
const cc = &params[2];
const positional = &params[3];
testNoErr(
&params,
&[_][]const u8{
"-a", "-b", "-ab", "-ba",
"-c", "0", "-c=0", "-ac",
"0", "-ac=0", "--aa", "--bb",
"--cc", "0", "--cc=0", "something",
"--", "-",
},
&[_]Arg(u8){
Arg(u8){ .param = aa },
Arg(u8){ .param = bb },
Arg(u8){ .param = aa },
Arg(u8){ .param = bb },
Arg(u8){ .param = bb },
Arg(u8){ .param = aa },
Arg(u8){ .param = cc, .value = "0" },
Arg(u8){ .param = cc, .value = "0" },
Arg(u8){ .param = aa },
Arg(u8){ .param = cc, .value = "0" },
Arg(u8){ .param = aa },
Arg(u8){ .param = cc, .value = "0" },
Arg(u8){ .param = aa },
Arg(u8){ .param = bb },
Arg(u8){ .param = cc, .value = "0" },
Arg(u8){ .param = cc, .value = "0" },
Arg(u8){ .param = positional, .value = "something" },
Arg(u8){ .param = positional, .value = "--" },
Arg(u8){ .param = positional, .value = "-" },
},
);
}