From eaa2e2d0f3f64b4230f3697c9fce5a836590d2a8 Mon Sep 17 00:00:00 2001 From: Jimmi Holst Christensen Date: Wed, 21 Mar 2018 11:05:48 +0100 Subject: Major refactor of clap * You can now specify the field an option is assosiated with * When a match is found, clap will generate code for parsing the argument and setting the field * WIP sub commands * Removed help printing. It'll be back, better and stronger --- clap.zig | 676 ++++++++++++++++++++++++++++++++++++--------------------------- 1 file changed, 388 insertions(+), 288 deletions(-) (limited to 'clap.zig') diff --git a/clap.zig b/clap.zig index c966b18..b3f859b 100644 --- a/clap.zig +++ b/clap.zig @@ -1,4 +1,5 @@ -const std = @import("std"); +const builtin = @import("builtin"); +const std = @import("std"); const mem = std.mem; const fmt = std.fmt; @@ -7,418 +8,517 @@ const io = std.io; const assert = debug.assert; -// TODO: -// * Inform the caller which argument caused the error. -pub fn Option(comptime Result: type, comptime ParseError: type) type { +pub fn Clap(comptime Result: type) type { return struct { const Self = this; - pub const Kind = enum { - Optional, - Required, - IgnoresRequired - }; - - parse: fn(&Result, []const u8) ParseError!void, - help: []const u8, - kind: Kind, - takes_value: bool, - short: ?u8, - long: ?[]const u8, - - pub fn init(parse_fn: fn(&Result, []const u8) ParseError!void) Self { - return Self { - .parse = parse_fn, - .help = "", - .kind = Kind.Optional, - .takes_value = false, - .short = null, - .long = null, - }; - } + program_name: []const u8, + author: []const u8, + version: []const u8, + about: []const u8, + command: Command, + defaults: Result, - pub fn setHelp(option: &const Self, help_str: []const u8) Self { - var res = *option; res.help = help_str; - return res; + pub fn parse(comptime clap: &const Self, arguments: []const []const u8) !Result { + return clap.command.parse(Result, clap.defaults, arguments); } - pub fn setKind(option: &const Self, kind: Kind) Self { - var res = *option; res.kind = kind; - return res; - } + pub const Builder = struct { + result: Self, + + pub fn init(defaults: &const Result) Builder { + return Builder { + .result = Self { + .program_name = "", + .author = "", + .version = "", + .about = "", + .command = Command.Builder.init("").build(), + .defaults = *defaults, + } + }; + } - pub fn takesValue(option: &const Self, takes_value: bool) Self { - var res = *option; res.takes_value = takes_value; - return res; - } + pub fn programName(builder: &const Builder, name: []const u8) Builder { + var res = *builder; + res.result.program_name = name; + return res; + } - pub fn setShort(option: &const Self, short: u8) Self { - var res = *option; res.short = short; - return res; - } + pub fn author(builder: &const Builder, name: []const u8) Builder { + var res = *builder; + res.result.author = name; + return res; + } - pub fn setLong(option: &const Self, long: []const u8) Self { - var res = *option; res.long = long; - return res; - } - }; -} + pub fn version(builder: &const Builder, version: []const u8) Builder { + var res = *builder; + res.result.author = version; + return res; + } -pub fn Parser(comptime Result: type, comptime ParseError: type, comptime defaults: &const Result, - comptime options: []const Option(Result, ParseError)) type { + pub fn about(builder: &const Builder, text: []const u8) Builder { + var res = *builder; + res.result.about = text; + return res; + } - const OptionT = Option(Result, ParseError); - const Arg = struct { - const Kind = enum { Long, Short, None }; + pub fn command(builder: &const Builder, cmd: &const Command) Builder { + var res = *builder; + res.result.command = *cmd; + return res; + } - arg: []const u8, - kind: Kind, - after_eql: ?[]const u8, + pub fn build(builder: &const Builder) Self { + return builder.result; + } + }; }; +} - const Iterator = struct { - slice: []const []const u8, +pub const Command = struct { + field: ?[]const u8, + name: []const u8, + arguments: []const Argument, + sub_commands: []const Command, - pub fn next(it: &this) ?[]const u8 { - if (it.slice.len == 0) - return null; + pub fn parse(comptime command: &const Command, comptime Result: type, defaults: &const Result, arguments: []const []const u8) !Result { + const Arg = struct { + const Kind = enum { Long, Short, Value }; - defer it.slice = it.slice[1..]; - return it.slice[0]; - } - }; + arg: []const u8, + kind: Kind, + after_eql: ?[]const u8, + }; - // NOTE: For now, a bitfield is used to keep track of the required arguments. - // This limits the user to 128 required arguments, which is more than - // enough. - const required_mask = comptime blk: { - var required_index : u128 = 0; - var required_res : u128 = 0; - for (options) |option, i| { - if (option.kind == OptionT.Kind.Required) { - required_res |= 0x1 << required_index; - required_index += 1; - } - } + const Iterator = struct { + index: usize, + slice: []const []const u8, - break :blk required_res; - }; + pub fn next(it: &this) ?[]const u8 { + if (it.index >= it.slice.len) + return null; - return struct { - fn newRequired(option: &const OptionT, old_required: u128, index: usize) u128 { - switch (option.kind) { - OptionT.Kind.Required => { - return old_required & ~(u128(1) << u7(index)); - }, - OptionT.Kind.IgnoresRequired => return 0, - else => return old_required, + defer it.index += 1; + return it.slice[it.index]; } - } - - pub fn parse(args: []const []const u8) !Result { - var result = *defaults; - var required = required_mask; - - var it = Iterator { .slice = args }; - while (it.next()) |item| { - const arg_info = blk: { - var arg = item; - var kind = Arg.Kind.None; - - if (mem.startsWith(u8, arg, "--")) { - arg = arg[2..]; - kind = Arg.Kind.Long; - } else if (mem.startsWith(u8, arg, "-")) { - arg = arg[1..]; - kind = Arg.Kind.Short; - } + }; - if (kind == Arg.Kind.None) - break :blk Arg { .arg = arg, .kind = kind, .after_eql = null }; + // NOTE: For now, a bitfield is used to keep track of the required arguments. + // This limits the user to 128 required arguments, which should be more + // than enough. + var required = comptime blk: { + var required_index : u128 = 0; + var required_res : u128 = 0; + for (command.arguments) |option| { + if (option.required) { + required_res |= 0x1 << required_index; + required_index += 1; + } + } + break :blk required_res; + }; - if (mem.indexOfScalar(u8, arg, '=')) |index| { - break :blk Arg { .arg = arg[0..index], .kind = kind, .after_eql = arg[index + 1..] }; - } else { - break :blk Arg { .arg = arg, .kind = kind, .after_eql = null }; - } - }; - const arg = arg_info.arg; - const kind = arg_info.kind; - const after_eql = arg_info.after_eql; + var result = *defaults; - success: { + var it = Iterator { .index = 0, .slice = arguments }; + while (it.next()) |item| { + const arg_info = blk: { + var arg = item; + var kind = Arg.Kind.Value; - switch (kind) { - Arg.Kind.None => { - var required_index = usize(0); - inline for (options) |option| { - defer if (option.kind == OptionT.Kind.Required) required_index += 1; - if (option.short != null) continue; - if (option.long != null) continue; + if (mem.startsWith(u8, arg, "--")) { + arg = arg[2..]; + kind = Arg.Kind.Long; + } else if (mem.startsWith(u8, arg, "-")) { + arg = arg[1..]; + kind = Arg.Kind.Short; + } - try option.parse(&result, arg); - required = newRequired(option, required, required_index); - break :success; - } - }, - Arg.Kind.Short => { - if (arg.len == 0) return error.FoundShortOptionWithNoName; - short_arg_loop: for (arg[0..arg.len - 1]) |short_arg| { - var required_index = usize(0); - inline for (options) |option| { - defer if (option.kind == OptionT.Kind.Required) required_index += 1; - const short = option.short ?? continue; - if (short_arg == short) { - if (option.takes_value) return error.OptionMissingValue; - - try option.parse(&result, []u8{}); - required = newRequired(option, required, required_index); - continue :short_arg_loop; - } - } + if (kind == Arg.Kind.Value) + break :blk Arg { .arg = arg, .kind = kind, .after_eql = null }; - return error.InvalidArgument; - } - const last_arg = arg[arg.len - 1]; + if (mem.indexOfScalar(u8, arg, '=')) |index| { + break :blk Arg { .arg = arg[0..index], .kind = kind, .after_eql = arg[index + 1..] }; + } else { + break :blk Arg { .arg = arg, .kind = kind, .after_eql = null }; + } + }; + const arg = arg_info.arg; + const kind = arg_info.kind; + const after_eql = arg_info.after_eql; + + success: { + switch (kind) { + // TODO: Handle subcommands + Arg.Kind.Value => { + var required_index = usize(0); + inline for (command.arguments) |option| { + defer if (option.required) required_index += 1; + if (option.short != null) continue; + if (option.long != null) continue; + + try option.parse(&result, arg); + required = newRequired(option, required, required_index); + break :success; + } + }, + Arg.Kind.Short => { + if (arg.len == 0) return error.FoundShortOptionWithNoName; + short_arg_loop: for (arg[0..arg.len - 1]) |short_arg| { var required_index = usize(0); - inline for (options) |option| { - defer if (option.kind == OptionT.Kind.Required) required_index += 1; + inline for (command.arguments) |option| { + defer if (option.required) required_index += 1; const short = option.short ?? continue; + if (short_arg == short) { + if (option.takes_value) return error.OptionMissingValue; - if (last_arg == short) { - if (option.takes_value) { - const value = after_eql ?? it.next() ?? return error.OptionMissingValue; - try option.parse(&result, value); - } else { - try option.parse(&result, []u8{}); - } - + *getFieldPtr(Result, &result, option.field) = true; required = newRequired(option, required, required_index); - break :success; + continue :short_arg_loop; } } - }, - Arg.Kind.Long => { - var required_index = usize(0); - inline for (options) |option| { - defer if (option.kind == OptionT.Kind.Required) required_index += 1; - const long = option.long ?? continue; - if (mem.eql(u8, arg, long)) { - if (option.takes_value) { - const value = after_eql ?? it.next() ?? return error.OptionMissingValue; - try option.parse(&result, value); - } else { - try option.parse(&result, []u8{}); - } - required = newRequired(option, required, required_index); - break :success; + return error.InvalidArgument; + } + + const last_arg = arg[arg.len - 1]; + var required_index = usize(0); + inline for (command.arguments) |option| { + defer if (option.required) required_index += 1; + const short = option.short ?? continue; + + if (last_arg == short) { + if (option.takes_value) { + const value = after_eql ?? it.next() ?? return error.OptionMissingValue; + *getFieldPtr(Result, &result, option.field) = try strToValue(FieldType(Result, option.field), value); + } else { + *getFieldPtr(Result, &result, option.field) = true; } + + required = newRequired(option, required, required_index); + break :success; } } - } + }, + Arg.Kind.Long => { + var required_index = usize(0); + inline for (command.arguments) |option| { + defer if (option.required) required_index += 1; + const long = option.long ?? continue; + + if (mem.eql(u8, arg, long)) { + if (option.takes_value) { + const value = after_eql ?? it.next() ?? return error.OptionMissingValue; + *getFieldPtr(Result, &result, option.field) = try strToValue(FieldType(Result, option.field), value); + } else { + *getFieldPtr(Result, &result, option.field) = true; + } - return error.InvalidArgument; + required = newRequired(option, required, required_index); + break :success; + } + } + } } - } - if (required != 0) { - return error.RequiredArgumentWasntHandled; + return error.InvalidArgument; } + } - return result; + if (required != 0) { + return error.RequiredArgumentWasntHandled; } - // TODO: - // * Usage - // * Description - pub fn help(out_stream: var) !void { - const equal_value : []const u8 = "=OPTION"; - const longest_long = comptime blk: { - var res = usize(0); - for (options) |option| { - const long = option.long ?? continue; - var len = long.len; - - if (option.takes_value) - len += equal_value.len; - - if (res < len) - res = len; - } + return result; + } - break :blk res; - }; + fn FieldType(comptime Result: type, comptime field: []const u8) type { + var i = usize(0); + inline while (i < @memberCount(Result)) : (i += 1) { + if (mem.eql(u8, @memberName(Result, i), field)) + return @memberType(Result, i); + } - for (options) |option| { - if (option.short == null and option.long == null) continue; + @compileError("Field not found!"); + } - try out_stream.print(" "); - if (option.short) |short| { - try out_stream.print("-{c}", short); - } else { - try out_stream.print(" "); - } + fn getFieldPtr(comptime Result: type, res: &Result, comptime field: []const u8) &FieldType(Result, field) { + return @intToPtr(&FieldType(Result, field), @ptrToInt(res) + @offsetOf(Result, field)); + } - if (option.short != null and option.long != null) { - try out_stream.print(", "); - } else { - try out_stream.print(" "); - } + fn strToValue(comptime Result: type, str: []const u8) !Result { + const TypeId = builtin.TypeId; + switch (@typeId(Result)) { + TypeId.Type, TypeId.Void, TypeId.NoReturn, TypeId.Pointer, + TypeId.Array, TypeId.Struct, TypeId.UndefinedLiteral, + TypeId.NullLiteral, TypeId.ErrorUnion, TypeId.ErrorSet, + TypeId.Union, TypeId.Fn, TypeId.Namespace, TypeId.Block, + TypeId.BoundFn, TypeId.ArgTuple, TypeId.Opaque, TypeId.Promise => @compileError("Type not supported!"), + + TypeId.Bool => { + if (mem.eql(u8, "true", str)) + return true; + if (mem.eql(u8, "false", str)) + return false; + + return error.CannotParseStringAsBool; + }, + TypeId.Int, TypeId.IntLiteral => return fmt.parseInt(Result, str, 10), + TypeId.Float, TypeId.FloatLiteral => return fmt.parseFloat(Result, str), + TypeId.Nullable => { + if (mem.eql(u8, "null", str)) + return null; + + return strToValue(Result.Child, str); + }, + TypeId.Enum => @compileError("TODO: Implement str to enum"), + } + } - // We need to ident by: - // "-- ".len - const missing_spaces = comptime blk: { - var res = longest_long + 3; - if (option.long) |long| { - try out_stream.print("--{}", long); - res -= 2 + long.len; - - if (option.takes_value) { - try out_stream.print("{}", equal_value); - res -= equal_value.len; - } - } + fn newRequired(argument: &const Argument, old_required: u128, index: usize) u128 { + if (argument.required) + return old_required & ~(u128(1) << u7(index)); - break :blk res; - }; + return old_required; + } - var i = usize(0); - while (i < missing_spaces) : (i += 1) { - try out_stream.print(" "); + pub const Builder = struct { + result: Command, + + pub fn init(command_name: []const u8) Builder { + return Builder { + .result = Command { + .field = null, + .name = command_name, + .arguments = []Argument{ }, + .sub_commands = []Command{ }, } - try out_stream.print("{}\n", option.help); - } + }; + } + + pub fn field(builder: &const Builder, field_name: []const u8) Builder { + var res = *builder; + res.result.field = field_name; + return res; + } + + pub fn name(builder: &const Builder, n: []const u8) Builder { + var res = *builder; + res.result.name = n; + return res; + } + + pub fn arguments(builder: &const Builder, args: []const Argument) Builder { + var res = *builder; + res.result.arguments = args; + return res; + } + + pub fn subCommands(builder: &const Builder, commands: []const Command) Builder { + var res = *builder; + res.result.commands = commands; + return res; + } + + pub fn build(builder: &const Builder) Command { + return builder.result; } }; -} +}; + +pub const Argument = struct { + field: []const u8, + help: []const u8, + takes_value: bool, + required: bool, + short: ?u8, + long: ?[]const u8, + + pub const Builder = struct { + result: Argument, + + pub fn init(field_name: []const u8) Builder { + return Builder { + .result = Argument { + .field = field_name, + .help = "", + .takes_value = false, + .required = false, + .short = null, + .long = null, + } + }; + } -test "clap.parse.Example" { - const Color = struct { - const Self = this; + pub fn field(builder: &const Builder, field_name: []const u8) Builder { + var res = *builder; + res.result.field = field_name; + return res; + } - r: u8, g: u8, b: u8, + pub fn help(builder: &const Builder, text: []const u8) Builder { + var res = *builder; + res.result.help = text; + return res; + } + + pub fn takesValue(builder: &const Builder, takes_value: bool) Builder { + var res = *builder; + res.result.takes_value = takes_value; + return res; + } - fn rFromStr(color: &Self, str: []const u8) !void { - color.r = try fmt.parseInt(u8, str, 10); + pub fn required(builder: &const Builder, is_required: bool) Builder { + var res = *builder; + res.result.required = is_required; + return res; } - fn gFromStr(color: &Self, str: []const u8) !void { - color.g = try fmt.parseInt(u8, str, 10); + pub fn short(builder: &const Builder, name: u8) Builder { + var res = *builder; + res.result.short = name; + return res; } - fn bFromStr(color: &Self, str: []const u8) !void { - color.b = try fmt.parseInt(u8, str, 10); + pub fn long(builder: &const Builder, name: []const u8) Builder { + var res = *builder; + res.result.long = name; + return res; } - // TODO: There is a segfault when we try to use the error set: @typeOf(fmt.parseInt).ReturnType.ErrorSet - fn setMax(color: &Self, str: []const u8) !void { - color.r = try fmt.parseInt(u8, "255", 10); - color.g = try fmt.parseInt(u8, "255", 10); - color.b = try fmt.parseInt(u8, "255", 10); + pub fn build(builder: &const Builder) Argument { + return builder.result; } }; - const Error = @typeOf(Color.setMax).ReturnType.ErrorSet; +}; + +test "clap.parse.Example" { + const Color = struct { + r: u8, g: u8, b: u8, max: bool + }; const Case = struct { args: []const []const u8, res: Color, err: ?error }; const cases = []Case { Case { .args = [][]const u8 { "-r", "100", "-g", "100", "-b", "100", }, - .res = Color { .r = 100, .g = 100, .b = 100 }, + .res = Color { .r = 100, .g = 100, .b = 100, .max = false }, .err = null, }, Case { .args = [][]const u8 { "--red", "100", "-g", "100", "--blue", "50", }, - .res = Color { .r = 100, .g = 100, .b = 50 }, + .res = Color { .r = 100, .g = 100, .b = 50, .max = false }, .err = null, }, Case { .args = [][]const u8 { "--red=100", "-g=100", "--blue=50", }, - .res = Color { .r = 100, .g = 100, .b = 50 }, + .res = Color { .r = 100, .g = 100, .b = 50, .max = false }, .err = null, }, Case { .args = [][]const u8 { "-g", "200", "--blue", "100", "--red", "100", }, - .res = Color { .r = 100, .g = 200, .b = 100 }, + .res = Color { .r = 100, .g = 200, .b = 100, .max = false }, .err = null, }, Case { .args = [][]const u8 { "-r", "200", "-r", "255" }, - .res = Color { .r = 255, .g = 0, .b = 0 }, + .res = Color { .r = 255, .g = 0, .b = 0, .max = false }, .err = null, }, Case { .args = [][]const u8 { "-mr", "100" }, - .res = Color { .r = 100, .g = 255, .b = 255 }, + .res = Color { .r = 100, .g = 0, .b = 0, .max = true }, .err = null, }, Case { .args = [][]const u8 { "-mr=100" }, - .res = Color { .r = 100, .g = 255, .b = 255 }, + .res = Color { .r = 100, .g = 0, .b = 0, .max = true }, .err = null, }, Case { .args = [][]const u8 { "-g", "200", "-b", "255" }, - .res = Color { .r = 0, .g = 0, .b = 0 }, + .res = Color { .r = 0, .g = 0, .b = 0, .max = false }, .err = error.RequiredArgumentWasntHandled, }, Case { .args = [][]const u8 { "-p" }, - .res = Color { .r = 0, .g = 0, .b = 0 }, + .res = Color { .r = 0, .g = 0, .b = 0, .max = false }, .err = error.InvalidArgument, }, Case { .args = [][]const u8 { "-g" }, - .res = Color { .r = 0, .g = 0, .b = 0 }, + .res = Color { .r = 0, .g = 0, .b = 0, .max = false }, .err = error.OptionMissingValue, }, Case { .args = [][]const u8 { "-" }, - .res = Color { .r = 0, .g = 0, .b = 0 }, + .res = Color { .r = 0, .g = 0, .b = 0, .max = false }, .err = error.FoundShortOptionWithNoName, }, Case { .args = [][]const u8 { "-rg", "100" }, - .res = Color { .r = 0, .g = 0, .b = 0 }, + .res = Color { .r = 0, .g = 0, .b = 0, .max = false }, .err = error.OptionMissingValue, }, }; - const COption = Option(Color, Error); - const Clap = Parser(Color, Error, - Color { .r = 0, .g = 0, .b = 0 }, - comptime []COption { - COption.init(Color.rFromStr) - .setHelp("The amount of red in our color") - .setShort('r') - .setLong("red") - .takesValue(true) - .setKind(COption.Kind.Required), - COption.init(Color.gFromStr) - .setHelp("The amount of green in our color") - .setShort('g') - .setLong("green") - .takesValue(true), - COption.init(Color.bFromStr) - .setHelp("The amount of blue in our color") - .setShort('b') - .setLong("blue") - .takesValue(true), - COption.init(Color.setMax) - .setHelp("Set all values to max") - .setShort('m') - .setLong("max"), - } - ); + const clap = comptime Clap(Color).Builder + .init( + Color { + .r = 0, + .b = 0, + .g = 0, + .max = false, + } + ) + .command( + Command.Builder + .init("color") + .arguments( + []Argument { + Argument.Builder + .init("r") + .help("The amount of red in our color") + .short('r') + .long("red") + .takesValue(true) + .required(true) + .build(), + Argument.Builder + .init("g") + .help("The amount of green in our color") + .short('g') + .long("green") + .takesValue(true) + .build(), + Argument.Builder + .init("b") + .help("The amount of blue in our color") + .short('b') + .long("blue") + .takesValue(true) + .build(), + Argument.Builder + .init("max") + .help("Set all values to max") + .short('m') + .long("max") + .build(), + } + ) + .build() + ) + .build(); for (cases) |case, i| { - if (Clap.parse(case.args)) |res| { + if (clap.parse(case.args)) |res| { assert(case.err == null); assert(res.r == case.res.r); assert(res.g == case.res.g); assert(res.b == case.res.b); + assert(res.max == case.res.max); } else |err| { assert(err == ??case.err); } -- cgit v1.2.3