diff --git a/README.md b/README.md index 9cf73a5..74bf45c 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ pub fn main() !void { .{}, ); - try std.io.getStdOut().writer().print("Hello, {s}!\n", .{cli.username}); + try std.debug.print("Hello, {s}!\n", .{cli.username}); } const std = @import("std"); diff --git a/build.zig b/build.zig index 8229d5a..530bb1d 100644 --- a/build.zig +++ b/build.zig @@ -45,6 +45,6 @@ pub fn build(b: *std.Build) void { }); example.root_module.addImport("flags", mod); const run_example = b.addRunArtifact(example); - if (b.args) |args| run_example.addArgs(args); + run_example.addPassthruArgs(); // Zig 0.17+ example_step.dependOn(&run_example.step); } diff --git a/build.zig.zon b/build.zig.zon index 3138506..e302886 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -2,7 +2,7 @@ .name = .flags, .version = "0.10.0", .fingerprint = 0xb0541bade61ff6b, - .minimum_zig_version = "0.15.1", + .minimum_zig_version = "0.16.0", // Want to limit these to only the things that really constitute "the library". // The hash needn't be updated by some small change to the README. .paths = .{ diff --git a/examples/colors.zig b/examples/colors.zig index 00239a3..a204345 100644 --- a/examples/colors.zig +++ b/examples/colors.zig @@ -1,14 +1,14 @@ const std = @import("std"); const flags = @import("flags"); -pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}).init; - defer _ = gpa.deinit(); +pub fn main(init: std.process.Init) !void { + const io = init.io; + const gpa = init.gpa; - const args = try std.process.argsAlloc(gpa.allocator()); - defer std.process.argsFree(gpa.allocator(), args); + const args = try init.minimal.args.toSlice(gpa); + defer gpa.free(args); - _ = flags.parse(args, "colors", Flags, .{ + _ = flags.parse(io, args, "colors", Flags, .{ // Use the `colors` option to provide a colorscheme for the error/help messages. // Specifying this as empty: `.colors = &.{}` will disable colors. // Each field is a list of type `std.io.tty.Color`. diff --git a/examples/overview.zig b/examples/overview.zig index 7966e1e..56cdfa9 100644 --- a/examples/overview.zig +++ b/examples/overview.zig @@ -1,17 +1,16 @@ const std = @import("std"); const flags = @import("flags"); -pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}).init; - defer _ = gpa.deinit(); +pub fn main(init: std.process.Init) !void { + const io = init.io; + const gpa = init.gpa; + const args = try init.minimal.args.toSlice(gpa); + defer gpa.free(args); - const args = try std.process.argsAlloc(gpa.allocator()); - defer std.process.argsFree(gpa.allocator(), args); - - const options = flags.parse(args, "overview", Flags, .{}); + const options = flags.parse(io, args, "overview", Flags, .{}); var buffer: [1024]u8 = undefined; - var file_writer = std.fs.File.stdout().writer(&buffer); + var file_writer = std.Io.File.stdout().writer(io, &buffer); try std.json.Stringify.value( options, .{ .whitespace = .indent_2 }, diff --git a/examples/trailing.zig b/examples/trailing.zig index 1262afb..dca57d5 100644 --- a/examples/trailing.zig +++ b/examples/trailing.zig @@ -1,16 +1,16 @@ const std = @import("std"); const flags = @import("flags"); -pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}).init; - defer _ = gpa.deinit(); +pub fn main(init: std.process.Init) !void { + const io = init.io; + const gpa = init.gpa; - const args = try std.process.argsAlloc(gpa.allocator()); - defer std.process.argsFree(gpa.allocator(), args); + const args = try init.minimal.args.toSlice(gpa); + defer gpa.free(args); - const options = flags.parse(args, "trailing", Flags, .{}); + const options = flags.parse(io, args, "trailing", Flags, .{}); - var stdout_writer = std.fs.File.stdout().writer(&.{}); + var stdout_writer = std.Io.File.stdout().writer(io, &.{}); try std.json.Stringify.value( options, .{ .whitespace = .indent_2 }, diff --git a/src/ColorScheme.zig b/src/ColorScheme.zig index 94783f4..59755ce 100644 --- a/src/ColorScheme.zig +++ b/src/ColorScheme.zig @@ -2,7 +2,7 @@ const ColorScheme = @This(); const std = @import("std"); -const Color = std.Io.tty.Color; +const Color = std.Io.Terminal.Color; pub const Style = []const Color; diff --git a/src/Help.zig b/src/Help.zig index 5ad904f..819e7e6 100644 --- a/src/Help.zig +++ b/src/Help.zig @@ -3,7 +3,7 @@ const Help = @This(); const std = @import("std"); const meta = @import("meta.zig"); -const File = std.fs.File; +const File = std.Io.File; const ColorScheme = @import("ColorScheme.zig"); const Terminal = @import("Terminal.zig"); @@ -17,9 +17,9 @@ pub const Usage = struct { command: []const u8, body: []const u8, - pub fn render(usage: Usage, stdout: File, colors: *const ColorScheme) void { - var stdout_writer = stdout.writer(&.{}); - const term = Terminal.init(stdout, &stdout_writer.interface); + pub fn render(io: std.Io, usage: Usage, stdout: File, colors: *const ColorScheme) void { + var stdout_writer = stdout.writerStreaming(io, &.{}); + const term = Terminal.init(io, stdout, &stdout_writer.interface) catch unreachable; usage.renderToTerminal(term, colors); } @@ -75,7 +75,11 @@ pub const Usage = struct { fn add(usage: *Usage, item: []const u8, line_len: *usize) void { if (line_len.* + " ".len + item.len > max_line_len) { const indent_len = "Usage: ".len + usage.command.len; - usage.body = usage.body ++ "\n" ++ " " ** indent_len; + usage.body = usage.body ++ "\n" ++ comptime blk: { + var indent: [indent_len]u8 = undefined; + @memset(&indent, ' '); + break :blk &indent; + }; line_len.* = indent_len; } @@ -100,9 +104,9 @@ const Section = struct { } }; -pub fn render(help: *const Help, stdout: File, colors: *const ColorScheme) void { - var stdout_writer = stdout.writer(&.{}); - const term = Terminal.init(stdout, &stdout_writer.interface); +pub fn render(help: *const Help, io: std.Io, stdout: File, colors: *const ColorScheme) void { + var stdout_writer = stdout.writerStreaming(io, &.{}); + const term = Terminal.init(io, stdout, &stdout_writer.interface) catch unreachable; help.usage.renderToTerminal(term, colors); if (help.description) |description| { @@ -166,10 +170,11 @@ pub fn generate(Flags: type, info: meta.FlagsInfo, command: []const u8) Help { const T = meta.unwrapOptional(flag.type); if (@typeInfo(T) == .@"enum") { const variant_descriptions = meta.getDescriptions(T); - for (@typeInfo(T).@"enum".fields) |variant| { + const enum_info = @typeInfo(T).@"enum"; + for (enum_info.field_names) |variant_name| { options.add(.{ - .name = " " ++ meta.toKebab(variant.name), - .desc = @field(variant_descriptions, variant.name), + .name = " " ++ meta.toKebab(variant_name), + .desc = @field(variant_descriptions, variant_name), }); } } @@ -194,10 +199,11 @@ pub fn generate(Flags: type, info: meta.FlagsInfo, command: []const u8) Help { const T = meta.unwrapOptional(arg.type); if (@typeInfo(T) == .@"enum") { const variant_descriptions = meta.getDescriptions(T); - for (@typeInfo(T).@"enum".fields) |variant| { + const enum_info = @typeInfo(T).@"enum"; + for (enum_info.field_names) |variant_name| { arguments.add(.{ - .name = " " ++ meta.toKebab(variant.name), - .desc = @field(variant_descriptions, variant.name), + .name = " " ++ meta.toKebab(variant_name), + .desc = @field(variant_descriptions, variant_name), }); } } diff --git a/src/Parser.zig b/src/Parser.zig index e405d4b..c5ef2e1 100644 --- a/src/Parser.zig +++ b/src/Parser.zig @@ -14,16 +14,16 @@ args: []const [:0]const u8, current_arg: usize, colors: *const ColorScheme, -fn fatal(parser: *const Parser, comptime fmt: []const u8, args: anytype) noreturn { - const stderr_file = std.fs.File.stderr(); - var stderr_file_writer = stderr_file.writer(&.{}); - const stderr = Terminal.init(stderr_file, &stderr_file_writer.interface); +fn fatal(parser: *const Parser, io: std.Io, comptime fmt: []const u8, args: anytype) noreturn { + var stderr_file = std.Io.File.stderr(); + var stderr_writer = stderr_file.writerStreaming(io, &.{}); + const stderr = Terminal.init(io, stderr_file, &stderr_writer.interface) catch unreachable; stderr.print(parser.colors.error_label, "Error: ", .{}); stderr.print(parser.colors.error_message, fmt ++ "\n", args); std.process.exit(1); } -pub fn parse(parser: *Parser, Flags: type, comptime command_name: []const u8) Flags { +pub fn parse(parser: *Parser, io: std.Io, Flags: type, comptime command_name: []const u8) Flags { const info = comptime meta.info(Flags); const help = comptime Help.generate(Flags, info, command_name); @@ -39,18 +39,18 @@ pub fn parse(parser: *Parser, Flags: type, comptime command_name: []const u8) Fl next_arg: while (parser.nextArg()) |arg| { if (arg.len == 0) { - parser.fatal("empty argument", .{}); + parser.fatal(io, "empty argument", .{}); } if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) { - help.render(std.fs.File.stdout(), parser.colors); + help.render(io, std.Io.File.stdout(), parser.colors); std.process.exit(0); } if (std.mem.eql(u8, arg, "--")) { // Blindly treat remaining arguments as positional. while (parser.nextArg()) |positional| { - if (parser.parsePositional(positional, positional_index, info.positionals, &flags) == .consumed_all) { + if (parser.parsePositional(io, positional, positional_index, info.positionals, &flags) == .consumed_all) { break :next_arg; } positional_index += 1; @@ -59,17 +59,17 @@ pub fn parse(parser: *Parser, Flags: type, comptime command_name: []const u8) Fl if (std.mem.startsWith(u8, arg, "--")) { inline for (info.flags) |flag| if (std.mem.eql(u8, arg, flag.flag_name)) { - @field(flags, flag.field_name) = parser.parseOption(flag.type, flag.flag_name); + @field(flags, flag.field_name) = parser.parseOption(io, flag.type, flag.flag_name); @field(passed, flag.field_name) = true; continue :next_arg; }; - parser.fatal("unrecognized flag: {s}", .{arg}); + parser.fatal(io, "unrecognized flag: {s}", .{arg}); } if (std.mem.startsWith(u8, arg, "-")) { if (arg.len == 1) { - parser.fatal("unrecognized argument: '-'", .{}); + parser.fatal(io, "unrecognized argument: '-'", .{}); } const switch_set = arg[1..]; @@ -79,9 +79,10 @@ pub fn parse(parser: *Parser, Flags: type, comptime command_name: []const u8) Fl // Removing this check would allow formats like: // `$ -abc value-for-a value-for-b value-for-c` if (flag.type != bool and i < switch_set.len - 1) { - parser.fatal("missing value after switch: {c}", .{switch_char}); + parser.fatal(io, "missing value after switch: {c}", .{switch_char}); } @field(flags, flag.field_name) = parser.parseOption( + io, flag.type, &.{ '-', switch_char }, ); @@ -89,21 +90,21 @@ pub fn parse(parser: *Parser, Flags: type, comptime command_name: []const u8) Fl continue :next_switch; } }; - parser.fatal("unrecognized switch: {c}", .{ch}); + parser.fatal(io, "unrecognized switch: {c}", .{ch}); } continue :next_arg; } inline for (info.subcommands) |cmd| { if (std.mem.eql(u8, arg, cmd.command_name)) { - const cmd_flags = parser.parse(cmd.type, command_name ++ " " ++ cmd.command_name); + const cmd_flags = parser.parse(io, cmd.type, command_name ++ " " ++ cmd.command_name); flags.command = @unionInit(@TypeOf(flags.command), cmd.field_name, cmd_flags); passed.command = true; continue :next_arg; } } - if (parser.parsePositional(arg, positional_index, info.positionals, &flags) == .consumed_all) { + if (parser.parsePositional(io, arg, positional_index, info.positionals, &flags) == .consumed_all) { break :next_arg; } positional_index += 1; @@ -115,7 +116,7 @@ pub fn parse(parser: *Parser, Flags: type, comptime command_name: []const u8) Fl .bool => false, .optional => null, else => { - parser.fatal("missing required flag: {s}", .{flag.flag_name}); + parser.fatal(io, "missing required flag: {s}", .{flag.flag_name}); }, }; }; @@ -126,14 +127,14 @@ pub fn parse(parser: *Parser, Flags: type, comptime command_name: []const u8) Fl switch (@typeInfo(pos.type)) { .optional => null, else => { - parser.fatal("missing required argument: {s}", .{pos.arg_name}); + parser.fatal(io, "missing required argument: {s}", .{pos.arg_name}); }, }; } } if (info.subcommands.len > 0 and !passed.command) { - parser.fatal("missing subcommand", .{}); + parser.fatal(io, "missing subcommand", .{}); } return flags; @@ -141,6 +142,7 @@ pub fn parse(parser: *Parser, Flags: type, comptime command_name: []const u8) Fl fn parsePositional( parser: *Parser, + io: std.Io, arg: [:0]const u8, index: usize, comptime positionals: []const meta.Positional, @@ -152,14 +154,14 @@ fn parsePositional( parser.current_arg = parser.args.len; return .consumed_all; } - parser.fatal("unexpected argument: {s}", .{arg}); + parser.fatal(io, "unexpected argument: {s}", .{arg}); } switch (index) { inline 0...positionals.len - 1 => |i| { const positional = positionals[i]; const T = meta.unwrapOptional(positional.type); - @field(flags.positional, positional.field_name) = parser.parseValue(T, arg); + @field(flags.positional, positional.field_name) = parser.parseValue(io, T, arg); return .consumed_one; }, @@ -167,27 +169,29 @@ fn parsePositional( } } -fn parseOption(parser: *Parser, T: type, option_name: []const u8) T { +fn parseOption(parser: *Parser, io: std.Io, T: type, option_name: []const u8) T { if (T == bool) return true; const value = parser.nextArg() orelse { - parser.fatal("missing value for '{s}'", .{option_name}); + parser.fatal(io, "missing value for '{s}'", .{option_name}); }; - return parser.parseValue(meta.unwrapOptional(T), value); + return parser.parseValue(io, meta.unwrapOptional(T), value); } -fn parseValue(parser: *const Parser, T: type, arg: [:0]const u8) T { +fn parseValue(parser: *const Parser, io: std.Io, T: type, arg: [:0]const u8) T { if (T == []const u8 or T == [:0]const u8) return arg; switch (@typeInfo(T)) { .int => |info| return std.fmt.parseInt(T, arg, 10) catch |err| { switch (err) { error.Overflow => parser.fatal( + io, "value out of bounds for {d}-bit {s} integer: {s}", .{ info.bits, @tagName(info.signedness), arg }, ), error.InvalidCharacter => parser.fatal( + io, "expected integer number, found '{s}'", .{arg}, ), @@ -196,18 +200,18 @@ fn parseValue(parser: *const Parser, T: type, arg: [:0]const u8) T { .float => return std.fmt.parseFloat(T, arg) catch |err| switch (err) { error.InvalidCharacter => { - parser.fatal("expected numerical value, found '{s}'", .{arg}); + parser.fatal(io, "expected numerical value, found '{s}'", .{arg}); }, }, .@"enum" => |info| { - inline for (info.fields) |field| { - if (std.mem.eql(u8, arg, meta.toKebab(field.name))) { - return @enumFromInt(field.value); + inline for (info.field_names, info.field_values) |field_name, field_value| { + if (std.mem.eql(u8, arg, meta.toKebab(field_name))) { + return @fromBackingInt(@intCast(field_value)); } } - parser.fatal("unrecognized option: '{s}'", .{arg}); + parser.fatal(io, "unrecognized option: '{s}'", .{arg}); }, else => comptime meta.compileError("invalid flag type: {s}", .{@typeName(T)}), diff --git a/src/Terminal.zig b/src/Terminal.zig index 79dacd5..5cebdee 100644 --- a/src/Terminal.zig +++ b/src/Terminal.zig @@ -1,18 +1,20 @@ +//! wrapper around std.Io.Terminal that gives us a convenient +//! init and print method. + const Terminal = @This(); const std = @import("std"); const ColorScheme = @import("ColorScheme.zig"); -const tty = std.Io.tty; -const File = std.fs.File; +const tty = std.Io.Terminal; +const File = std.Io.File; -writer: *std.Io.Writer, -config: tty.Config, +instance: tty, -pub fn init(file: File, writer: *std.Io.Writer) Terminal { +pub fn init(io: std.Io, file: File, writer: *std.Io.Writer) !Terminal { + const terminal_mode: std.Io.Terminal.Mode = try .detect(io, file, false, false); return .{ - .writer = writer, - .config = tty.detectConfig(file), + .instance = .{ .writer = writer, .mode = terminal_mode }, }; } @@ -23,12 +25,12 @@ pub fn print( args: anytype, ) void { for (style) |color| { - terminal.config.setColor(terminal.writer, color) catch {}; + terminal.instance.setColor(color) catch {}; } - terminal.writer.print(format, args) catch {}; + terminal.instance.writer.print(format, args) catch {}; if (style.len > 0) { - terminal.config.setColor(terminal.writer, .reset) catch {}; + terminal.instance.setColor(.reset) catch {}; } } diff --git a/src/flags.zig b/src/flags.zig index eeb74c9..64079fd 100644 --- a/src/flags.zig +++ b/src/flags.zig @@ -12,6 +12,7 @@ pub const Options = struct { }; pub fn parse( + io: std.Io, args: []const [:0]const u8, /// The name of your program. comptime exe_name: []const u8, @@ -24,5 +25,5 @@ pub fn parse( .colors = options.colors, }; - return parser.parse(Flags, exe_name); + return parser.parse(io, Flags, exe_name); } diff --git a/src/meta.zig b/src/meta.zig index 7f9eda7..0bbb426 100644 --- a/src/meta.zig +++ b/src/meta.zig @@ -51,53 +51,63 @@ pub fn info(comptime Flags: type) FlagsInfo { const switches = getSwitches(Flags); - for (@typeInfo(Flags).@"struct".fields) |field| { - if (std.mem.eql(u8, field.name, "positional")) { - if (@typeInfo(field.type) != .@"struct") compileError( + const FlagsTypeInfo = @typeInfo(Flags).@"struct"; + for (0..FlagsTypeInfo.field_names.len) |field_index| { + const field_name = FlagsTypeInfo.field_names[field_index]; + const field_type = FlagsTypeInfo.field_types[field_index]; + const field_attrs = FlagsTypeInfo.field_attrs[field_index]; + + if (std.mem.eql(u8, field_name, "positional")) { + if (@typeInfo(field_type) != .@"struct") compileError( "'positional' field is not a struct type: {s}", - .{@typeName(field.type)}, + .{@typeName(field_type)}, ); + const PosTypeInfo = @typeInfo(field_type).@"struct"; var seen_optional = false; - for (@typeInfo(field.type).@"struct".fields) |positional| { - if (std.mem.eql(u8, positional.name, "trailing")) { + for (0..PosTypeInfo.field_names.len) |positional_index| { + const positional_name = PosTypeInfo.field_names[positional_index]; + const positional_type = PosTypeInfo.field_types[positional_index]; + const positional_attrs = PosTypeInfo.field_attrs[positional_index]; + if (std.mem.eql(u8, positional_name, "trailing")) { continue; } - if (@typeInfo(positional.type) != .optional) { + if (@typeInfo(positional_type) != .optional) { if (seen_optional) compileError( "non-optional positional field after optional: {s}", - .{positional.name}, + .{positional_name}, ); } else { seen_optional = true; } command.positionals = command.positionals ++ .{Positional{ - .type = positional.type, - .default_value = positional.default_value_ptr, - .field_name = positional.name, - .arg_name = positionalName(positional), + .type = positional_type, + .default_value = positional_attrs.default_value_ptr, + .field_name = positional_name, + .arg_name = positionalName(positional_name), }}; } - } else if (std.mem.eql(u8, field.name, "command")) { - if (@typeInfo(field.type) != .@"union") compileError( + } else if (std.mem.eql(u8, field_name, "command")) { + if (@typeInfo(field_type) != .@"union") compileError( "command field type is not a union: {s}", - .{@typeName(field.type)}, + .{@typeName(field_type)}, ); - for (@typeInfo(field.type).@"union".fields) |cmd| { + const UnionTypeInfo = @typeInfo(field_type).@"union"; + for (0..UnionTypeInfo.field_names.len) |cmd_index| { command.subcommands = command.subcommands ++ .{SubCommand{ - .type = cmd.type, - .field_name = cmd.name, - .command_name = toKebab(cmd.name), + .type = UnionTypeInfo.field_types[cmd_index], + .field_name = UnionTypeInfo.field_names[cmd_index], + .command_name = toKebab(UnionTypeInfo.field_names[cmd_index]), }}; } } else { command.flags = command.flags ++ .{Flag{ - .type = field.type, - .default_value = field.default_value_ptr, - .field_name = field.name, - .flag_name = "--" ++ toKebab(field.name), - .switch_char = @field(switches, field.name), + .type = field_type, + .default_value = field_attrs.default_value_ptr, + .field_name = field_name, + .flag_name = "--" ++ toKebab(field_name), + .switch_char = @field(switches, field_name), }}; } } @@ -127,13 +137,14 @@ fn getSwitches(T: type) FieldAttr(T, u8) { compileError("switches is not a struct value: {s}", .{@typeName(Switches)}); } - const switch_fields = @typeInfo(Switches).@"struct".fields; - for (switch_fields, 0..) |switch_field, field_index| { - if (!@hasField(T, switch_field.name)) { - compileError("switch name does not match any field: {s}", .{switch_field.name}); + const SwitchTypeInfo = @typeInfo(Switches).@"struct"; + for (0..SwitchTypeInfo.field_names.len) |field_index| { + const switch_field = SwitchTypeInfo.field_names[field_index]; + if (!@hasField(T, switch_field)) { + compileError("switch name does not match any field: {s}", .{switch_field}); } - const switch_val = @field(T.switches, switch_field.name); + const switch_val = @field(T.switches, switch_field); if (@TypeOf(switch_val) != comptime_int) { compileError("switch value is not a character: {any}", .{switch_val}); } @@ -144,15 +155,15 @@ fn getSwitches(T: type) FieldAttr(T, u8) { compileError("switch character is not a letter or digit: {c}", .{switch_char}); } - for (switch_fields[field_index + 1 ..]) |other_field| { - const other_val = @field(T.switches, other_field.name); + for (SwitchTypeInfo.field_names[field_index + 1 ..]) |other_field| { + const other_val = @field(T.switches, other_field); if (switch_val == other_val) compileError( "duplicate switch values: {s} and {s}", - .{ switch_field.name, other_field.name }, + .{ switch_field, other_field }, ); } - @field(switches, switch_field.name) = switch_char; + @field(switches, switch_field) = switch_char; } return switches; @@ -170,13 +181,15 @@ pub fn getDescriptions(T: type) FieldAttr(T, []const u8) { compileError("descriptions is not a struct value: {s}", .{@typeName(D)}); } - for (@typeInfo(D).@"struct".fields) |field| { - if (!@hasField(T, field.name)) { - compileError("description name does not match any field: '{s}'", .{field.name}); + const DTypeInfo = @typeInfo(D).@"struct"; + for (0..DTypeInfo.field_names.len) |field_index| { + const field_name = DTypeInfo.field_names[field_index]; + if (!@hasField(T, field_name)) { + compileError("description name does not match any field: '{s}'", .{field_name}); } - const description = @field(T.descriptions, field.name); - @field(descriptions, field.name) = + const description = @field(T.descriptions, field_name); + @field(descriptions, field_name) = @as([]const u8, description); // description must be a string } @@ -194,13 +207,15 @@ pub fn getFormats(T: type) FieldAttr(T, []const u8) { compileError("formats is not a struct value: {s}", .{@typeName(F)}); } - for (@typeInfo(F).@"struct".fields) |field| { - if (!@hasField(T, field.name)) { - compileError("format name does not match any field: {s}", .{field.name}); + const FTypeInfo = @typeInfo(F).@"struct"; + for (0..FTypeInfo.field_names.len) |field_index| { + const field_name = FTypeInfo.field_names[field_index]; + if (!@hasField(T, field_name)) { + compileError("format name does not match any field: {s}", .{field_name}); } - const format = @field(T.formats, field.name); - @field(formats, field.name) = + const format = @field(T.formats, field_name); + @field(formats, field_name) = @as([]const u8, format); // format must be a string } @@ -226,9 +241,9 @@ pub fn defaultValue(comptime option: anytype) ?option.type { } /// Converts "positional_field" to ".". -pub fn positionalName(comptime field: std.builtin.Type.StructField) []const u8 { +pub fn positionalName(comptime field_name: []const u8) []const u8 { comptime var upper: []const u8 = &.{}; - comptime for (field.name) |c| { + comptime for (field_name) |c| { upper = upper ++ .{std.ascii.toUpper(c)}; }; return std.fmt.comptimePrint("<{s}>", .{upper});