mirror of
https://github.com/MasterQ32/zig-args.git
synced 2024-09-08 00:25:42 +02:00
1167 lines
43 KiB
Zig
1167 lines
43 KiB
Zig
const std = @import("std");
|
|
|
|
/// Parses arguments for the given specification and our current process.
|
|
/// - `Spec` is the configuration of the arguments.
|
|
/// - `allocator` is the allocator that is used to allocate all required memory
|
|
/// - `error_handling` defines how parser errors will be handled.
|
|
pub fn parseForCurrentProcess(comptime Spec: type, allocator: std.mem.Allocator, comptime error_handling: ErrorHandling) !ParseArgsResult(Spec, null) {
|
|
// Use argsWithAllocator for portability.
|
|
// All data allocated by the ArgIterator is freed at the end of the function.
|
|
// Data returned to the user is always duplicated using the allocator.
|
|
var args = try std.process.argsWithAllocator(allocator);
|
|
defer args.deinit();
|
|
|
|
const executable_name = args.next() orelse {
|
|
try error_handling.process(error.NoExecutableName, Error{
|
|
.option = "",
|
|
.kind = .missing_executable_name,
|
|
});
|
|
|
|
// we do not assume any more arguments appear here anyways...
|
|
return error.NoExecutableName;
|
|
};
|
|
|
|
var result = try parseInternal(Spec, null, &args, allocator, error_handling);
|
|
|
|
result.executable_name = try allocator.dupeZ(u8, executable_name);
|
|
|
|
return result;
|
|
}
|
|
|
|
/// Parses arguments for the given specification and our current process.
|
|
/// - `Spec` is the configuration of the arguments.
|
|
/// - `allocator` is the allocator that is used to allocate all required memory
|
|
/// - `error_handling` defines how parser errors will be handled.
|
|
pub fn parseWithVerbForCurrentProcess(comptime Spec: type, comptime Verb: type, allocator: std.mem.Allocator, comptime error_handling: ErrorHandling) !ParseArgsResult(Spec, Verb) {
|
|
// Use argsWithAllocator for portability.
|
|
// All data allocated by the ArgIterator is freed at the end of the function.
|
|
// Data returned to the user is always duplicated using the allocator.
|
|
var args = try std.process.argsWithAllocator(allocator);
|
|
defer args.deinit();
|
|
|
|
const executable_name = args.next() orelse {
|
|
try error_handling.process(error.NoExecutableName, Error{
|
|
.option = "",
|
|
.kind = .missing_executable_name,
|
|
});
|
|
|
|
// we do not assume any more arguments appear here anyways...
|
|
return error.NoExecutableName;
|
|
};
|
|
|
|
var result = try parseInternal(Spec, Verb, &args, allocator, error_handling);
|
|
|
|
result.executable_name = try allocator.dupeZ(u8, executable_name);
|
|
|
|
return result;
|
|
}
|
|
|
|
/// Parses arguments for the given specification.
|
|
/// - `Generic` is the configuration of the arguments.
|
|
/// - `args_iterator` is a pointer to an std.process.ArgIterator that will yield the command line arguments.
|
|
/// - `allocator` is the allocator that is used to allocate all required memory
|
|
/// - `error_handling` defines how parser errors will be handled.
|
|
///
|
|
/// Note that `.executable_name` in the result will not be set!
|
|
pub fn parse(comptime Generic: type, args_iterator: anytype, allocator: std.mem.Allocator, comptime error_handling: ErrorHandling) !ParseArgsResult(Generic, null) {
|
|
return parseInternal(Generic, null, args_iterator, allocator, error_handling);
|
|
}
|
|
|
|
/// Parses arguments for the given specification using a `Verb` method.
|
|
/// This means that the first positional argument is interpreted as a verb, that can
|
|
/// be considered a sub-command that provides more specific options.
|
|
/// - `Generic` is the configuration of the arguments.
|
|
/// - `Verb` is the configuration of the verbs.
|
|
/// - `args_iterator` is a pointer to an std.process.ArgIterator that will yield the command line arguments.
|
|
/// - `allocator` is the allocator that is used to allocate all required memory
|
|
/// - `error_handling` defines how parser errors will be handled.
|
|
///
|
|
/// Note that `.executable_name` in the result will not be set!
|
|
pub fn parseWithVerb(comptime Generic: type, comptime Verb: type, args_iterator: anytype, allocator: std.mem.Allocator, comptime error_handling: ErrorHandling) !ParseArgsResult(Generic, Verb) {
|
|
return parseInternal(Generic, Verb, args_iterator, allocator, error_handling);
|
|
}
|
|
|
|
/// Same as parse, but with anytype argument for testability
|
|
fn parseInternal(comptime Generic: type, comptime MaybeVerb: ?type, args_iterator: anytype, allocator: std.mem.Allocator, comptime error_handling: ErrorHandling) !ParseArgsResult(Generic, MaybeVerb) {
|
|
var result = ParseArgsResult(Generic, MaybeVerb){
|
|
.arena = std.heap.ArenaAllocator.init(allocator),
|
|
.options = Generic{},
|
|
.verb = if (MaybeVerb != null) null else {}, // no verb by default
|
|
.positionals = undefined,
|
|
.executable_name = null,
|
|
};
|
|
errdefer result.arena.deinit();
|
|
var result_arena_allocator = result.arena.allocator();
|
|
|
|
var arglist = std.ArrayList([:0]const u8).init(allocator);
|
|
defer arglist.deinit();
|
|
|
|
var last_error: ?anyerror = null;
|
|
|
|
while (args_iterator.next()) |item| {
|
|
if (std.mem.startsWith(u8, item, "--")) {
|
|
if (std.mem.eql(u8, item, "--")) {
|
|
// double hyphen is considered 'everything from here now is positional'
|
|
result.raw_start_index = arglist.items.len;
|
|
break;
|
|
}
|
|
|
|
const Pair = struct {
|
|
name: []const u8,
|
|
value: ?[]const u8,
|
|
};
|
|
|
|
const pair = if (std.mem.indexOf(u8, item, "=")) |index|
|
|
Pair{
|
|
.name = item[2..index],
|
|
.value = item[index + 1 ..],
|
|
}
|
|
else
|
|
Pair{
|
|
.name = item[2..],
|
|
.value = null,
|
|
};
|
|
|
|
var found = false;
|
|
inline for (std.meta.fields(Generic)) |fld| {
|
|
if (std.mem.eql(u8, pair.name, fld.name)) {
|
|
try parseOption(Generic, result_arena_allocator, &result.options, args_iterator, error_handling, &last_error, fld.name, pair.value);
|
|
found = true;
|
|
}
|
|
}
|
|
|
|
if (MaybeVerb) |Verb| {
|
|
if (result.verb) |*verb| {
|
|
if (!found) {
|
|
const Tag = std.meta.Tag(Verb);
|
|
inline for (std.meta.fields(Verb)) |verb_info| {
|
|
if (verb.* == @field(Tag, verb_info.name)) {
|
|
if (comptime canHaveFieldsAndIsNotZeroSized(verb_info.type)) {
|
|
inline for (std.meta.fields(verb_info.type)) |fld| {
|
|
if (std.mem.eql(u8, pair.name, fld.name)) {
|
|
try parseOption(
|
|
verb_info.type,
|
|
result_arena_allocator,
|
|
&@field(verb.*, verb_info.name),
|
|
args_iterator,
|
|
error_handling,
|
|
&last_error,
|
|
fld.name,
|
|
pair.value,
|
|
);
|
|
found = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!found) {
|
|
last_error = error.EncounteredUnknownArgument;
|
|
try error_handling.process(error.EncounteredUnknownArgument, Error{
|
|
.option = pair.name,
|
|
.kind = .unknown,
|
|
});
|
|
}
|
|
} else if (std.mem.startsWith(u8, item, "-")) {
|
|
if (std.mem.eql(u8, item, "-")) {
|
|
// single hyphen is considered a positional argument
|
|
try arglist.append(try result_arena_allocator.dupeZ(u8, item));
|
|
} else {
|
|
var any_shorthands = false;
|
|
for (item[1..], 0..) |char, index| {
|
|
var option_name = [2]u8{ '-', char };
|
|
var found = false;
|
|
if (@hasDecl(Generic, "shorthands")) {
|
|
any_shorthands = true;
|
|
inline for (std.meta.fields(@TypeOf(Generic.shorthands))) |fld| {
|
|
if (fld.name.len != 1)
|
|
@compileError("All shorthand fields must be exactly one character long!");
|
|
if (fld.name[0] == char) {
|
|
const real_name = @field(Generic.shorthands, fld.name);
|
|
const real_fld_type = @TypeOf(@field(result.options, real_name));
|
|
|
|
// -2 because we stripped of the "-" at the beginning
|
|
if (requiresArg(real_fld_type) and index != item.len - 2) {
|
|
last_error = error.EncounteredUnexpectedArgument;
|
|
try error_handling.process(error.EncounteredUnexpectedArgument, Error{
|
|
.option = &option_name,
|
|
.kind = .invalid_placement,
|
|
});
|
|
} else {
|
|
try parseOption(Generic, result_arena_allocator, &result.options, args_iterator, error_handling, &last_error, real_name, null);
|
|
}
|
|
|
|
found = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (MaybeVerb) |Verb| {
|
|
if (result.verb) |*verb| {
|
|
if (!found) {
|
|
const Tag = std.meta.Tag(Verb);
|
|
inline for (std.meta.fields(Verb)) |verb_info| {
|
|
const VerbType = verb_info.type;
|
|
if (comptime canHaveFieldsAndIsNotZeroSized(VerbType)) {
|
|
if (verb.* == @field(Tag, verb_info.name)) {
|
|
const target_value = &@field(verb.*, verb_info.name);
|
|
if (@hasDecl(VerbType, "shorthands")) {
|
|
any_shorthands = true;
|
|
inline for (std.meta.fields(@TypeOf(VerbType.shorthands))) |fld| {
|
|
if (fld.name.len != 1)
|
|
@compileError("All shorthand fields must be exactly one character long!");
|
|
if (fld.name[0] == char) {
|
|
const real_name = @field(VerbType.shorthands, fld.name);
|
|
const real_fld_type = @TypeOf(@field(target_value.*, real_name));
|
|
|
|
// -2 because we stripped of the "-" at the beginning
|
|
if (requiresArg(real_fld_type) and index != item.len - 2) {
|
|
last_error = error.EncounteredUnexpectedArgument;
|
|
try error_handling.process(error.EncounteredUnexpectedArgument, Error{
|
|
.option = &option_name,
|
|
.kind = .invalid_placement,
|
|
});
|
|
} else {
|
|
try parseOption(VerbType, result_arena_allocator, target_value, args_iterator, error_handling, &last_error, real_name, null);
|
|
}
|
|
last_error = null; // we need to reset that error here, as it was set previously
|
|
found = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (!found) {
|
|
last_error = error.EncounteredUnknownArgument;
|
|
try error_handling.process(error.EncounteredUnknownArgument, Error{
|
|
.option = &option_name,
|
|
.kind = .unknown,
|
|
});
|
|
}
|
|
}
|
|
if (!any_shorthands) {
|
|
try error_handling.process(error.EncounteredUnsupportedArgument, Error{
|
|
.option = item,
|
|
.kind = .unsupported,
|
|
});
|
|
}
|
|
}
|
|
} else {
|
|
if (MaybeVerb) |Verb| {
|
|
if (result.verb == null) {
|
|
inline for (std.meta.fields(Verb)) |fld| {
|
|
if (std.mem.eql(u8, item, fld.name)) {
|
|
// found active verb, default-initialize it
|
|
result.verb = @unionInit(Verb, fld.name, fld.type{});
|
|
}
|
|
}
|
|
|
|
if (result.verb == null) {
|
|
try error_handling.process(error.EncounteredUnknownVerb, Error{
|
|
.option = "verb",
|
|
.kind = .unsupported,
|
|
});
|
|
}
|
|
|
|
continue;
|
|
}
|
|
}
|
|
|
|
try arglist.append(try result_arena_allocator.dupeZ(u8, item));
|
|
}
|
|
}
|
|
|
|
if (last_error != null)
|
|
return error.InvalidArguments;
|
|
|
|
// This will consume the rest of the arguments as positional ones.
|
|
// Only executes when the above loop is broken.
|
|
while (args_iterator.next()) |item| {
|
|
try arglist.append(try result_arena_allocator.dupeZ(u8, item));
|
|
}
|
|
|
|
result.positionals = try arglist.toOwnedSlice();
|
|
return result;
|
|
}
|
|
|
|
fn canHaveFieldsAndIsNotZeroSized(comptime T: type) bool {
|
|
return switch (@typeInfo(T)) {
|
|
.@"struct", .@"union", .@"enum", .error_set => @sizeOf(T) != 0,
|
|
else => false,
|
|
};
|
|
}
|
|
|
|
/// The return type of the argument parser.
|
|
pub fn ParseArgsResult(comptime Generic: type, comptime MaybeVerb: ?type) type {
|
|
if (@typeInfo(Generic) != .@"struct")
|
|
@compileError("Generic argument definition must be a struct");
|
|
|
|
if (MaybeVerb) |Verb| {
|
|
const ti: std.builtin.Type = @typeInfo(Verb);
|
|
if (ti != .@"union" or ti.@"union".tag_type == null)
|
|
@compileError("Verb must be a tagged union");
|
|
}
|
|
|
|
return struct {
|
|
const Self = @This();
|
|
|
|
/// Exports the type of options.
|
|
pub const GenericOptions = Generic;
|
|
pub const Verbs = MaybeVerb orelse void;
|
|
|
|
arena: std.heap.ArenaAllocator,
|
|
|
|
/// The options with either default or set values.
|
|
options: Generic,
|
|
|
|
/// The verb that was parsed or `null` if no first positional was provided.
|
|
/// Is `void` when verb parsing is disabled
|
|
verb: if (MaybeVerb) |Verb| ?Verb else void,
|
|
|
|
/// The positional arguments that were passed to the process.
|
|
positionals: [][:0]const u8,
|
|
|
|
// The index of the first "raw arg", meaning the first arg after "--"
|
|
raw_start_index: ?usize = null,
|
|
|
|
/// Name of the executable file (or: zeroth argument)
|
|
executable_name: ?[:0]const u8,
|
|
|
|
pub fn deinit(self: Self) void {
|
|
self.arena.child_allocator.free(self.positionals);
|
|
|
|
if (self.executable_name) |n|
|
|
self.arena.child_allocator.free(n);
|
|
|
|
self.arena.deinit();
|
|
}
|
|
};
|
|
}
|
|
|
|
/// Returns true if the given type requires an argument to be parsed.
|
|
fn requiresArg(comptime T: type) bool {
|
|
const H = struct {
|
|
fn doesArgTypeRequireArg(comptime Type: type) bool {
|
|
if (Type == []const u8)
|
|
return true;
|
|
|
|
return switch (@as(std.builtin.TypeId, @typeInfo(Type))) {
|
|
.int, .float, .@"enum" => true,
|
|
.bool => false,
|
|
.@"struct", .@"union" => true,
|
|
.pointer => true,
|
|
else => @compileError(@typeName(Type) ++ " is not a supported argument type!"),
|
|
};
|
|
}
|
|
};
|
|
|
|
const ti = @typeInfo(T);
|
|
if (ti == .optional) {
|
|
return H.doesArgTypeRequireArg(ti.optional.child);
|
|
} else {
|
|
return H.doesArgTypeRequireArg(T);
|
|
}
|
|
}
|
|
|
|
/// Parses a boolean option.
|
|
fn parseBoolean(str: []const u8) !bool {
|
|
return switch (str.len) {
|
|
1 => switch (str[0]) {
|
|
'y', 'Y', 't', 'T' => true,
|
|
'n', 'N', 'f', 'F' => false,
|
|
else => error.NotABooleanValue,
|
|
},
|
|
2 => if (std.ascii.eqlIgnoreCase("no", str)) false else error.NotABooleanValue,
|
|
3 => if (std.ascii.eqlIgnoreCase("yes", str)) true else error.NotABooleanValue,
|
|
4 => if (std.ascii.eqlIgnoreCase("true", str)) true else error.NotABooleanValue,
|
|
5 => if (std.ascii.eqlIgnoreCase("false", str)) false else error.NotABooleanValue,
|
|
else => error.NotABooleanValue,
|
|
};
|
|
}
|
|
|
|
/// Parses an int option.
|
|
fn parseInt(comptime T: type, str: []const u8) !T {
|
|
var buf = str;
|
|
var multiplier: T = 1;
|
|
|
|
if (buf.len != 0) {
|
|
var base1024 = false;
|
|
if (std.ascii.toLower(buf[buf.len - 1]) == 'i') { //ki vs k for instance
|
|
buf.len -= 1;
|
|
base1024 = true;
|
|
}
|
|
if (buf.len != 0) {
|
|
const pow: u3 = switch (buf[buf.len - 1]) {
|
|
'k', 'K' => 1, //kilo
|
|
'm', 'M' => 2, //mega
|
|
'g', 'G' => 3, //giga
|
|
't', 'T' => 4, //tera
|
|
'p', 'P' => 5, //peta
|
|
else => 0,
|
|
};
|
|
|
|
if (pow != 0) {
|
|
buf.len -= 1;
|
|
|
|
if (comptime std.math.maxInt(T) < 1024)
|
|
return error.Overflow;
|
|
const base: T = if (base1024) 1024 else 1000;
|
|
multiplier = try std.math.powi(T, base, @as(T, @intCast(pow)));
|
|
}
|
|
}
|
|
}
|
|
|
|
const ret: T = switch (@typeInfo(T).int.signedness) {
|
|
.signed => try std.fmt.parseInt(T, buf, 0),
|
|
.unsigned => try std.fmt.parseUnsigned(T, buf, 0),
|
|
};
|
|
|
|
return try std.math.mul(T, ret, multiplier);
|
|
}
|
|
|
|
test "parseInt" {
|
|
const tst = std.testing;
|
|
|
|
try tst.expectEqual(@as(i32, 50), try parseInt(i32, "50"));
|
|
try tst.expectEqual(@as(i32, 6000), try parseInt(i32, "6k"));
|
|
try tst.expectEqual(@as(u32, 2048), try parseInt(u32, "0x2KI"));
|
|
try tst.expectEqual(@as(i8, 0), try parseInt(i8, "0"));
|
|
try tst.expectEqual(@as(usize, 10_000_000_000), try parseInt(usize, "0xAg"));
|
|
try tst.expectError(error.Overflow, parseInt(i2, "1m"));
|
|
try tst.expectError(error.Overflow, parseInt(u16, "1Ti"));
|
|
}
|
|
|
|
/// Converts an argument value to the target type.
|
|
fn convertArgumentValue(comptime T: type, allocator: std.mem.Allocator, textInput: []const u8) !T {
|
|
switch (@typeInfo(T)) {
|
|
.optional => |opt| return try convertArgumentValue(opt.child, allocator, textInput),
|
|
.bool => if (textInput.len > 0)
|
|
return try parseBoolean(textInput)
|
|
else
|
|
return true, // boolean options are always true
|
|
.int => return try parseInt(T, textInput),
|
|
.float => return try std.fmt.parseFloat(T, textInput),
|
|
.@"enum" => {
|
|
if (@hasDecl(T, "parse")) {
|
|
return try T.parse(textInput);
|
|
} else {
|
|
return std.meta.stringToEnum(T, textInput) orelse return error.InvalidEnumeration;
|
|
}
|
|
},
|
|
.@"struct", .@"union" => {
|
|
if (@hasDecl(T, "parse")) {
|
|
return try T.parse(textInput);
|
|
} else {
|
|
@compileError(@typeName(T) ++ " has no public visible `fn parse([]const u8) !T`!");
|
|
}
|
|
},
|
|
.pointer => |ptr| switch (ptr.size) {
|
|
.Slice => {
|
|
if (ptr.child != u8) {
|
|
@compileError(@typeName(T) ++ " is not a supported pointer type, only slices of u8 are supported");
|
|
}
|
|
|
|
// If the type contains a sentinel dupe the text input to a new buffer.
|
|
// This is equivalent to allocator.dupeZ but works with any sentinel.
|
|
if (comptime std.meta.sentinel(T)) |sentinel| {
|
|
const data = try allocator.alloc(u8, textInput.len + 1);
|
|
@memcpy(data[0..textInput.len], textInput);
|
|
data[textInput.len] = sentinel;
|
|
|
|
return data[0..textInput.len :sentinel];
|
|
}
|
|
|
|
// Otherwise the type is []const u8 so just return the text input.
|
|
return textInput;
|
|
},
|
|
else => @compileError(@typeName(T) ++ " is not a supported pointer type!"),
|
|
},
|
|
else => @compileError(@typeName(T) ++ " is not a supported argument type!"),
|
|
}
|
|
}
|
|
|
|
/// Parses an option value into the correct type.
|
|
fn parseOption(
|
|
comptime Spec: type,
|
|
arena: std.mem.Allocator,
|
|
target_struct: *Spec,
|
|
args: anytype,
|
|
comptime error_handling: ErrorHandling,
|
|
last_error: *?anyerror,
|
|
/// The name of the option that is currently parsed.
|
|
comptime name: []const u8,
|
|
/// Optional pre-defined value for options that use `--foo=bar`
|
|
value: ?[]const u8,
|
|
) !void {
|
|
const field_type = @TypeOf(@field(target_struct, name));
|
|
|
|
const final_value = if (value) |val| blk: {
|
|
// use the literal value
|
|
const res = try arena.dupeZ(u8, val);
|
|
break :blk res;
|
|
} else if (requiresArg(field_type)) blk: {
|
|
// fetch from parser
|
|
const val = args.next();
|
|
if (val == null or std.mem.eql(u8, val.?, "--")) {
|
|
last_error.* = error.MissingArgument;
|
|
try error_handling.process(error.MissingArgument, Error{
|
|
.option = "--" ++ name,
|
|
.kind = .missing_argument,
|
|
});
|
|
return;
|
|
}
|
|
|
|
const res = try arena.dupeZ(u8, val.?);
|
|
break :blk res;
|
|
} else blk: {
|
|
// argument is "empty"
|
|
break :blk "";
|
|
};
|
|
|
|
@field(target_struct, name) = convertArgumentValue(field_type, arena, final_value) catch |err| {
|
|
last_error.* = err;
|
|
try error_handling.process(err, Error{
|
|
.option = "--" ++ name,
|
|
.kind = .{ .invalid_value = final_value },
|
|
});
|
|
// we couldn't parse the value, so we return a undefined value as we have signalled an
|
|
// error and won't return this anyways.
|
|
return;
|
|
};
|
|
}
|
|
|
|
/// A collection of errors that were encountered while parsing arguments.
|
|
pub const ErrorCollection = struct {
|
|
const Self = @This();
|
|
|
|
arena: std.heap.ArenaAllocator,
|
|
list: std.ArrayList(Error),
|
|
|
|
pub fn init(allocator: std.mem.Allocator) Self {
|
|
return Self{
|
|
.arena = std.heap.ArenaAllocator.init(allocator),
|
|
.list = std.ArrayList(Error).init(allocator),
|
|
};
|
|
}
|
|
|
|
pub fn deinit(self: *Self) void {
|
|
self.list.deinit();
|
|
self.arena.deinit();
|
|
self.* = undefined;
|
|
}
|
|
|
|
/// Returns the current enumeration of errors.
|
|
pub fn errors(self: Self) []const Error {
|
|
return self.list.items;
|
|
}
|
|
|
|
/// Appends an error to the collection
|
|
fn insert(self: *Self, err: Error) !void {
|
|
const dupe = Error{
|
|
.option = try self.arena.allocator().dupe(u8, err.option),
|
|
.kind = switch (err.kind) {
|
|
.invalid_value => |v| Error.Kind{
|
|
.invalid_value = try self.arena.allocator().dupe(u8, v),
|
|
},
|
|
// flat copy
|
|
.unknown, .out_of_memory, .unsupported, .invalid_placement, .missing_argument, .missing_executable_name, .unknown_verb => err.kind,
|
|
},
|
|
};
|
|
try self.list.append(dupe);
|
|
}
|
|
};
|
|
|
|
/// An argument parsing error.
|
|
pub const Error = struct {
|
|
const Self = @This();
|
|
|
|
/// The option that yielded the error
|
|
option: []const u8,
|
|
|
|
/// The kind of error, might include additional information
|
|
kind: Kind,
|
|
|
|
pub fn format(self: Self, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void {
|
|
_ = fmt;
|
|
_ = options;
|
|
switch (self.kind) {
|
|
.unknown => try writer.print("The option {s} does not exist", .{self.option}),
|
|
.invalid_value => |value| try writer.print("Invalid value '{s}' for option {s}", .{ value, self.option }),
|
|
.out_of_memory => try writer.print("Out of memory while parsing option {s}", .{self.option}),
|
|
.unsupported => try writer.writeAll("Short command line options are not supported."),
|
|
.invalid_placement => try writer.writeAll("An option with argument must be the last option for short command line options."),
|
|
.missing_argument => try writer.print("Missing argument for option {s}", .{self.option}),
|
|
|
|
.missing_executable_name => try writer.writeAll("Failed to get executable name from the argument list!"),
|
|
.unknown_verb => try writer.print("Unknown verb '{s}'.", .{self.option}),
|
|
}
|
|
}
|
|
|
|
pub const Kind = union(enum) {
|
|
/// When the argument itself is unknown
|
|
unknown,
|
|
|
|
/// When the parsing of an argument value failed
|
|
invalid_value: []const u8,
|
|
|
|
/// When the parsing of an argument value triggered a out of memory error
|
|
out_of_memory,
|
|
|
|
/// When the argument is a short argument and no shorthands are enabled
|
|
unsupported,
|
|
|
|
/// Can only happen when a shorthand for an option requires an argument, but is followed by more shorthands.
|
|
invalid_placement,
|
|
|
|
/// An option was passed that requires an argument, but the option was passed last.
|
|
missing_argument,
|
|
|
|
/// This error has an empty option name and can only happen when parsing the argument list for a process.
|
|
missing_executable_name,
|
|
|
|
/// This error has the verb as an option name and will happen when a verb is provided that is not known.
|
|
unknown_verb,
|
|
};
|
|
};
|
|
|
|
/// The error handling method that should be used.
|
|
pub const ErrorHandling = union(enum) {
|
|
const Self = @This();
|
|
|
|
/// Do not print or process any errors, just
|
|
/// return a fitting error on the first argument mismatch.
|
|
silent,
|
|
|
|
/// Print errors to stderr and return a `error.InvalidArguments`.
|
|
print,
|
|
|
|
/// Collect errors into the error collection and return
|
|
/// `error.InvalidArguments` when any error was encountered.
|
|
collect: *ErrorCollection,
|
|
|
|
/// Forwards the parsing error to a functionm
|
|
forward: fn (err: Error) anyerror!void,
|
|
|
|
/// Processes an error with the given handling method.
|
|
fn process(comptime self: Self, src_error: anytype, err: Error) !void {
|
|
if (@typeInfo(@TypeOf(src_error)) != .error_set)
|
|
@compileError("src_error must be a error union!");
|
|
switch (self) {
|
|
.silent => return src_error,
|
|
.print => try std.io.getStdErr().writer().print("{}\n", .{err}),
|
|
.collect => |collection| try collection.insert(err),
|
|
.forward => |func| try func(err),
|
|
}
|
|
}
|
|
};
|
|
|
|
test {
|
|
std.testing.refAllDecls(@This());
|
|
}
|
|
|
|
test "ErrorCollection" {
|
|
var option_buf = "option".*;
|
|
var invalid_buf = "invalid".*;
|
|
|
|
var ec = ErrorCollection.init(std.testing.allocator);
|
|
defer ec.deinit();
|
|
|
|
try ec.insert(Error{
|
|
.option = &option_buf,
|
|
.kind = .{ .invalid_value = &invalid_buf },
|
|
});
|
|
|
|
option_buf = undefined;
|
|
invalid_buf = undefined;
|
|
|
|
try std.testing.expectEqualStrings("option", ec.errors()[0].option);
|
|
try std.testing.expectEqualStrings("invalid", ec.errors()[0].kind.invalid_value);
|
|
}
|
|
|
|
const TestIterator = struct {
|
|
sequence: []const [:0]const u8,
|
|
index: usize = 0,
|
|
|
|
pub fn init(items: []const [:0]const u8) TestIterator {
|
|
return TestIterator{ .sequence = items };
|
|
}
|
|
|
|
pub fn next(self: *@This()) ?[:0]const u8 {
|
|
if (self.index >= self.sequence.len)
|
|
return null;
|
|
const result = self.sequence[self.index];
|
|
self.index += 1;
|
|
return result;
|
|
}
|
|
};
|
|
|
|
const TestEnum = enum { default, special, slow, fast };
|
|
|
|
const TestGenericOptions = struct {
|
|
output: ?[]const u8 = null,
|
|
@"with-offset": bool = false,
|
|
@"with-hexdump": bool = false,
|
|
@"intermix-source": bool = false,
|
|
numberOfBytes: ?i32 = null,
|
|
signed_number: ?i64 = null,
|
|
unsigned_number: ?u64 = null,
|
|
mode: TestEnum = .default,
|
|
|
|
// This declares short-hand options for single hyphen
|
|
pub const shorthands = .{
|
|
.S = "intermix-source",
|
|
.b = "with-hexdump",
|
|
.O = "with-offset",
|
|
.o = "output",
|
|
};
|
|
};
|
|
|
|
const TestVerb = union(enum) {
|
|
magic: MagicOptions,
|
|
booze: BoozeOptions,
|
|
|
|
const MagicOptions = struct { invoke: bool = false };
|
|
const BoozeOptions = struct {
|
|
cocktail: bool = false,
|
|
longdrink: bool = false,
|
|
|
|
pub const shorthands = .{
|
|
.c = "cocktail",
|
|
.l = "longdrink",
|
|
};
|
|
};
|
|
};
|
|
|
|
test "basic parsing (no verbs)" {
|
|
var titerator = TestIterator.init(&[_][:0]const u8{
|
|
"--output",
|
|
"foobar",
|
|
"--with-offset",
|
|
"--numberOfBytes",
|
|
"-250",
|
|
"--unsigned_number",
|
|
"0xFF00FF",
|
|
"positional 1",
|
|
"--mode",
|
|
"special",
|
|
"positional 2",
|
|
});
|
|
var args = try parseInternal(TestGenericOptions, null, &titerator, std.testing.allocator, .print);
|
|
defer args.deinit();
|
|
|
|
try std.testing.expectEqual(@as(?[:0]const u8, null), args.executable_name);
|
|
try std.testing.expect(void == @TypeOf(args.verb));
|
|
try std.testing.expectEqual(@as(usize, 2), args.positionals.len);
|
|
try std.testing.expectEqualStrings("positional 1", args.positionals[0]);
|
|
try std.testing.expectEqualStrings("positional 2", args.positionals[1]);
|
|
|
|
try std.testing.expectEqualStrings("foobar", args.options.output.?);
|
|
|
|
try std.testing.expectEqual(@as(?i32, -250), args.options.numberOfBytes);
|
|
try std.testing.expectEqual(@as(?u64, 0xFF00FF), args.options.unsigned_number);
|
|
try std.testing.expectEqual(TestEnum.special, args.options.mode);
|
|
|
|
try std.testing.expectEqual(@as(?i64, null), args.options.signed_number);
|
|
|
|
try std.testing.expectEqual(true, args.options.@"with-offset");
|
|
try std.testing.expectEqual(false, args.options.@"with-hexdump");
|
|
try std.testing.expectEqual(false, args.options.@"intermix-source");
|
|
}
|
|
|
|
test "shorthand parsing (no verbs)" {
|
|
var titerator = TestIterator.init(&[_][:0]const u8{
|
|
"-o",
|
|
"foobar",
|
|
"-O",
|
|
"--numberOfBytes",
|
|
"-250",
|
|
"--unsigned_number",
|
|
"0xFF00FF",
|
|
"positional 1",
|
|
"--mode",
|
|
"special",
|
|
"positional 2",
|
|
});
|
|
var args = try parseInternal(TestGenericOptions, null, &titerator, std.testing.allocator, .print);
|
|
defer args.deinit();
|
|
|
|
try std.testing.expectEqual(@as(?[:0]const u8, null), args.executable_name);
|
|
try std.testing.expect(void == @TypeOf(args.verb));
|
|
try std.testing.expectEqual(@as(usize, 2), args.positionals.len);
|
|
try std.testing.expectEqualStrings("positional 1", args.positionals[0]);
|
|
try std.testing.expectEqualStrings("positional 2", args.positionals[1]);
|
|
|
|
try std.testing.expectEqualStrings("foobar", args.options.output.?);
|
|
|
|
try std.testing.expectEqual(@as(?i32, -250), args.options.numberOfBytes);
|
|
try std.testing.expectEqual(@as(?u64, 0xFF00FF), args.options.unsigned_number);
|
|
try std.testing.expectEqual(TestEnum.special, args.options.mode);
|
|
|
|
try std.testing.expectEqual(@as(?i64, null), args.options.signed_number);
|
|
|
|
try std.testing.expectEqual(true, args.options.@"with-offset");
|
|
try std.testing.expectEqual(false, args.options.@"with-hexdump");
|
|
try std.testing.expectEqual(false, args.options.@"intermix-source");
|
|
}
|
|
|
|
test "basic parsing (with verbs)" {
|
|
var titerator = TestIterator.init(&[_][:0]const u8{
|
|
"--output", // non-verb options can come before or after verb
|
|
"foobar",
|
|
"booze", // verb
|
|
"--with-offset",
|
|
"--numberOfBytes",
|
|
"-250",
|
|
"--unsigned_number",
|
|
"0xFF00FF",
|
|
"positional 1",
|
|
"--mode",
|
|
"special",
|
|
"positional 2",
|
|
"--cocktail",
|
|
});
|
|
var args = try parseInternal(TestGenericOptions, TestVerb, &titerator, std.testing.allocator, .print);
|
|
defer args.deinit();
|
|
|
|
try std.testing.expectEqual(@as(?[:0]const u8, null), args.executable_name);
|
|
try std.testing.expect(?TestVerb == @TypeOf(args.verb));
|
|
try std.testing.expectEqual(@as(usize, 2), args.positionals.len);
|
|
try std.testing.expectEqualStrings("positional 1", args.positionals[0]);
|
|
try std.testing.expectEqualStrings("positional 2", args.positionals[1]);
|
|
|
|
try std.testing.expectEqualStrings("foobar", args.options.output.?);
|
|
|
|
try std.testing.expectEqual(@as(?i32, -250), args.options.numberOfBytes);
|
|
try std.testing.expectEqual(@as(?u64, 0xFF00FF), args.options.unsigned_number);
|
|
try std.testing.expectEqual(TestEnum.special, args.options.mode);
|
|
|
|
try std.testing.expectEqual(@as(?i64, null), args.options.signed_number);
|
|
|
|
try std.testing.expectEqual(true, args.options.@"with-offset");
|
|
try std.testing.expectEqual(false, args.options.@"with-hexdump");
|
|
try std.testing.expectEqual(false, args.options.@"intermix-source");
|
|
|
|
try std.testing.expect(args.verb.? == .booze);
|
|
|
|
const booze = args.verb.?.booze;
|
|
|
|
try std.testing.expectEqual(true, booze.cocktail);
|
|
try std.testing.expectEqual(false, booze.longdrink);
|
|
}
|
|
|
|
test "shorthand parsing (with verbs)" {
|
|
var titerator = TestIterator.init(&[_][:0]const u8{
|
|
"booze", // verb
|
|
"-o",
|
|
"foobar",
|
|
"-O",
|
|
"--numberOfBytes",
|
|
"-250",
|
|
"--unsigned_number",
|
|
"0xFF00FF",
|
|
"positional 1",
|
|
"--mode",
|
|
"special",
|
|
"positional 2",
|
|
"-c", // --cocktail
|
|
});
|
|
var args = try parseInternal(TestGenericOptions, TestVerb, &titerator, std.testing.allocator, .print);
|
|
defer args.deinit();
|
|
|
|
try std.testing.expectEqual(@as(?[:0]const u8, null), args.executable_name);
|
|
try std.testing.expect(?TestVerb == @TypeOf(args.verb));
|
|
try std.testing.expectEqual(@as(usize, 2), args.positionals.len);
|
|
try std.testing.expectEqualStrings("positional 1", args.positionals[0]);
|
|
try std.testing.expectEqualStrings("positional 2", args.positionals[1]);
|
|
|
|
try std.testing.expectEqualStrings("foobar", args.options.output.?);
|
|
|
|
try std.testing.expectEqual(@as(?i32, -250), args.options.numberOfBytes);
|
|
try std.testing.expectEqual(@as(?u64, 0xFF00FF), args.options.unsigned_number);
|
|
try std.testing.expectEqual(TestEnum.special, args.options.mode);
|
|
|
|
try std.testing.expectEqual(@as(?i64, null), args.options.signed_number);
|
|
|
|
try std.testing.expectEqual(true, args.options.@"with-offset");
|
|
try std.testing.expectEqual(false, args.options.@"with-hexdump");
|
|
try std.testing.expectEqual(false, args.options.@"intermix-source");
|
|
|
|
try std.testing.expect(args.verb.? == .booze);
|
|
|
|
const booze = args.verb.?.booze;
|
|
|
|
try std.testing.expectEqual(true, booze.cocktail);
|
|
try std.testing.expectEqual(false, booze.longdrink);
|
|
}
|
|
|
|
test "strings with sentinel" {
|
|
var titerator = TestIterator.init(&[_][:0]const u8{
|
|
"--output",
|
|
"foobar",
|
|
});
|
|
var args = try parseInternal(
|
|
struct {
|
|
output: ?[:0]const u8 = null,
|
|
},
|
|
null,
|
|
&titerator,
|
|
std.testing.allocator,
|
|
.print,
|
|
);
|
|
defer args.deinit();
|
|
|
|
try std.testing.expectEqual(@as(?[:0]const u8, null), args.executable_name);
|
|
try std.testing.expect(void == @TypeOf(args.verb));
|
|
try std.testing.expectEqual(@as(usize, 0), args.positionals.len);
|
|
|
|
try std.testing.expectEqualStrings("foobar", args.options.output.?);
|
|
}
|
|
|
|
test "option argument --" {
|
|
var titerator = TestIterator.init(&[_][:0]const u8{
|
|
"--output",
|
|
"--",
|
|
});
|
|
|
|
try std.testing.expectError(error.MissingArgument, parseInternal(
|
|
struct {
|
|
output: ?[:0]const u8 = null,
|
|
},
|
|
null,
|
|
&titerator,
|
|
std.testing.allocator,
|
|
.silent,
|
|
));
|
|
}
|
|
|
|
test "index of raw indicator --" {
|
|
var titerator = TestIterator.init(&[_][:0]const u8{ "stdin", "-", "--", "not-stdin", "-", "--" });
|
|
|
|
var args = try parseInternal(
|
|
struct {},
|
|
null,
|
|
&titerator,
|
|
std.testing.allocator,
|
|
.print,
|
|
);
|
|
defer args.deinit();
|
|
|
|
try std.testing.expectEqual(args.raw_start_index, 2);
|
|
try std.testing.expectEqual(args.positionals.len, 5);
|
|
}
|
|
|
|
fn reserved_argument(arg: []const u8) bool {
|
|
return std.mem.eql(u8, arg, "shorthands") or std.mem.eql(u8, arg, "meta");
|
|
}
|
|
|
|
pub fn printHelp(comptime Generic: type, name: []const u8, writer: anytype) !void {
|
|
if (!@hasDecl(Generic, "meta")) {
|
|
@compileError("Missing meta declaration in Generic");
|
|
}
|
|
|
|
const Meta = @TypeOf(Generic.meta);
|
|
|
|
try writer.print("Usage: {s}", .{name});
|
|
|
|
if (@hasField(Meta, "usage_summary")) {
|
|
try writer.print(" {s}", .{Generic.meta.usage_summary});
|
|
}
|
|
try writer.print("\n\n", .{});
|
|
|
|
if (@hasField(Meta, "full_text")) {
|
|
try writer.print("{s}\n\n", .{Generic.meta.full_text});
|
|
}
|
|
|
|
if (@hasField(Meta, "option_docs")) {
|
|
const fields = std.meta.fields(Generic);
|
|
|
|
try writer.print("Options:\n", .{});
|
|
comptime var maxOptionLength = 0;
|
|
inline for (fields) |field| {
|
|
if (!reserved_argument(field.name)) {
|
|
if (!@hasField(@TypeOf(Generic.meta.option_docs), field.name)) {
|
|
@compileError("option_docs not specified for field: " ++ field.name);
|
|
}
|
|
}
|
|
|
|
if (field.name.len > maxOptionLength) {
|
|
maxOptionLength = field.name.len;
|
|
}
|
|
}
|
|
|
|
inline for (fields) |field| {
|
|
if (!reserved_argument(field.name)) {
|
|
if (@hasDecl(Generic, "shorthands")) {
|
|
var foundShorthand = false;
|
|
inline for (std.meta.fields(@TypeOf(Generic.shorthands))) |shorthand| {
|
|
const option = @field(Generic.shorthands, shorthand.name);
|
|
if (std.mem.eql(u8, option, field.name)) {
|
|
try writer.print(" -{s}, ", .{shorthand.name});
|
|
foundShorthand = true;
|
|
}
|
|
}
|
|
if (!foundShorthand)
|
|
try writer.print(" ", .{});
|
|
}
|
|
if (@hasDecl(Generic, "wrap_len")) {
|
|
var it = std.mem.splitScalar(u8, @field(Generic.meta.option_docs, field.name), ' ');
|
|
const threshold = Generic.wrap_len;
|
|
var line_len: usize = 0;
|
|
var newline = false;
|
|
var first = true;
|
|
while (it.next()) |word| {
|
|
if (first) {
|
|
const fmtString = std.fmt.comptimePrint("--{{s: <{}}} {{s}}", .{maxOptionLength});
|
|
try writer.print(fmtString, .{ field.name, word });
|
|
first = false;
|
|
} else if (newline) {
|
|
const fmtString = std.fmt.comptimePrint("\n{{s: <{}}} {{s}}", .{maxOptionLength + 10});
|
|
try writer.print(fmtString, .{ " ", word });
|
|
newline = false;
|
|
} else {
|
|
try writer.print(" {s}", .{word});
|
|
}
|
|
line_len += word.len;
|
|
if (line_len >= threshold) {
|
|
newline = true;
|
|
line_len = 0;
|
|
}
|
|
}
|
|
try writer.writeByte('\n');
|
|
} else {
|
|
const fmtString = std.fmt.comptimePrint("--{{s: <{}}} {{s}}\n", .{maxOptionLength});
|
|
try writer.print(fmtString, .{ field.name, @field(Generic.meta.option_docs, field.name) });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
test "full help" {
|
|
const Options = struct {
|
|
boolflag: bool = false,
|
|
stringflag: []const u8 = "hello",
|
|
|
|
pub const shorthands = .{
|
|
.b = "boolflag",
|
|
};
|
|
|
|
pub const meta = .{
|
|
.name = "test",
|
|
.full_text = "testing tool",
|
|
.usage_summary = "[--boolflag] [--stringflag]",
|
|
.option_docs = .{
|
|
.boolflag = "a boolean flag",
|
|
.stringflag = "a string flag",
|
|
},
|
|
};
|
|
};
|
|
|
|
var test_buffer = std.ArrayList(u8).init(std.testing.allocator);
|
|
defer test_buffer.deinit();
|
|
|
|
try printHelp(Options, "test", test_buffer.writer());
|
|
|
|
const expected =
|
|
\\Usage: test [--boolflag] [--stringflag]
|
|
\\
|
|
\\testing tool
|
|
\\
|
|
\\Options:
|
|
\\ -b, --boolflag a boolean flag
|
|
\\ --stringflag a string flag
|
|
\\
|
|
;
|
|
|
|
try std.testing.expectEqualStrings(expected, test_buffer.items);
|
|
}
|
|
|
|
test "help with no usage summary" {
|
|
const Options = struct {
|
|
boolflag: bool = false,
|
|
stringflag: []const u8 = "hello",
|
|
|
|
pub const shorthands = .{
|
|
.b = "boolflag",
|
|
};
|
|
|
|
pub const meta = .{
|
|
.full_text = "testing tool",
|
|
.option_docs = .{
|
|
.boolflag = "a boolean flag",
|
|
.stringflag = "a string flag",
|
|
},
|
|
};
|
|
};
|
|
|
|
var test_buffer = std.ArrayList(u8).init(std.testing.allocator);
|
|
defer test_buffer.deinit();
|
|
|
|
try printHelp(Options, "test", test_buffer.writer());
|
|
|
|
const expected =
|
|
\\Usage: test
|
|
\\
|
|
\\testing tool
|
|
\\
|
|
\\Options:
|
|
\\ -b, --boolflag a boolean flag
|
|
\\ --stringflag a string flag
|
|
\\
|
|
;
|
|
|
|
try std.testing.expectEqualStrings(expected, test_buffer.items);
|
|
}
|
|
|
|
test "help with wrapping" {
|
|
const Options = struct {
|
|
boolflag: bool = false,
|
|
stringflag: []const u8 = "hello",
|
|
|
|
pub const shorthands = .{
|
|
.b = "boolflag",
|
|
};
|
|
|
|
pub const wrap_len = 10;
|
|
|
|
pub const meta = .{
|
|
.full_text = "testing tool",
|
|
.option_docs = .{
|
|
.boolflag = "a boolean flag with a pretty long description about booleans",
|
|
.stringflag = "a string flag with another long description about strings",
|
|
},
|
|
};
|
|
};
|
|
|
|
var test_buffer = std.ArrayList(u8).init(std.testing.allocator);
|
|
defer test_buffer.deinit();
|
|
|
|
try printHelp(Options, "test", test_buffer.writer());
|
|
|
|
const expected =
|
|
\\Usage: test
|
|
\\
|
|
\\testing tool
|
|
\\
|
|
\\Options:
|
|
\\ -b, --boolflag a boolean flag
|
|
\\ with a pretty
|
|
\\ long description
|
|
\\ about booleans
|
|
\\ --stringflag a string flag
|
|
\\ with another
|
|
\\ long description
|
|
\\ about strings
|
|
\\
|
|
;
|
|
|
|
try std.testing.expectEqualStrings(expected, test_buffer.items);
|
|
}
|