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); }