zig

fork of https://codeberg.org/ziglang/zig
Log | Files | Refs | README | LICENSE

cli.zig (97797B) - Raw


      1 const std = @import("std");
      2 const code_pages = @import("code_pages.zig");
      3 const SupportedCodePage = code_pages.SupportedCodePage;
      4 const lang = @import("lang.zig");
      5 const res = @import("res.zig");
      6 const Allocator = std.mem.Allocator;
      7 const lex = @import("lex.zig");
      8 const cvtres = @import("cvtres.zig");
      9 
     10 /// This is what /SL 100 will set the maximum string literal length to
     11 pub const max_string_literal_length_100_percent = 8192;
     12 
     13 pub const usage_string_after_command_name =
     14     \\ [options] [--] <INPUT> [<OUTPUT>]
     15     \\
     16     \\The sequence -- can be used to signify when to stop parsing options.
     17     \\This is necessary when the input path begins with a forward slash.
     18     \\
     19     \\Supported option prefixes are /, -, and --, so e.g. /h, -h, and --h all work.
     20     \\Drop-in compatible with the Microsoft Resource Compiler.
     21     \\
     22     \\Supported Win32 RC Options:
     23     \\  /?, /h                  Print this help and exit.
     24     \\  /v                      Verbose (print progress messages).
     25     \\  /d <name>[=<value>]     Define a symbol (during preprocessing).
     26     \\  /u <name>               Undefine a symbol (during preprocessing).
     27     \\  /fo <value>             Specify output file path.
     28     \\  /l <value>              Set default language using hexadecimal id (ex: 409).
     29     \\  /ln <value>             Set default language using language name (ex: en-us).
     30     \\  /i <value>              Add an include path.
     31     \\  /x                      Ignore INCLUDE environment variable.
     32     \\  /c <value>              Set default code page (ex: 65001).
     33     \\  /w                      Warn on invalid code page in .rc (instead of error).
     34     \\  /y                      Suppress warnings for duplicate control IDs.
     35     \\  /n                      Null-terminate all strings in string tables.
     36     \\  /sl <value>             Specify string literal length limit in percentage (1-100)
     37     \\                          where 100 corresponds to a limit of 8192. If the /sl
     38     \\                          option is not specified, the default limit is 4097.
     39     \\  /p                      Only run the preprocessor and output a .rcpp file.
     40     \\
     41     \\No-op Win32 RC Options:
     42     \\  /nologo, /a, /r         Options that are recognized but do nothing.
     43     \\
     44     \\Unsupported Win32 RC Options:
     45     \\  /fm, /q, /g, /gn, /g1, /g2     Unsupported MUI-related options.
     46     \\  /?c, /hc, /t, /tp:<prefix>,    Unsupported LCX/LCE-related options.
     47     \\     /tn, /tm, /tc, /tw, /te,
     48     \\                    /ti, /ta
     49     \\  /z                             Unsupported font-substitution-related option.
     50     \\  /s                             Unsupported HWB-related option.
     51     \\
     52     \\Custom Options (resinator-specific):
     53     \\  /:no-preprocess           Do not run the preprocessor.
     54     \\  /:debug                   Output the preprocessed .rc file and the parsed AST.
     55     \\  /:auto-includes <value>   Set the automatic include path detection behavior.
     56     \\    any                     (default) Use MSVC if available, fall back to MinGW
     57     \\    msvc                    Use MSVC include paths (must be present on the system)
     58     \\    gnu                     Use MinGW include paths
     59     \\    none                    Do not use any autodetected include paths
     60     \\  /:depfile <path>          Output a file containing a list of all the files that
     61     \\                            the .rc includes or otherwise depends on.
     62     \\  /:depfile-fmt <value>     Output format of the depfile, if /:depfile is set.
     63     \\    json                    (default) A top-level JSON array of paths
     64     \\  /:input-format <value>    If not specified, the input format is inferred.
     65     \\    rc                      (default if input format cannot be inferred)
     66     \\    res                     Compiled .rc file, implies /:output-format coff
     67     \\    rcpp                    Preprocessed .rc file, implies /:no-preprocess
     68     \\  /:output-format <value>   If not specified, the output format is inferred.
     69     \\    res                     (default if output format cannot be inferred)
     70     \\    coff                    COFF object file (extension: .obj or .o)
     71     \\    rcpp                    Preprocessed .rc file, implies /p
     72     \\  /:target <arch>           Set the target machine for COFF object files.
     73     \\                            Can be specified either as PE/COFF machine constant
     74     \\                            name (X64, ARM64, etc) or Zig/LLVM CPU name (x86_64,
     75     \\                            aarch64, etc). The default is X64 (aka x86_64).
     76     \\                            Also accepts a full Zig/LLVM triple, but everything
     77     \\                            except the architecture is ignored.
     78     \\
     79     \\Note: For compatibility reasons, all custom options start with :
     80     \\
     81 ;
     82 
     83 pub fn writeUsage(writer: anytype, command_name: []const u8) !void {
     84     try writer.writeAll("Usage: ");
     85     try writer.writeAll(command_name);
     86     try writer.writeAll(usage_string_after_command_name);
     87 }
     88 
     89 pub const Diagnostics = struct {
     90     errors: std.ArrayListUnmanaged(ErrorDetails) = .empty,
     91     allocator: Allocator,
     92 
     93     pub const ErrorDetails = struct {
     94         arg_index: usize,
     95         arg_span: ArgSpan = .{},
     96         msg: std.ArrayListUnmanaged(u8) = .empty,
     97         type: Type = .err,
     98         print_args: bool = true,
     99 
    100         pub const Type = enum { err, warning, note };
    101         pub const ArgSpan = struct {
    102             point_at_next_arg: bool = false,
    103             name_offset: usize = 0,
    104             prefix_len: usize = 0,
    105             value_offset: usize = 0,
    106             name_len: usize = 0,
    107         };
    108     };
    109 
    110     pub fn init(allocator: Allocator) Diagnostics {
    111         return .{
    112             .allocator = allocator,
    113         };
    114     }
    115 
    116     pub fn deinit(self: *Diagnostics) void {
    117         for (self.errors.items) |*details| {
    118             details.msg.deinit(self.allocator);
    119         }
    120         self.errors.deinit(self.allocator);
    121     }
    122 
    123     pub fn append(self: *Diagnostics, error_details: ErrorDetails) !void {
    124         try self.errors.append(self.allocator, error_details);
    125     }
    126 
    127     pub fn renderToStdErr(self: *Diagnostics, args: []const []const u8, config: std.io.tty.Config) void {
    128         const stderr = std.debug.lockStderrWriter(&.{});
    129         defer std.debug.unlockStderrWriter();
    130         self.renderToWriter(args, stderr, config) catch return;
    131     }
    132 
    133     pub fn renderToWriter(self: *Diagnostics, args: []const []const u8, writer: *std.io.Writer, config: std.io.tty.Config) !void {
    134         for (self.errors.items) |err_details| {
    135             try renderErrorMessage(writer, config, err_details, args);
    136         }
    137     }
    138 
    139     pub fn hasError(self: *const Diagnostics) bool {
    140         for (self.errors.items) |err| {
    141             if (err.type == .err) return true;
    142         }
    143         return false;
    144     }
    145 };
    146 
    147 pub const Options = struct {
    148     allocator: Allocator,
    149     input_source: IoSource = .{ .filename = &[_]u8{} },
    150     output_source: IoSource = .{ .filename = &[_]u8{} },
    151     extra_include_paths: std.ArrayListUnmanaged([]const u8) = .empty,
    152     ignore_include_env_var: bool = false,
    153     preprocess: Preprocess = .yes,
    154     default_language_id: ?u16 = null,
    155     default_code_page: ?SupportedCodePage = null,
    156     verbose: bool = false,
    157     symbols: std.StringArrayHashMapUnmanaged(SymbolValue) = .empty,
    158     null_terminate_string_table_strings: bool = false,
    159     max_string_literal_codepoints: u15 = lex.default_max_string_literal_codepoints,
    160     silent_duplicate_control_ids: bool = false,
    161     warn_instead_of_error_on_invalid_code_page: bool = false,
    162     debug: bool = false,
    163     print_help_and_exit: bool = false,
    164     auto_includes: AutoIncludes = .any,
    165     depfile_path: ?[]const u8 = null,
    166     depfile_fmt: DepfileFormat = .json,
    167     input_format: InputFormat = .rc,
    168     output_format: OutputFormat = .res,
    169     coff_options: cvtres.CoffOptions = .{},
    170 
    171     pub const IoSource = union(enum) {
    172         stdio: std.fs.File,
    173         filename: []const u8,
    174     };
    175     pub const AutoIncludes = enum { any, msvc, gnu, none };
    176     pub const DepfileFormat = enum { json };
    177     pub const InputFormat = enum { rc, res, rcpp };
    178     pub const OutputFormat = enum {
    179         res,
    180         coff,
    181         rcpp,
    182 
    183         pub fn extension(format: OutputFormat) []const u8 {
    184             return switch (format) {
    185                 .rcpp => ".rcpp",
    186                 .coff => ".obj",
    187                 .res => ".res",
    188             };
    189         }
    190     };
    191     pub const Preprocess = enum { no, yes, only };
    192     pub const SymbolAction = enum { define, undefine };
    193     pub const SymbolValue = union(SymbolAction) {
    194         define: []const u8,
    195         undefine: void,
    196 
    197         pub fn deinit(self: SymbolValue, allocator: Allocator) void {
    198             switch (self) {
    199                 .define => |value| allocator.free(value),
    200                 .undefine => {},
    201             }
    202         }
    203     };
    204 
    205     /// Does not check that identifier contains only valid characters
    206     pub fn define(self: *Options, identifier: []const u8, value: []const u8) !void {
    207         if (self.symbols.getPtr(identifier)) |val_ptr| {
    208             // If the symbol is undefined, then that always takes precedence so
    209             // we shouldn't change anything.
    210             if (val_ptr.* == .undefine) return;
    211             // Otherwise, the new value takes precedence.
    212             const duped_value = try self.allocator.dupe(u8, value);
    213             errdefer self.allocator.free(duped_value);
    214             val_ptr.deinit(self.allocator);
    215             val_ptr.* = .{ .define = duped_value };
    216             return;
    217         }
    218         const duped_key = try self.allocator.dupe(u8, identifier);
    219         errdefer self.allocator.free(duped_key);
    220         const duped_value = try self.allocator.dupe(u8, value);
    221         errdefer self.allocator.free(duped_value);
    222         try self.symbols.put(self.allocator, duped_key, .{ .define = duped_value });
    223     }
    224 
    225     /// Does not check that identifier contains only valid characters
    226     pub fn undefine(self: *Options, identifier: []const u8) !void {
    227         if (self.symbols.getPtr(identifier)) |action| {
    228             action.deinit(self.allocator);
    229             action.* = .{ .undefine = {} };
    230             return;
    231         }
    232         const duped_key = try self.allocator.dupe(u8, identifier);
    233         errdefer self.allocator.free(duped_key);
    234         try self.symbols.put(self.allocator, duped_key, .{ .undefine = {} });
    235     }
    236 
    237     /// If the current input filename:
    238     /// - does not have an extension, and
    239     /// - does not exist in the cwd, and
    240     /// - the input format is .rc
    241     /// then this function will append `.rc` to the input filename
    242     ///
    243     /// Note: This behavior is different from the Win32 compiler.
    244     ///       It always appends .RC if the filename does not have
    245     ///       a `.` in it and it does not even try the verbatim name
    246     ///       in that scenario.
    247     ///
    248     /// The approach taken here is meant to give us a 'best of both
    249     /// worlds' situation where we'll be compatible with most use-cases
    250     /// of the .rc extension being omitted from the CLI args, but still
    251     /// work fine if the file itself does not have an extension.
    252     pub fn maybeAppendRC(options: *Options, cwd: std.fs.Dir) !void {
    253         switch (options.input_source) {
    254             .stdio => return,
    255             .filename => {},
    256         }
    257         if (options.input_format == .rc and std.fs.path.extension(options.input_source.filename).len == 0) {
    258             cwd.access(options.input_source.filename, .{}) catch |err| switch (err) {
    259                 error.FileNotFound => {
    260                     var filename_bytes = try options.allocator.alloc(u8, options.input_source.filename.len + 3);
    261                     @memcpy(filename_bytes[0..options.input_source.filename.len], options.input_source.filename);
    262                     @memcpy(filename_bytes[filename_bytes.len - 3 ..], ".rc");
    263                     options.allocator.free(options.input_source.filename);
    264                     options.input_source = .{ .filename = filename_bytes };
    265                 },
    266                 else => {},
    267             };
    268         }
    269     }
    270 
    271     pub fn deinit(self: *Options) void {
    272         for (self.extra_include_paths.items) |extra_include_path| {
    273             self.allocator.free(extra_include_path);
    274         }
    275         self.extra_include_paths.deinit(self.allocator);
    276         switch (self.input_source) {
    277             .stdio => {},
    278             .filename => |filename| self.allocator.free(filename),
    279         }
    280         switch (self.output_source) {
    281             .stdio => {},
    282             .filename => |filename| self.allocator.free(filename),
    283         }
    284         var symbol_it = self.symbols.iterator();
    285         while (symbol_it.next()) |entry| {
    286             self.allocator.free(entry.key_ptr.*);
    287             entry.value_ptr.deinit(self.allocator);
    288         }
    289         self.symbols.deinit(self.allocator);
    290         if (self.depfile_path) |depfile_path| {
    291             self.allocator.free(depfile_path);
    292         }
    293         if (self.coff_options.define_external_symbol) |symbol_name| {
    294             self.allocator.free(symbol_name);
    295         }
    296     }
    297 
    298     pub fn dumpVerbose(self: *const Options, writer: anytype) !void {
    299         const input_source_name = switch (self.input_source) {
    300             .stdio => "<stdin>",
    301             .filename => |filename| filename,
    302         };
    303         const output_source_name = switch (self.output_source) {
    304             .stdio => "<stdout>",
    305             .filename => |filename| filename,
    306         };
    307         try writer.print("Input filename: {s} (format={s})\n", .{ input_source_name, @tagName(self.input_format) });
    308         try writer.print("Output filename: {s} (format={s})\n", .{ output_source_name, @tagName(self.output_format) });
    309         if (self.output_format == .coff) {
    310             try writer.print(" Target machine type for COFF: {s}\n", .{@tagName(self.coff_options.target)});
    311         }
    312 
    313         if (self.extra_include_paths.items.len > 0) {
    314             try writer.writeAll(" Extra include paths:\n");
    315             for (self.extra_include_paths.items) |extra_include_path| {
    316                 try writer.print("  \"{s}\"\n", .{extra_include_path});
    317             }
    318         }
    319         if (self.ignore_include_env_var) {
    320             try writer.writeAll(" The INCLUDE environment variable will be ignored\n");
    321         }
    322         if (self.preprocess == .no) {
    323             try writer.writeAll(" The preprocessor will not be invoked\n");
    324         } else if (self.preprocess == .only) {
    325             try writer.writeAll(" Only the preprocessor will be invoked\n");
    326         }
    327         if (self.symbols.count() > 0) {
    328             try writer.writeAll(" Symbols:\n");
    329             var it = self.symbols.iterator();
    330             while (it.next()) |symbol| {
    331                 try writer.print("  {s} {s}", .{ switch (symbol.value_ptr.*) {
    332                     .define => "#define",
    333                     .undefine => "#undef",
    334                 }, symbol.key_ptr.* });
    335                 if (symbol.value_ptr.* == .define) {
    336                     try writer.print(" {s}", .{symbol.value_ptr.define});
    337                 }
    338                 try writer.writeAll("\n");
    339             }
    340         }
    341         if (self.null_terminate_string_table_strings) {
    342             try writer.writeAll(" Strings in string tables will be null-terminated\n");
    343         }
    344         if (self.max_string_literal_codepoints != lex.default_max_string_literal_codepoints) {
    345             try writer.print(" Max string literal length: {}\n", .{self.max_string_literal_codepoints});
    346         }
    347         if (self.silent_duplicate_control_ids) {
    348             try writer.writeAll(" Duplicate control IDs will not emit warnings\n");
    349         }
    350         if (self.silent_duplicate_control_ids) {
    351             try writer.writeAll(" Invalid code page in .rc will produce a warning (instead of an error)\n");
    352         }
    353 
    354         const language_id = self.default_language_id orelse res.Language.default;
    355         const language_name = language_name: {
    356             if (std.enums.fromInt(lang.LanguageId, language_id)) |lang_enum_val| {
    357                 break :language_name @tagName(lang_enum_val);
    358             }
    359             if (language_id == lang.LOCALE_CUSTOM_UNSPECIFIED) {
    360                 break :language_name "LOCALE_CUSTOM_UNSPECIFIED";
    361             }
    362             break :language_name "<UNKNOWN>";
    363         };
    364         try writer.print("Default language: {s} (id=0x{x})\n", .{ language_name, language_id });
    365 
    366         const code_page = self.default_code_page orelse .windows1252;
    367         try writer.print("Default codepage: {s} (id={})\n", .{ @tagName(code_page), @intFromEnum(code_page) });
    368     }
    369 };
    370 
    371 pub const Arg = struct {
    372     prefix: enum { long, short, slash },
    373     name_offset: usize,
    374     full: []const u8,
    375 
    376     pub fn fromString(str: []const u8) ?@This() {
    377         if (std.mem.startsWith(u8, str, "--")) {
    378             return .{ .prefix = .long, .name_offset = 2, .full = str };
    379         } else if (std.mem.startsWith(u8, str, "-")) {
    380             return .{ .prefix = .short, .name_offset = 1, .full = str };
    381         } else if (std.mem.startsWith(u8, str, "/")) {
    382             return .{ .prefix = .slash, .name_offset = 1, .full = str };
    383         }
    384         return null;
    385     }
    386 
    387     pub fn prefixSlice(self: Arg) []const u8 {
    388         return self.full[0..(if (self.prefix == .long) 2 else 1)];
    389     }
    390 
    391     pub fn name(self: Arg) []const u8 {
    392         return self.full[self.name_offset..];
    393     }
    394 
    395     pub fn optionWithoutPrefix(self: Arg, option_len: usize) []const u8 {
    396         if (option_len == 0) return self.name();
    397         return self.name()[0..option_len];
    398     }
    399 
    400     pub fn missingSpan(self: Arg) Diagnostics.ErrorDetails.ArgSpan {
    401         return .{
    402             .point_at_next_arg = true,
    403             .value_offset = 0,
    404             .name_offset = self.name_offset,
    405             .prefix_len = self.prefixSlice().len,
    406         };
    407     }
    408 
    409     pub fn optionAndAfterSpan(self: Arg) Diagnostics.ErrorDetails.ArgSpan {
    410         return self.optionSpan(0);
    411     }
    412 
    413     pub fn optionSpan(self: Arg, option_len: usize) Diagnostics.ErrorDetails.ArgSpan {
    414         return .{
    415             .name_offset = self.name_offset,
    416             .prefix_len = self.prefixSlice().len,
    417             .name_len = option_len,
    418         };
    419     }
    420 
    421     pub fn looksLikeFilepath(self: Arg) bool {
    422         const meets_min_requirements = self.prefix == .slash and isSupportedInputExtension(std.fs.path.extension(self.full));
    423         if (!meets_min_requirements) return false;
    424 
    425         const could_be_fo_option = could_be_fo_option: {
    426             var window_it = std.mem.window(u8, self.full[1..], 2, 1);
    427             while (window_it.next()) |window| {
    428                 if (std.ascii.eqlIgnoreCase(window, "fo")) break :could_be_fo_option true;
    429                 // If we see '/' before "fo", then it's not possible for this to be a valid
    430                 // `/fo` option.
    431                 if (window[0] == '/') break;
    432             }
    433             break :could_be_fo_option false;
    434         };
    435         if (!could_be_fo_option) return true;
    436 
    437         // It's still possible for a file path to look like a /fo option but not actually
    438         // be one, e.g. `/foo/bar.rc`. As a last ditch effort to reduce false negatives,
    439         // check if the file path exists and, if so, then we ignore the 'could be /fo option'-ness
    440         std.fs.accessAbsolute(self.full, .{}) catch return false;
    441         return true;
    442     }
    443 
    444     pub const Value = struct {
    445         slice: []const u8,
    446         /// Amount to increment the arg index to skip over both the option and the value arg(s)
    447         /// e.g. 1 if /<option><value>, 2 if /<option> <value>
    448         index_increment: u2 = 1,
    449 
    450         pub fn argSpan(self: Value, arg: Arg) Diagnostics.ErrorDetails.ArgSpan {
    451             const prefix_len = arg.prefixSlice().len;
    452             switch (self.index_increment) {
    453                 1 => return .{
    454                     .value_offset = @intFromPtr(self.slice.ptr) - @intFromPtr(arg.full.ptr),
    455                     .prefix_len = prefix_len,
    456                     .name_offset = arg.name_offset,
    457                 },
    458                 2 => return .{
    459                     .point_at_next_arg = true,
    460                     .prefix_len = prefix_len,
    461                     .name_offset = arg.name_offset,
    462                 },
    463                 else => unreachable,
    464             }
    465         }
    466 
    467         pub fn index(self: Value, arg_index: usize) usize {
    468             if (self.index_increment == 2) return arg_index + 1;
    469             return arg_index;
    470         }
    471     };
    472 
    473     pub fn value(self: Arg, option_len: usize, index: usize, args: []const []const u8) error{MissingValue}!Value {
    474         const rest = self.full[self.name_offset + option_len ..];
    475         if (rest.len > 0) return .{ .slice = rest };
    476         if (index + 1 >= args.len) return error.MissingValue;
    477         return .{ .slice = args[index + 1], .index_increment = 2 };
    478     }
    479 
    480     pub const Context = struct {
    481         index: usize,
    482         option_len: usize,
    483         arg: Arg,
    484         value: Value,
    485     };
    486 };
    487 
    488 pub const ParseError = error{ParseError} || Allocator.Error;
    489 
    490 /// Note: Does not run `Options.maybeAppendRC` automatically. If that behavior is desired,
    491 ///       it must be called separately.
    492 pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagnostics) ParseError!Options {
    493     var options = Options{ .allocator = allocator };
    494     errdefer options.deinit();
    495 
    496     var output_filename: ?[]const u8 = null;
    497     var output_filename_context: union(enum) {
    498         unspecified: void,
    499         positional: usize,
    500         arg: Arg.Context,
    501     } = .{ .unspecified = {} };
    502     var output_format: ?Options.OutputFormat = null;
    503     var output_format_context: Arg.Context = undefined;
    504     var input_format: ?Options.InputFormat = null;
    505     var input_format_context: Arg.Context = undefined;
    506     var input_filename_arg_i: usize = undefined;
    507     var preprocess_only_context: Arg.Context = undefined;
    508     var depfile_context: Arg.Context = undefined;
    509 
    510     var arg_i: usize = 0;
    511     next_arg: while (arg_i < args.len) {
    512         var arg = Arg.fromString(args[arg_i]) orelse break;
    513         if (arg.name().len == 0) {
    514             switch (arg.prefix) {
    515                 // -- on its own ends arg parsing
    516                 .long => {
    517                     arg_i += 1;
    518                     break;
    519                 },
    520                 // - or / on its own is an error
    521                 else => {
    522                     var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.optionAndAfterSpan() };
    523                     var msg_writer = err_details.msg.writer(allocator);
    524                     try msg_writer.print("invalid option: {s}", .{arg.prefixSlice()});
    525                     try diagnostics.append(err_details);
    526                     arg_i += 1;
    527                     continue :next_arg;
    528                 },
    529             }
    530         }
    531 
    532         const args_remaining = args.len - arg_i;
    533         if (args_remaining <= 2 and arg.looksLikeFilepath()) {
    534             var err_details = Diagnostics.ErrorDetails{ .type = .note, .print_args = true, .arg_index = arg_i };
    535             var msg_writer = err_details.msg.writer(allocator);
    536             try msg_writer.writeAll("this argument was inferred to be a filepath, so argument parsing was terminated");
    537             try diagnostics.append(err_details);
    538 
    539             break;
    540         }
    541 
    542         while (arg.name().len > 0) {
    543             const arg_name = arg.name();
    544             // Note: These cases should be in order from longest to shortest, since
    545             //       shorter options that are a substring of a longer one could make
    546             //       the longer option's branch unreachable.
    547             if (std.ascii.startsWithIgnoreCase(arg_name, ":no-preprocess")) {
    548                 options.preprocess = .no;
    549                 arg.name_offset += ":no-preprocess".len;
    550             } else if (std.ascii.startsWithIgnoreCase(arg_name, ":output-format")) {
    551                 const value = arg.value(":output-format".len, arg_i, args) catch {
    552                     var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
    553                     var msg_writer = err_details.msg.writer(allocator);
    554                     try msg_writer.print("missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(":output-format".len) });
    555                     try diagnostics.append(err_details);
    556                     arg_i += 1;
    557                     break :next_arg;
    558                 };
    559                 output_format = std.meta.stringToEnum(Options.OutputFormat, value.slice) orelse blk: {
    560                     var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
    561                     var msg_writer = err_details.msg.writer(allocator);
    562                     try msg_writer.print("invalid output format setting: {s} ", .{value.slice});
    563                     try diagnostics.append(err_details);
    564                     break :blk output_format;
    565                 };
    566                 output_format_context = .{ .index = arg_i, .option_len = ":output-format".len, .arg = arg, .value = value };
    567                 arg_i += value.index_increment;
    568                 continue :next_arg;
    569             } else if (std.ascii.startsWithIgnoreCase(arg_name, ":auto-includes")) {
    570                 const value = arg.value(":auto-includes".len, arg_i, args) catch {
    571                     var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
    572                     var msg_writer = err_details.msg.writer(allocator);
    573                     try msg_writer.print("missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(":auto-includes".len) });
    574                     try diagnostics.append(err_details);
    575                     arg_i += 1;
    576                     break :next_arg;
    577                 };
    578                 options.auto_includes = std.meta.stringToEnum(Options.AutoIncludes, value.slice) orelse blk: {
    579                     var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
    580                     var msg_writer = err_details.msg.writer(allocator);
    581                     try msg_writer.print("invalid auto includes setting: {s} ", .{value.slice});
    582                     try diagnostics.append(err_details);
    583                     break :blk options.auto_includes;
    584                 };
    585                 arg_i += value.index_increment;
    586                 continue :next_arg;
    587             } else if (std.ascii.startsWithIgnoreCase(arg_name, ":input-format")) {
    588                 const value = arg.value(":input-format".len, arg_i, args) catch {
    589                     var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
    590                     var msg_writer = err_details.msg.writer(allocator);
    591                     try msg_writer.print("missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(":input-format".len) });
    592                     try diagnostics.append(err_details);
    593                     arg_i += 1;
    594                     break :next_arg;
    595                 };
    596                 input_format = std.meta.stringToEnum(Options.InputFormat, value.slice) orelse blk: {
    597                     var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
    598                     var msg_writer = err_details.msg.writer(allocator);
    599                     try msg_writer.print("invalid input format setting: {s} ", .{value.slice});
    600                     try diagnostics.append(err_details);
    601                     break :blk input_format;
    602                 };
    603                 input_format_context = .{ .index = arg_i, .option_len = ":input-format".len, .arg = arg, .value = value };
    604                 arg_i += value.index_increment;
    605                 continue :next_arg;
    606             } else if (std.ascii.startsWithIgnoreCase(arg_name, ":depfile-fmt")) {
    607                 const value = arg.value(":depfile-fmt".len, arg_i, args) catch {
    608                     var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
    609                     var msg_writer = err_details.msg.writer(allocator);
    610                     try msg_writer.print("missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(":depfile-fmt".len) });
    611                     try diagnostics.append(err_details);
    612                     arg_i += 1;
    613                     break :next_arg;
    614                 };
    615                 options.depfile_fmt = std.meta.stringToEnum(Options.DepfileFormat, value.slice) orelse blk: {
    616                     var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
    617                     var msg_writer = err_details.msg.writer(allocator);
    618                     try msg_writer.print("invalid depfile format setting: {s} ", .{value.slice});
    619                     try diagnostics.append(err_details);
    620                     break :blk options.depfile_fmt;
    621                 };
    622                 arg_i += value.index_increment;
    623                 continue :next_arg;
    624             } else if (std.ascii.startsWithIgnoreCase(arg_name, ":depfile")) {
    625                 const value = arg.value(":depfile".len, arg_i, args) catch {
    626                     var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
    627                     var msg_writer = err_details.msg.writer(allocator);
    628                     try msg_writer.print("missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(":depfile".len) });
    629                     try diagnostics.append(err_details);
    630                     arg_i += 1;
    631                     break :next_arg;
    632                 };
    633                 if (options.depfile_path) |overwritten_path| {
    634                     allocator.free(overwritten_path);
    635                     options.depfile_path = null;
    636                 }
    637                 const path = try allocator.dupe(u8, value.slice);
    638                 errdefer allocator.free(path);
    639                 options.depfile_path = path;
    640                 depfile_context = .{ .index = arg_i, .option_len = ":depfile".len, .arg = arg, .value = value };
    641                 arg_i += value.index_increment;
    642                 continue :next_arg;
    643             } else if (std.ascii.startsWithIgnoreCase(arg_name, ":target")) {
    644                 const value = arg.value(":target".len, arg_i, args) catch {
    645                     var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
    646                     var msg_writer = err_details.msg.writer(allocator);
    647                     try msg_writer.print("missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(":target".len) });
    648                     try diagnostics.append(err_details);
    649                     arg_i += 1;
    650                     break :next_arg;
    651                 };
    652                 // Take the substring up to the first dash so that a full target triple
    653                 // can be used, e.g. x86_64-windows-gnu becomes x86_64
    654                 var target_it = std.mem.splitScalar(u8, value.slice, '-');
    655                 const arch_str = target_it.first();
    656                 const arch = cvtres.supported_targets.Arch.fromStringIgnoreCase(arch_str) orelse {
    657                     var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
    658                     var msg_writer = err_details.msg.writer(allocator);
    659                     try msg_writer.print("invalid or unsupported target architecture: {s}", .{arch_str});
    660                     try diagnostics.append(err_details);
    661                     arg_i += value.index_increment;
    662                     continue :next_arg;
    663                 };
    664                 options.coff_options.target = arch.toCoffMachineType();
    665                 arg_i += value.index_increment;
    666                 continue :next_arg;
    667             } else if (std.ascii.startsWithIgnoreCase(arg_name, "nologo")) {
    668                 // No-op, we don't display any 'logo' to suppress
    669                 arg.name_offset += "nologo".len;
    670             } else if (std.ascii.startsWithIgnoreCase(arg_name, ":debug")) {
    671                 options.debug = true;
    672                 arg.name_offset += ":debug".len;
    673             }
    674             // Unsupported LCX/LCE options that need a value (within the same arg only)
    675             else if (std.ascii.startsWithIgnoreCase(arg_name, "tp:")) {
    676                 const rest = arg.full[arg.name_offset + 3 ..];
    677                 if (rest.len == 0) {
    678                     var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = .{
    679                         .name_offset = arg.name_offset,
    680                         .prefix_len = arg.prefixSlice().len,
    681                         .value_offset = arg.name_offset + 3,
    682                     } };
    683                     var msg_writer = err_details.msg.writer(allocator);
    684                     try msg_writer.print("missing value for {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(3) });
    685                     try diagnostics.append(err_details);
    686                 }
    687                 var err_details = Diagnostics.ErrorDetails{ .type = .err, .arg_index = arg_i, .arg_span = arg.optionAndAfterSpan() };
    688                 var msg_writer = err_details.msg.writer(allocator);
    689                 try msg_writer.print("the {s}{s} option is unsupported", .{ arg.prefixSlice(), arg.optionWithoutPrefix(3) });
    690                 try diagnostics.append(err_details);
    691                 arg_i += 1;
    692                 continue :next_arg;
    693             }
    694             // Unsupported LCX/LCE options that need a value
    695             else if (std.ascii.startsWithIgnoreCase(arg_name, "tn")) {
    696                 const value = arg.value(2, arg_i, args) catch no_value: {
    697                     var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
    698                     var msg_writer = err_details.msg.writer(allocator);
    699                     try msg_writer.print("missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) });
    700                     try diagnostics.append(err_details);
    701                     // dummy zero-length slice starting where the value would have been
    702                     const value_start = arg.name_offset + 2;
    703                     break :no_value Arg.Value{ .slice = arg.full[value_start..value_start] };
    704                 };
    705                 var err_details = Diagnostics.ErrorDetails{ .type = .err, .arg_index = arg_i, .arg_span = arg.optionAndAfterSpan() };
    706                 var msg_writer = err_details.msg.writer(allocator);
    707                 try msg_writer.print("the {s}{s} option is unsupported", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) });
    708                 try diagnostics.append(err_details);
    709                 arg_i += value.index_increment;
    710                 continue :next_arg;
    711             }
    712             // Unsupported MUI options that need a value
    713             else if (std.ascii.startsWithIgnoreCase(arg_name, "fm") or
    714                 std.ascii.startsWithIgnoreCase(arg_name, "gn") or
    715                 std.ascii.startsWithIgnoreCase(arg_name, "g2"))
    716             {
    717                 const value = arg.value(2, arg_i, args) catch no_value: {
    718                     var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
    719                     var msg_writer = err_details.msg.writer(allocator);
    720                     try msg_writer.print("missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) });
    721                     try diagnostics.append(err_details);
    722                     // dummy zero-length slice starting where the value would have been
    723                     const value_start = arg.name_offset + 2;
    724                     break :no_value Arg.Value{ .slice = arg.full[value_start..value_start] };
    725                 };
    726                 var err_details = Diagnostics.ErrorDetails{ .type = .err, .arg_index = arg_i, .arg_span = arg.optionAndAfterSpan() };
    727                 var msg_writer = err_details.msg.writer(allocator);
    728                 try msg_writer.print("the {s}{s} option is unsupported", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) });
    729                 try diagnostics.append(err_details);
    730                 arg_i += value.index_increment;
    731                 continue :next_arg;
    732             }
    733             // Unsupported MUI options that do not need a value
    734             else if (std.ascii.startsWithIgnoreCase(arg_name, "g1")) {
    735                 var err_details = Diagnostics.ErrorDetails{ .type = .err, .arg_index = arg_i, .arg_span = arg.optionSpan(2) };
    736                 var msg_writer = err_details.msg.writer(allocator);
    737                 try msg_writer.print("the {s}{s} option is unsupported", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) });
    738                 try diagnostics.append(err_details);
    739                 arg.name_offset += 2;
    740             }
    741             // Unsupported LCX/LCE options that do not need a value
    742             else if (std.ascii.startsWithIgnoreCase(arg_name, "tm") or
    743                 std.ascii.startsWithIgnoreCase(arg_name, "tc") or
    744                 std.ascii.startsWithIgnoreCase(arg_name, "tw") or
    745                 std.ascii.startsWithIgnoreCase(arg_name, "te") or
    746                 std.ascii.startsWithIgnoreCase(arg_name, "ti") or
    747                 std.ascii.startsWithIgnoreCase(arg_name, "ta"))
    748             {
    749                 var err_details = Diagnostics.ErrorDetails{ .type = .err, .arg_index = arg_i, .arg_span = arg.optionSpan(2) };
    750                 var msg_writer = err_details.msg.writer(allocator);
    751                 try msg_writer.print("the {s}{s} option is unsupported", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) });
    752                 try diagnostics.append(err_details);
    753                 arg.name_offset += 2;
    754             } else if (std.ascii.startsWithIgnoreCase(arg_name, "fo")) {
    755                 const value = arg.value(2, arg_i, args) catch {
    756                     var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
    757                     var msg_writer = err_details.msg.writer(allocator);
    758                     try msg_writer.print("missing output path after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) });
    759                     try diagnostics.append(err_details);
    760                     arg_i += 1;
    761                     break :next_arg;
    762                 };
    763                 output_filename_context = .{ .arg = .{ .index = arg_i, .option_len = "fo".len, .arg = arg, .value = value } };
    764                 output_filename = value.slice;
    765                 arg_i += value.index_increment;
    766                 continue :next_arg;
    767             } else if (std.ascii.startsWithIgnoreCase(arg_name, "sl")) {
    768                 const value = arg.value(2, arg_i, args) catch {
    769                     var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
    770                     var msg_writer = err_details.msg.writer(allocator);
    771                     try msg_writer.print("missing language tag after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) });
    772                     try diagnostics.append(err_details);
    773                     arg_i += 1;
    774                     break :next_arg;
    775                 };
    776                 const percent_str = value.slice;
    777                 const percent: u32 = parsePercent(percent_str) catch {
    778                     var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
    779                     var msg_writer = err_details.msg.writer(allocator);
    780                     try msg_writer.print("invalid percent format '{s}'", .{percent_str});
    781                     try diagnostics.append(err_details);
    782                     var note_details = Diagnostics.ErrorDetails{ .type = .note, .print_args = false, .arg_index = arg_i };
    783                     var note_writer = note_details.msg.writer(allocator);
    784                     try note_writer.writeAll("string length percent must be an integer between 1 and 100 (inclusive)");
    785                     try diagnostics.append(note_details);
    786                     arg_i += value.index_increment;
    787                     continue :next_arg;
    788                 };
    789                 if (percent == 0 or percent > 100) {
    790                     var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
    791                     var msg_writer = err_details.msg.writer(allocator);
    792                     try msg_writer.print("percent out of range: {} (parsed from '{s}')", .{ percent, percent_str });
    793                     try diagnostics.append(err_details);
    794                     var note_details = Diagnostics.ErrorDetails{ .type = .note, .print_args = false, .arg_index = arg_i };
    795                     var note_writer = note_details.msg.writer(allocator);
    796                     try note_writer.writeAll("string length percent must be an integer between 1 and 100 (inclusive)");
    797                     try diagnostics.append(note_details);
    798                     arg_i += value.index_increment;
    799                     continue :next_arg;
    800                 }
    801                 const percent_float = @as(f32, @floatFromInt(percent)) / 100;
    802                 options.max_string_literal_codepoints = @intFromFloat(percent_float * max_string_literal_length_100_percent);
    803                 arg_i += value.index_increment;
    804                 continue :next_arg;
    805             } else if (std.ascii.startsWithIgnoreCase(arg_name, "ln")) {
    806                 const value = arg.value(2, arg_i, args) catch {
    807                     var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
    808                     var msg_writer = err_details.msg.writer(allocator);
    809                     try msg_writer.print("missing language tag after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) });
    810                     try diagnostics.append(err_details);
    811                     arg_i += 1;
    812                     break :next_arg;
    813                 };
    814                 const tag = value.slice;
    815                 options.default_language_id = lang.tagToInt(tag) catch {
    816                     var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
    817                     var msg_writer = err_details.msg.writer(allocator);
    818                     try msg_writer.print("invalid language tag: {s}", .{tag});
    819                     try diagnostics.append(err_details);
    820                     arg_i += value.index_increment;
    821                     continue :next_arg;
    822                 };
    823                 if (options.default_language_id.? == lang.LOCALE_CUSTOM_UNSPECIFIED) {
    824                     var err_details = Diagnostics.ErrorDetails{ .type = .warning, .arg_index = arg_i, .arg_span = value.argSpan(arg) };
    825                     var msg_writer = err_details.msg.writer(allocator);
    826                     try msg_writer.print("language tag '{s}' does not have an assigned ID so it will be resolved to LOCALE_CUSTOM_UNSPECIFIED (id=0x{x})", .{ tag, lang.LOCALE_CUSTOM_UNSPECIFIED });
    827                     try diagnostics.append(err_details);
    828                 }
    829                 arg_i += value.index_increment;
    830                 continue :next_arg;
    831             } else if (std.ascii.startsWithIgnoreCase(arg_name, "l")) {
    832                 const value = arg.value(1, arg_i, args) catch {
    833                     var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
    834                     var msg_writer = err_details.msg.writer(allocator);
    835                     try msg_writer.print("missing language ID after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
    836                     try diagnostics.append(err_details);
    837                     arg_i += 1;
    838                     break :next_arg;
    839                 };
    840                 const num_str = value.slice;
    841                 options.default_language_id = lang.parseInt(num_str) catch {
    842                     var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
    843                     var msg_writer = err_details.msg.writer(allocator);
    844                     try msg_writer.print("invalid language ID: {s}", .{num_str});
    845                     try diagnostics.append(err_details);
    846                     arg_i += value.index_increment;
    847                     continue :next_arg;
    848                 };
    849                 arg_i += value.index_increment;
    850                 continue :next_arg;
    851             } else if (std.ascii.startsWithIgnoreCase(arg_name, "h") or std.mem.startsWith(u8, arg_name, "?")) {
    852                 options.print_help_and_exit = true;
    853                 // If there's been an error to this point, then we still want to fail
    854                 if (diagnostics.hasError()) return error.ParseError;
    855                 return options;
    856             }
    857             // 1 char unsupported MUI options that need a value
    858             else if (std.ascii.startsWithIgnoreCase(arg_name, "q") or
    859                 std.ascii.startsWithIgnoreCase(arg_name, "g"))
    860             {
    861                 const value = arg.value(1, arg_i, args) catch no_value: {
    862                     var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
    863                     var msg_writer = err_details.msg.writer(allocator);
    864                     try msg_writer.print("missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
    865                     try diagnostics.append(err_details);
    866                     // dummy zero-length slice starting where the value would have been
    867                     const value_start = arg.name_offset + 1;
    868                     break :no_value Arg.Value{ .slice = arg.full[value_start..value_start] };
    869                 };
    870                 var err_details = Diagnostics.ErrorDetails{ .type = .err, .arg_index = arg_i, .arg_span = arg.optionAndAfterSpan() };
    871                 var msg_writer = err_details.msg.writer(allocator);
    872                 try msg_writer.print("the {s}{s} option is unsupported", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
    873                 try diagnostics.append(err_details);
    874                 arg_i += value.index_increment;
    875                 continue :next_arg;
    876             }
    877             // Undocumented (and unsupported) options that need a value
    878             //  /z has to do something with font substitution
    879             //  /s has something to do with HWB resources being inserted into the .res
    880             else if (std.ascii.startsWithIgnoreCase(arg_name, "z") or
    881                 std.ascii.startsWithIgnoreCase(arg_name, "s"))
    882             {
    883                 const value = arg.value(1, arg_i, args) catch no_value: {
    884                     var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
    885                     var msg_writer = err_details.msg.writer(allocator);
    886                     try msg_writer.print("missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
    887                     try diagnostics.append(err_details);
    888                     // dummy zero-length slice starting where the value would have been
    889                     const value_start = arg.name_offset + 1;
    890                     break :no_value Arg.Value{ .slice = arg.full[value_start..value_start] };
    891                 };
    892                 var err_details = Diagnostics.ErrorDetails{ .type = .err, .arg_index = arg_i, .arg_span = arg.optionAndAfterSpan() };
    893                 var msg_writer = err_details.msg.writer(allocator);
    894                 try msg_writer.print("the {s}{s} option is unsupported", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
    895                 try diagnostics.append(err_details);
    896                 arg_i += value.index_increment;
    897                 continue :next_arg;
    898             }
    899             // 1 char unsupported LCX/LCE options that do not need a value
    900             else if (std.ascii.startsWithIgnoreCase(arg_name, "t")) {
    901                 var err_details = Diagnostics.ErrorDetails{ .type = .err, .arg_index = arg_i, .arg_span = arg.optionSpan(1) };
    902                 var msg_writer = err_details.msg.writer(allocator);
    903                 try msg_writer.print("the {s}{s} option is unsupported", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
    904                 try diagnostics.append(err_details);
    905                 arg.name_offset += 1;
    906             } else if (std.ascii.startsWithIgnoreCase(arg_name, "c")) {
    907                 const value = arg.value(1, arg_i, args) catch {
    908                     var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
    909                     var msg_writer = err_details.msg.writer(allocator);
    910                     try msg_writer.print("missing code page ID after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
    911                     try diagnostics.append(err_details);
    912                     arg_i += 1;
    913                     break :next_arg;
    914                 };
    915                 const num_str = value.slice;
    916                 const code_page_id = std.fmt.parseUnsigned(u16, num_str, 10) catch {
    917                     var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
    918                     var msg_writer = err_details.msg.writer(allocator);
    919                     try msg_writer.print("invalid code page ID: {s}", .{num_str});
    920                     try diagnostics.append(err_details);
    921                     arg_i += value.index_increment;
    922                     continue :next_arg;
    923                 };
    924                 options.default_code_page = code_pages.getByIdentifierEnsureSupported(code_page_id) catch |err| switch (err) {
    925                     error.InvalidCodePage => {
    926                         var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
    927                         var msg_writer = err_details.msg.writer(allocator);
    928                         try msg_writer.print("invalid or unknown code page ID: {}", .{code_page_id});
    929                         try diagnostics.append(err_details);
    930                         arg_i += value.index_increment;
    931                         continue :next_arg;
    932                     },
    933                     error.UnsupportedCodePage => {
    934                         var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
    935                         var msg_writer = err_details.msg.writer(allocator);
    936                         try msg_writer.print("unsupported code page: {s} (id={})", .{
    937                             @tagName(code_pages.getByIdentifier(code_page_id) catch unreachable),
    938                             code_page_id,
    939                         });
    940                         try diagnostics.append(err_details);
    941                         arg_i += value.index_increment;
    942                         continue :next_arg;
    943                     },
    944                 };
    945                 arg_i += value.index_increment;
    946                 continue :next_arg;
    947             } else if (std.ascii.startsWithIgnoreCase(arg_name, "v")) {
    948                 options.verbose = true;
    949                 arg.name_offset += 1;
    950             } else if (std.ascii.startsWithIgnoreCase(arg_name, "x")) {
    951                 options.ignore_include_env_var = true;
    952                 arg.name_offset += 1;
    953             } else if (std.ascii.startsWithIgnoreCase(arg_name, "p")) {
    954                 options.preprocess = .only;
    955                 preprocess_only_context = .{ .index = arg_i, .option_len = "p".len, .arg = arg, .value = undefined };
    956                 arg.name_offset += 1;
    957             } else if (std.ascii.startsWithIgnoreCase(arg_name, "i")) {
    958                 const value = arg.value(1, arg_i, args) catch {
    959                     var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
    960                     var msg_writer = err_details.msg.writer(allocator);
    961                     try msg_writer.print("missing include path after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
    962                     try diagnostics.append(err_details);
    963                     arg_i += 1;
    964                     break :next_arg;
    965                 };
    966                 const path = value.slice;
    967                 const duped = try allocator.dupe(u8, path);
    968                 errdefer allocator.free(duped);
    969                 try options.extra_include_paths.append(options.allocator, duped);
    970                 arg_i += value.index_increment;
    971                 continue :next_arg;
    972             } else if (std.ascii.startsWithIgnoreCase(arg_name, "r")) {
    973                 // From https://learn.microsoft.com/en-us/windows/win32/menurc/using-rc-the-rc-command-line-
    974                 // "Ignored. Provided for compatibility with existing makefiles."
    975                 arg.name_offset += 1;
    976             } else if (std.ascii.startsWithIgnoreCase(arg_name, "n")) {
    977                 options.null_terminate_string_table_strings = true;
    978                 arg.name_offset += 1;
    979             } else if (std.ascii.startsWithIgnoreCase(arg_name, "y")) {
    980                 options.silent_duplicate_control_ids = true;
    981                 arg.name_offset += 1;
    982             } else if (std.ascii.startsWithIgnoreCase(arg_name, "w")) {
    983                 options.warn_instead_of_error_on_invalid_code_page = true;
    984                 arg.name_offset += 1;
    985             } else if (std.ascii.startsWithIgnoreCase(arg_name, "a")) {
    986                 // Undocumented option with unknown function
    987                 // TODO: More investigation to figure out what it does (if anything)
    988                 var err_details = Diagnostics.ErrorDetails{ .type = .warning, .arg_index = arg_i, .arg_span = arg.optionSpan(1) };
    989                 var msg_writer = err_details.msg.writer(allocator);
    990                 try msg_writer.print("option {s}{s} has no effect (it is undocumented and its function is unknown in the Win32 RC compiler)", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
    991                 try diagnostics.append(err_details);
    992                 arg.name_offset += 1;
    993             } else if (std.ascii.startsWithIgnoreCase(arg_name, "d")) {
    994                 const value = arg.value(1, arg_i, args) catch {
    995                     var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
    996                     var msg_writer = err_details.msg.writer(allocator);
    997                     try msg_writer.print("missing symbol to define after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
    998                     try diagnostics.append(err_details);
    999                     arg_i += 1;
   1000                     break :next_arg;
   1001                 };
   1002                 var tokenizer = std.mem.tokenizeScalar(u8, value.slice, '=');
   1003                 // guaranteed to exist since an empty value.slice would invoke
   1004                 // the 'missing symbol to define' branch above
   1005                 const symbol = tokenizer.next().?;
   1006                 const symbol_value = tokenizer.next() orelse "1";
   1007 
   1008                 if (isValidIdentifier(symbol)) {
   1009                     try options.define(symbol, symbol_value);
   1010                 } else {
   1011                     var err_details = Diagnostics.ErrorDetails{ .type = .warning, .arg_index = arg_i, .arg_span = value.argSpan(arg) };
   1012                     var msg_writer = err_details.msg.writer(allocator);
   1013                     try msg_writer.print("symbol \"{s}\" is not a valid identifier and therefore cannot be defined", .{symbol});
   1014                     try diagnostics.append(err_details);
   1015                 }
   1016                 arg_i += value.index_increment;
   1017                 continue :next_arg;
   1018             } else if (std.ascii.startsWithIgnoreCase(arg_name, "u")) {
   1019                 const value = arg.value(1, arg_i, args) catch {
   1020                     var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
   1021                     var msg_writer = err_details.msg.writer(allocator);
   1022                     try msg_writer.print("missing symbol to undefine after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
   1023                     try diagnostics.append(err_details);
   1024                     arg_i += 1;
   1025                     break :next_arg;
   1026                 };
   1027                 const symbol = value.slice;
   1028                 if (isValidIdentifier(symbol)) {
   1029                     try options.undefine(symbol);
   1030                 } else {
   1031                     var err_details = Diagnostics.ErrorDetails{ .type = .warning, .arg_index = arg_i, .arg_span = value.argSpan(arg) };
   1032                     var msg_writer = err_details.msg.writer(allocator);
   1033                     try msg_writer.print("symbol \"{s}\" is not a valid identifier and therefore cannot be undefined", .{symbol});
   1034                     try diagnostics.append(err_details);
   1035                 }
   1036                 arg_i += value.index_increment;
   1037                 continue :next_arg;
   1038             } else {
   1039                 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.optionAndAfterSpan() };
   1040                 var msg_writer = err_details.msg.writer(allocator);
   1041                 try msg_writer.print("invalid option: {s}{s}", .{ arg.prefixSlice(), arg.name() });
   1042                 try diagnostics.append(err_details);
   1043                 arg_i += 1;
   1044                 continue :next_arg;
   1045             }
   1046         } else {
   1047             // The while loop exited via its conditional, meaning we are done with
   1048             // the current arg and can move on the the next
   1049             arg_i += 1;
   1050             continue;
   1051         }
   1052     }
   1053 
   1054     const positionals = args[arg_i..];
   1055 
   1056     if (positionals.len == 0) {
   1057         var err_details = Diagnostics.ErrorDetails{ .print_args = false, .arg_index = arg_i };
   1058         var msg_writer = err_details.msg.writer(allocator);
   1059         try msg_writer.writeAll("missing input filename");
   1060         try diagnostics.append(err_details);
   1061 
   1062         if (args.len > 0) {
   1063             const last_arg = args[args.len - 1];
   1064             if (arg_i > 0 and last_arg.len > 0 and last_arg[0] == '/' and isSupportedInputExtension(std.fs.path.extension(last_arg))) {
   1065                 var note_details = Diagnostics.ErrorDetails{ .type = .note, .print_args = true, .arg_index = arg_i - 1 };
   1066                 var note_writer = note_details.msg.writer(allocator);
   1067                 try note_writer.writeAll("if this argument was intended to be the input filename, adding -- in front of it will exclude it from option parsing");
   1068                 try diagnostics.append(note_details);
   1069             }
   1070         }
   1071 
   1072         // This is a fatal enough problem to justify an early return, since
   1073         // things after this rely on the value of the input filename.
   1074         return error.ParseError;
   1075     }
   1076     options.input_source = .{ .filename = try allocator.dupe(u8, positionals[0]) };
   1077     input_filename_arg_i = arg_i;
   1078 
   1079     const InputFormatSource = enum {
   1080         inferred_from_input_filename,
   1081         input_format_arg,
   1082     };
   1083 
   1084     var input_format_source: InputFormatSource = undefined;
   1085     if (input_format == null) {
   1086         const ext = std.fs.path.extension(options.input_source.filename);
   1087         if (std.ascii.eqlIgnoreCase(ext, ".res")) {
   1088             input_format = .res;
   1089         } else if (std.ascii.eqlIgnoreCase(ext, ".rcpp")) {
   1090             input_format = .rcpp;
   1091         } else {
   1092             input_format = .rc;
   1093         }
   1094         input_format_source = .inferred_from_input_filename;
   1095     } else {
   1096         input_format_source = .input_format_arg;
   1097     }
   1098 
   1099     if (positionals.len > 1) {
   1100         if (output_filename != null) {
   1101             var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i + 1 };
   1102             var msg_writer = err_details.msg.writer(allocator);
   1103             try msg_writer.writeAll("output filename already specified");
   1104             try diagnostics.append(err_details);
   1105             var note_details = Diagnostics.ErrorDetails{
   1106                 .type = .note,
   1107                 .arg_index = output_filename_context.arg.index,
   1108                 .arg_span = output_filename_context.arg.value.argSpan(output_filename_context.arg.arg),
   1109             };
   1110             var note_writer = note_details.msg.writer(allocator);
   1111             try note_writer.writeAll("output filename previously specified here");
   1112             try diagnostics.append(note_details);
   1113         } else {
   1114             output_filename = positionals[1];
   1115             output_filename_context = .{ .positional = arg_i + 1 };
   1116         }
   1117     }
   1118 
   1119     const OutputFormatSource = enum {
   1120         inferred_from_input_filename,
   1121         inferred_from_output_filename,
   1122         output_format_arg,
   1123         unable_to_infer_from_input_filename,
   1124         unable_to_infer_from_output_filename,
   1125         inferred_from_preprocess_only,
   1126     };
   1127 
   1128     var output_format_source: OutputFormatSource = undefined;
   1129     if (output_filename == null) {
   1130         if (output_format == null) {
   1131             output_format_source = .inferred_from_input_filename;
   1132             const input_ext = std.fs.path.extension(options.input_source.filename);
   1133             if (std.ascii.eqlIgnoreCase(input_ext, ".res")) {
   1134                 output_format = .coff;
   1135             } else if (options.preprocess == .only and (input_format.? == .rc or std.ascii.eqlIgnoreCase(input_ext, ".rc"))) {
   1136                 output_format = .rcpp;
   1137                 output_format_source = .inferred_from_preprocess_only;
   1138             } else {
   1139                 if (!std.ascii.eqlIgnoreCase(input_ext, ".res")) {
   1140                     output_format_source = .unable_to_infer_from_input_filename;
   1141                 }
   1142                 output_format = .res;
   1143             }
   1144         } else {
   1145             output_format_source = .output_format_arg;
   1146         }
   1147         options.output_source = .{ .filename = try filepathWithExtension(allocator, options.input_source.filename, output_format.?.extension()) };
   1148     } else {
   1149         options.output_source = .{ .filename = try allocator.dupe(u8, output_filename.?) };
   1150         if (output_format == null) {
   1151             output_format_source = .inferred_from_output_filename;
   1152             const ext = std.fs.path.extension(options.output_source.filename);
   1153             if (std.ascii.eqlIgnoreCase(ext, ".obj") or std.ascii.eqlIgnoreCase(ext, ".o")) {
   1154                 output_format = .coff;
   1155             } else if (std.ascii.eqlIgnoreCase(ext, ".rcpp")) {
   1156                 output_format = .rcpp;
   1157             } else {
   1158                 if (!std.ascii.eqlIgnoreCase(ext, ".res")) {
   1159                     output_format_source = .unable_to_infer_from_output_filename;
   1160                 }
   1161                 output_format = .res;
   1162             }
   1163         } else {
   1164             output_format_source = .output_format_arg;
   1165         }
   1166     }
   1167 
   1168     options.input_format = input_format.?;
   1169     options.output_format = output_format.?;
   1170 
   1171     // Check for incompatible options
   1172     var print_input_format_source_note: bool = false;
   1173     var print_output_format_source_note: bool = false;
   1174     if (options.depfile_path != null and (options.input_format == .res or options.output_format == .rcpp)) {
   1175         var err_details = Diagnostics.ErrorDetails{ .type = .warning, .arg_index = depfile_context.index, .arg_span = depfile_context.value.argSpan(depfile_context.arg) };
   1176         var msg_writer = err_details.msg.writer(allocator);
   1177         if (options.input_format == .res) {
   1178             try msg_writer.print("the {s}{s} option was ignored because the input format is '{s}'", .{
   1179                 depfile_context.arg.prefixSlice(),
   1180                 depfile_context.arg.optionWithoutPrefix(depfile_context.option_len),
   1181                 @tagName(options.input_format),
   1182             });
   1183             print_input_format_source_note = true;
   1184         } else if (options.output_format == .rcpp) {
   1185             try msg_writer.print("the {s}{s} option was ignored because the output format is '{s}'", .{
   1186                 depfile_context.arg.prefixSlice(),
   1187                 depfile_context.arg.optionWithoutPrefix(depfile_context.option_len),
   1188                 @tagName(options.output_format),
   1189             });
   1190             print_output_format_source_note = true;
   1191         }
   1192         try diagnostics.append(err_details);
   1193     }
   1194     if (!isSupportedTransformation(options.input_format, options.output_format)) {
   1195         var err_details = Diagnostics.ErrorDetails{ .arg_index = input_filename_arg_i, .print_args = false };
   1196         var msg_writer = err_details.msg.writer(allocator);
   1197         try msg_writer.print("input format '{s}' cannot be converted to output format '{s}'", .{ @tagName(options.input_format), @tagName(options.output_format) });
   1198         try diagnostics.append(err_details);
   1199         print_input_format_source_note = true;
   1200         print_output_format_source_note = true;
   1201     }
   1202     if (options.preprocess == .only and options.output_format != .rcpp) {
   1203         var err_details = Diagnostics.ErrorDetails{ .arg_index = preprocess_only_context.index };
   1204         var msg_writer = err_details.msg.writer(allocator);
   1205         try msg_writer.print("the {s}{s} option cannot be used with output format '{s}'", .{
   1206             preprocess_only_context.arg.prefixSlice(),
   1207             preprocess_only_context.arg.optionWithoutPrefix(preprocess_only_context.option_len),
   1208             @tagName(options.output_format),
   1209         });
   1210         try diagnostics.append(err_details);
   1211         print_output_format_source_note = true;
   1212     }
   1213     if (print_input_format_source_note) {
   1214         switch (input_format_source) {
   1215             .inferred_from_input_filename => {
   1216                 var err_details = Diagnostics.ErrorDetails{ .type = .note, .arg_index = input_filename_arg_i };
   1217                 var msg_writer = err_details.msg.writer(allocator);
   1218                 try msg_writer.writeAll("the input format was inferred from the input filename");
   1219                 try diagnostics.append(err_details);
   1220             },
   1221             .input_format_arg => {
   1222                 var err_details = Diagnostics.ErrorDetails{
   1223                     .type = .note,
   1224                     .arg_index = input_format_context.index,
   1225                     .arg_span = input_format_context.value.argSpan(input_format_context.arg),
   1226                 };
   1227                 var msg_writer = err_details.msg.writer(allocator);
   1228                 try msg_writer.writeAll("the input format was specified here");
   1229                 try diagnostics.append(err_details);
   1230             },
   1231         }
   1232     }
   1233     if (print_output_format_source_note) {
   1234         switch (output_format_source) {
   1235             .inferred_from_input_filename, .unable_to_infer_from_input_filename => {
   1236                 var err_details = Diagnostics.ErrorDetails{ .type = .note, .arg_index = input_filename_arg_i };
   1237                 var msg_writer = err_details.msg.writer(allocator);
   1238                 if (output_format_source == .inferred_from_input_filename) {
   1239                     try msg_writer.writeAll("the output format was inferred from the input filename");
   1240                 } else {
   1241                     try msg_writer.writeAll("the output format was unable to be inferred from the input filename, so the default was used");
   1242                 }
   1243                 try diagnostics.append(err_details);
   1244             },
   1245             .inferred_from_output_filename, .unable_to_infer_from_output_filename => {
   1246                 var err_details: Diagnostics.ErrorDetails = switch (output_filename_context) {
   1247                     .positional => |i| .{ .type = .note, .arg_index = i },
   1248                     .arg => |ctx| .{ .type = .note, .arg_index = ctx.index, .arg_span = ctx.value.argSpan(ctx.arg) },
   1249                     .unspecified => unreachable,
   1250                 };
   1251                 var msg_writer = err_details.msg.writer(allocator);
   1252                 if (output_format_source == .inferred_from_output_filename) {
   1253                     try msg_writer.writeAll("the output format was inferred from the output filename");
   1254                 } else {
   1255                     try msg_writer.writeAll("the output format was unable to be inferred from the output filename, so the default was used");
   1256                 }
   1257                 try diagnostics.append(err_details);
   1258             },
   1259             .output_format_arg => {
   1260                 var err_details = Diagnostics.ErrorDetails{
   1261                     .type = .note,
   1262                     .arg_index = output_format_context.index,
   1263                     .arg_span = output_format_context.value.argSpan(output_format_context.arg),
   1264                 };
   1265                 var msg_writer = err_details.msg.writer(allocator);
   1266                 try msg_writer.writeAll("the output format was specified here");
   1267                 try diagnostics.append(err_details);
   1268             },
   1269             .inferred_from_preprocess_only => {
   1270                 var err_details = Diagnostics.ErrorDetails{ .type = .note, .arg_index = preprocess_only_context.index };
   1271                 var msg_writer = err_details.msg.writer(allocator);
   1272                 try msg_writer.print("the output format was inferred from the usage of the {s}{s} option", .{
   1273                     preprocess_only_context.arg.prefixSlice(),
   1274                     preprocess_only_context.arg.optionWithoutPrefix(preprocess_only_context.option_len),
   1275                 });
   1276                 try diagnostics.append(err_details);
   1277             },
   1278         }
   1279     }
   1280 
   1281     if (diagnostics.hasError()) {
   1282         return error.ParseError;
   1283     }
   1284 
   1285     // Implied settings from input/output formats
   1286     if (options.output_format == .rcpp) options.preprocess = .only;
   1287     if (options.input_format == .res) options.output_format = .coff;
   1288     if (options.input_format == .rcpp) options.preprocess = .no;
   1289 
   1290     return options;
   1291 }
   1292 
   1293 pub fn filepathWithExtension(allocator: Allocator, path: []const u8, ext: []const u8) ![]const u8 {
   1294     var buf = std.array_list.Managed(u8).init(allocator);
   1295     errdefer buf.deinit();
   1296     if (std.fs.path.dirname(path)) |dirname| {
   1297         var end_pos = dirname.len;
   1298         // We want to ensure that we write a path separator at the end, so if the dirname
   1299         // doesn't end with a path sep then include the char after the dirname
   1300         // which must be a path sep.
   1301         if (!std.fs.path.isSep(dirname[dirname.len - 1])) end_pos += 1;
   1302         try buf.appendSlice(path[0..end_pos]);
   1303     }
   1304     try buf.appendSlice(std.fs.path.stem(path));
   1305     try buf.appendSlice(ext);
   1306     return try buf.toOwnedSlice();
   1307 }
   1308 
   1309 pub fn isSupportedInputExtension(ext: []const u8) bool {
   1310     if (std.ascii.eqlIgnoreCase(ext, ".rc")) return true;
   1311     if (std.ascii.eqlIgnoreCase(ext, ".res")) return true;
   1312     if (std.ascii.eqlIgnoreCase(ext, ".rcpp")) return true;
   1313     return false;
   1314 }
   1315 
   1316 pub fn isSupportedTransformation(input: Options.InputFormat, output: Options.OutputFormat) bool {
   1317     return switch (input) {
   1318         .rc => switch (output) {
   1319             .res => true,
   1320             .coff => true,
   1321             .rcpp => true,
   1322         },
   1323         .res => switch (output) {
   1324             .res => false,
   1325             .coff => true,
   1326             .rcpp => false,
   1327         },
   1328         .rcpp => switch (output) {
   1329             .res => true,
   1330             .coff => true,
   1331             .rcpp => false,
   1332         },
   1333     };
   1334 }
   1335 
   1336 /// Returns true if the str is a valid C identifier for use in a #define/#undef macro
   1337 pub fn isValidIdentifier(str: []const u8) bool {
   1338     for (str, 0..) |c, i| switch (c) {
   1339         '0'...'9' => if (i == 0) return false,
   1340         'a'...'z', 'A'...'Z', '_' => {},
   1341         else => return false,
   1342     };
   1343     return true;
   1344 }
   1345 
   1346 /// This function is specific to how the Win32 RC command line interprets
   1347 /// max string literal length percent.
   1348 /// - Wraps on overflow of u32
   1349 /// - Stops parsing on any invalid hexadecimal digits
   1350 /// - Errors if a digit is not the first char
   1351 /// - `-` (negative) prefix is allowed
   1352 pub fn parsePercent(str: []const u8) error{InvalidFormat}!u32 {
   1353     var result: u32 = 0;
   1354     const radix: u8 = 10;
   1355     var buf = str;
   1356 
   1357     const Prefix = enum { none, minus };
   1358     var prefix: Prefix = .none;
   1359     switch (buf[0]) {
   1360         '-' => {
   1361             prefix = .minus;
   1362             buf = buf[1..];
   1363         },
   1364         else => {},
   1365     }
   1366 
   1367     for (buf, 0..) |c, i| {
   1368         const digit = switch (c) {
   1369             // On invalid digit for the radix, just stop parsing but don't fail
   1370             '0'...'9' => std.fmt.charToDigit(c, radix) catch break,
   1371             else => {
   1372                 // First digit must be valid
   1373                 if (i == 0) {
   1374                     return error.InvalidFormat;
   1375                 }
   1376                 break;
   1377             },
   1378         };
   1379 
   1380         if (result != 0) {
   1381             result *%= radix;
   1382         }
   1383         result +%= digit;
   1384     }
   1385 
   1386     switch (prefix) {
   1387         .none => {},
   1388         .minus => result = 0 -% result,
   1389     }
   1390 
   1391     return result;
   1392 }
   1393 
   1394 test parsePercent {
   1395     try std.testing.expectEqual(@as(u32, 16), try parsePercent("16"));
   1396     try std.testing.expectEqual(@as(u32, 0), try parsePercent("0x1A"));
   1397     try std.testing.expectEqual(@as(u32, 0x1), try parsePercent("1zzzz"));
   1398     try std.testing.expectEqual(@as(u32, 0xffffffff), try parsePercent("-1"));
   1399     try std.testing.expectEqual(@as(u32, 0xfffffff0), try parsePercent("-16"));
   1400     try std.testing.expectEqual(@as(u32, 1), try parsePercent("4294967297"));
   1401     try std.testing.expectError(error.InvalidFormat, parsePercent("--1"));
   1402     try std.testing.expectError(error.InvalidFormat, parsePercent("ha"));
   1403     try std.testing.expectError(error.InvalidFormat, parsePercent("ยน"));
   1404     try std.testing.expectError(error.InvalidFormat, parsePercent("~1"));
   1405 }
   1406 
   1407 pub fn renderErrorMessage(writer: *std.io.Writer, config: std.io.tty.Config, err_details: Diagnostics.ErrorDetails, args: []const []const u8) !void {
   1408     try config.setColor(writer, .dim);
   1409     try writer.writeAll("<cli>");
   1410     try config.setColor(writer, .reset);
   1411     try config.setColor(writer, .bold);
   1412     try writer.writeAll(": ");
   1413     switch (err_details.type) {
   1414         .err => {
   1415             try config.setColor(writer, .red);
   1416             try writer.writeAll("error: ");
   1417         },
   1418         .warning => {
   1419             try config.setColor(writer, .yellow);
   1420             try writer.writeAll("warning: ");
   1421         },
   1422         .note => {
   1423             try config.setColor(writer, .cyan);
   1424             try writer.writeAll("note: ");
   1425         },
   1426     }
   1427     try config.setColor(writer, .reset);
   1428     try config.setColor(writer, .bold);
   1429     try writer.writeAll(err_details.msg.items);
   1430     try writer.writeByte('\n');
   1431     try config.setColor(writer, .reset);
   1432 
   1433     if (!err_details.print_args) {
   1434         try writer.writeByte('\n');
   1435         return;
   1436     }
   1437 
   1438     try config.setColor(writer, .dim);
   1439     const prefix = " ... ";
   1440     try writer.writeAll(prefix);
   1441     try config.setColor(writer, .reset);
   1442 
   1443     const arg_with_name = args[err_details.arg_index];
   1444     const prefix_slice = arg_with_name[0..err_details.arg_span.prefix_len];
   1445     const before_name_slice = arg_with_name[err_details.arg_span.prefix_len..err_details.arg_span.name_offset];
   1446     var name_slice = arg_with_name[err_details.arg_span.name_offset..];
   1447     if (err_details.arg_span.name_len > 0) name_slice.len = err_details.arg_span.name_len;
   1448     const after_name_slice = arg_with_name[err_details.arg_span.name_offset + name_slice.len ..];
   1449 
   1450     try writer.writeAll(prefix_slice);
   1451     if (before_name_slice.len > 0) {
   1452         try config.setColor(writer, .dim);
   1453         try writer.writeAll(before_name_slice);
   1454         try config.setColor(writer, .reset);
   1455     }
   1456     try writer.writeAll(name_slice);
   1457     if (after_name_slice.len > 0) {
   1458         try config.setColor(writer, .dim);
   1459         try writer.writeAll(after_name_slice);
   1460         try config.setColor(writer, .reset);
   1461     }
   1462 
   1463     var next_arg_len: usize = 0;
   1464     if (err_details.arg_span.point_at_next_arg and err_details.arg_index + 1 < args.len) {
   1465         const next_arg = args[err_details.arg_index + 1];
   1466         try writer.writeByte(' ');
   1467         try writer.writeAll(next_arg);
   1468         next_arg_len = next_arg.len;
   1469     }
   1470 
   1471     const last_shown_arg_index = if (err_details.arg_span.point_at_next_arg) err_details.arg_index + 1 else err_details.arg_index;
   1472     if (last_shown_arg_index + 1 < args.len) {
   1473         // special case for when pointing to a missing value within the same arg
   1474         // as the name
   1475         if (err_details.arg_span.value_offset >= arg_with_name.len) {
   1476             try writer.writeByte(' ');
   1477         }
   1478         try config.setColor(writer, .dim);
   1479         try writer.writeAll(" ...");
   1480         try config.setColor(writer, .reset);
   1481     }
   1482     try writer.writeByte('\n');
   1483 
   1484     try config.setColor(writer, .green);
   1485     try writer.splatByteAll(' ', prefix.len);
   1486     // Special case for when the option is *only* a prefix (e.g. invalid option: -)
   1487     if (err_details.arg_span.prefix_len == arg_with_name.len) {
   1488         try writer.splatByteAll('^', err_details.arg_span.prefix_len);
   1489     } else {
   1490         try writer.splatByteAll('~', err_details.arg_span.prefix_len);
   1491         try writer.splatByteAll(' ', err_details.arg_span.name_offset - err_details.arg_span.prefix_len);
   1492         if (!err_details.arg_span.point_at_next_arg and err_details.arg_span.value_offset == 0) {
   1493             try writer.writeByte('^');
   1494             try writer.splatByteAll('~', name_slice.len - 1);
   1495         } else if (err_details.arg_span.value_offset > 0) {
   1496             try writer.splatByteAll('~', err_details.arg_span.value_offset - err_details.arg_span.name_offset);
   1497             try writer.writeByte('^');
   1498             if (err_details.arg_span.value_offset < arg_with_name.len) {
   1499                 try writer.splatByteAll('~', arg_with_name.len - err_details.arg_span.value_offset - 1);
   1500             }
   1501         } else if (err_details.arg_span.point_at_next_arg) {
   1502             try writer.splatByteAll('~', arg_with_name.len - err_details.arg_span.name_offset + 1);
   1503             try writer.writeByte('^');
   1504             if (next_arg_len > 0) {
   1505                 try writer.splatByteAll('~', next_arg_len - 1);
   1506             }
   1507         }
   1508     }
   1509     try writer.writeByte('\n');
   1510     try config.setColor(writer, .reset);
   1511 }
   1512 
   1513 fn testParse(args: []const []const u8) !Options {
   1514     return (try testParseOutput(args, "")).?;
   1515 }
   1516 
   1517 fn testParseWarning(args: []const []const u8, expected_output: []const u8) !Options {
   1518     return (try testParseOutput(args, expected_output)).?;
   1519 }
   1520 
   1521 fn testParseError(args: []const []const u8, expected_output: []const u8) !void {
   1522     var maybe_options = try testParseOutput(args, expected_output);
   1523     if (maybe_options != null) {
   1524         std.debug.print("expected error, got options: {}\n", .{maybe_options.?});
   1525         maybe_options.?.deinit();
   1526         return error.TestExpectedError;
   1527     }
   1528 }
   1529 
   1530 fn testParseOutput(args: []const []const u8, expected_output: []const u8) !?Options {
   1531     var diagnostics = Diagnostics.init(std.testing.allocator);
   1532     defer diagnostics.deinit();
   1533 
   1534     var output: std.io.Writer.Allocating = .init(std.testing.allocator);
   1535     defer output.deinit();
   1536 
   1537     var options = parse(std.testing.allocator, args, &diagnostics) catch |err| switch (err) {
   1538         error.ParseError => {
   1539             try diagnostics.renderToWriter(args, &output.writer, .no_color);
   1540             try std.testing.expectEqualStrings(expected_output, output.getWritten());
   1541             return null;
   1542         },
   1543         else => |e| return e,
   1544     };
   1545     errdefer options.deinit();
   1546 
   1547     try diagnostics.renderToWriter(args, &output.writer, .no_color);
   1548     try std.testing.expectEqualStrings(expected_output, output.getWritten());
   1549     return options;
   1550 }
   1551 
   1552 test "parse errors: basic" {
   1553     try testParseError(&.{"/"},
   1554         \\<cli>: error: invalid option: /
   1555         \\ ... /
   1556         \\     ^
   1557         \\<cli>: error: missing input filename
   1558         \\
   1559         \\
   1560     );
   1561     try testParseError(&.{"/ln"},
   1562         \\<cli>: error: missing language tag after /ln option
   1563         \\ ... /ln
   1564         \\     ~~~~^
   1565         \\<cli>: error: missing input filename
   1566         \\
   1567         \\
   1568     );
   1569     try testParseError(&.{"-vln"},
   1570         \\<cli>: error: missing language tag after -ln option
   1571         \\ ... -vln
   1572         \\     ~ ~~~^
   1573         \\<cli>: error: missing input filename
   1574         \\
   1575         \\
   1576     );
   1577     try testParseError(&.{"/_not-an-option"},
   1578         \\<cli>: error: invalid option: /_not-an-option
   1579         \\ ... /_not-an-option
   1580         \\     ~^~~~~~~~~~~~~~
   1581         \\<cli>: error: missing input filename
   1582         \\
   1583         \\
   1584     );
   1585     try testParseError(&.{"-_not-an-option"},
   1586         \\<cli>: error: invalid option: -_not-an-option
   1587         \\ ... -_not-an-option
   1588         \\     ~^~~~~~~~~~~~~~
   1589         \\<cli>: error: missing input filename
   1590         \\
   1591         \\
   1592     );
   1593     try testParseError(&.{"--_not-an-option"},
   1594         \\<cli>: error: invalid option: --_not-an-option
   1595         \\ ... --_not-an-option
   1596         \\     ~~^~~~~~~~~~~~~~
   1597         \\<cli>: error: missing input filename
   1598         \\
   1599         \\
   1600     );
   1601     try testParseError(&.{"/v_not-an-option"},
   1602         \\<cli>: error: invalid option: /_not-an-option
   1603         \\ ... /v_not-an-option
   1604         \\     ~ ^~~~~~~~~~~~~~
   1605         \\<cli>: error: missing input filename
   1606         \\
   1607         \\
   1608     );
   1609     try testParseError(&.{"-v_not-an-option"},
   1610         \\<cli>: error: invalid option: -_not-an-option
   1611         \\ ... -v_not-an-option
   1612         \\     ~ ^~~~~~~~~~~~~~
   1613         \\<cli>: error: missing input filename
   1614         \\
   1615         \\
   1616     );
   1617     try testParseError(&.{"--v_not-an-option"},
   1618         \\<cli>: error: invalid option: --_not-an-option
   1619         \\ ... --v_not-an-option
   1620         \\     ~~ ^~~~~~~~~~~~~~
   1621         \\<cli>: error: missing input filename
   1622         \\
   1623         \\
   1624     );
   1625 }
   1626 
   1627 test "inferred absolute filepaths" {
   1628     {
   1629         var options = try testParseWarning(&.{ "/fo", "foo.res", "/home/absolute/path.rc" },
   1630             \\<cli>: note: this argument was inferred to be a filepath, so argument parsing was terminated
   1631             \\ ... /home/absolute/path.rc
   1632             \\     ^~~~~~~~~~~~~~~~~~~~~~
   1633             \\
   1634         );
   1635         defer options.deinit();
   1636     }
   1637     {
   1638         var options = try testParseWarning(&.{ "/home/absolute/path.rc", "foo.res" },
   1639             \\<cli>: note: this argument was inferred to be a filepath, so argument parsing was terminated
   1640             \\ ... /home/absolute/path.rc ...
   1641             \\     ^~~~~~~~~~~~~~~~~~~~~~
   1642             \\
   1643         );
   1644         defer options.deinit();
   1645     }
   1646     {
   1647         // Only the last two arguments are checked, so the /h is parsed as an option
   1648         var options = try testParse(&.{ "/home/absolute/path.rc", "foo.rc", "foo.res" });
   1649         defer options.deinit();
   1650 
   1651         try std.testing.expect(options.print_help_and_exit);
   1652     }
   1653     {
   1654         var options = try testParse(&.{ "/xvFO/some/absolute/path.res", "foo.rc" });
   1655         defer options.deinit();
   1656 
   1657         try std.testing.expectEqual(true, options.verbose);
   1658         try std.testing.expectEqual(true, options.ignore_include_env_var);
   1659         try std.testing.expectEqualStrings("foo.rc", options.input_source.filename);
   1660         try std.testing.expectEqualStrings("/some/absolute/path.res", options.output_source.filename);
   1661     }
   1662 }
   1663 
   1664 test "parse errors: /ln" {
   1665     try testParseError(&.{ "/ln", "invalid", "foo.rc" },
   1666         \\<cli>: error: invalid language tag: invalid
   1667         \\ ... /ln invalid ...
   1668         \\     ~~~~^~~~~~~
   1669         \\
   1670     );
   1671     try testParseError(&.{ "/lninvalid", "foo.rc" },
   1672         \\<cli>: error: invalid language tag: invalid
   1673         \\ ... /lninvalid ...
   1674         \\     ~~~^~~~~~~
   1675         \\
   1676     );
   1677 }
   1678 
   1679 test "parse: options" {
   1680     {
   1681         var options = try testParse(&.{ "/v", "foo.rc" });
   1682         defer options.deinit();
   1683 
   1684         try std.testing.expectEqual(true, options.verbose);
   1685         try std.testing.expectEqualStrings("foo.rc", options.input_source.filename);
   1686         try std.testing.expectEqualStrings("foo.res", options.output_source.filename);
   1687     }
   1688     {
   1689         var options = try testParse(&.{ "/vx", "foo.rc" });
   1690         defer options.deinit();
   1691 
   1692         try std.testing.expectEqual(true, options.verbose);
   1693         try std.testing.expectEqual(true, options.ignore_include_env_var);
   1694         try std.testing.expectEqualStrings("foo.rc", options.input_source.filename);
   1695         try std.testing.expectEqualStrings("foo.res", options.output_source.filename);
   1696     }
   1697     {
   1698         var options = try testParse(&.{ "/xv", "foo.rc" });
   1699         defer options.deinit();
   1700 
   1701         try std.testing.expectEqual(true, options.verbose);
   1702         try std.testing.expectEqual(true, options.ignore_include_env_var);
   1703         try std.testing.expectEqualStrings("foo.rc", options.input_source.filename);
   1704         try std.testing.expectEqualStrings("foo.res", options.output_source.filename);
   1705     }
   1706     {
   1707         var options = try testParse(&.{ "/xvFObar.res", "foo.rc" });
   1708         defer options.deinit();
   1709 
   1710         try std.testing.expectEqual(true, options.verbose);
   1711         try std.testing.expectEqual(true, options.ignore_include_env_var);
   1712         try std.testing.expectEqualStrings("foo.rc", options.input_source.filename);
   1713         try std.testing.expectEqualStrings("bar.res", options.output_source.filename);
   1714     }
   1715 }
   1716 
   1717 test "parse: define and undefine" {
   1718     {
   1719         var options = try testParse(&.{ "/dfoo", "foo.rc" });
   1720         defer options.deinit();
   1721 
   1722         const action = options.symbols.get("foo").?;
   1723         try std.testing.expectEqualStrings("1", action.define);
   1724     }
   1725     {
   1726         var options = try testParse(&.{ "/dfoo=bar", "/dfoo=baz", "foo.rc" });
   1727         defer options.deinit();
   1728 
   1729         const action = options.symbols.get("foo").?;
   1730         try std.testing.expectEqualStrings("baz", action.define);
   1731     }
   1732     {
   1733         var options = try testParse(&.{ "/ufoo", "foo.rc" });
   1734         defer options.deinit();
   1735 
   1736         const action = options.symbols.get("foo").?;
   1737         try std.testing.expectEqual(Options.SymbolAction.undefine, action);
   1738     }
   1739     {
   1740         // Once undefined, future defines are ignored
   1741         var options = try testParse(&.{ "/ufoo", "/dfoo", "foo.rc" });
   1742         defer options.deinit();
   1743 
   1744         const action = options.symbols.get("foo").?;
   1745         try std.testing.expectEqual(Options.SymbolAction.undefine, action);
   1746     }
   1747     {
   1748         // Undefined always takes precedence
   1749         var options = try testParse(&.{ "/dfoo", "/ufoo", "/dfoo", "foo.rc" });
   1750         defer options.deinit();
   1751 
   1752         const action = options.symbols.get("foo").?;
   1753         try std.testing.expectEqual(Options.SymbolAction.undefine, action);
   1754     }
   1755     {
   1756         // Warn + ignore invalid identifiers
   1757         var options = try testParseWarning(
   1758             &.{ "/dfoo bar", "/u", "0leadingdigit", "foo.rc" },
   1759             \\<cli>: warning: symbol "foo bar" is not a valid identifier and therefore cannot be defined
   1760             \\ ... /dfoo bar ...
   1761             \\     ~~^~~~~~~
   1762             \\<cli>: warning: symbol "0leadingdigit" is not a valid identifier and therefore cannot be undefined
   1763             \\ ... /u 0leadingdigit ...
   1764             \\     ~~~^~~~~~~~~~~~~
   1765             \\
   1766             ,
   1767         );
   1768         defer options.deinit();
   1769 
   1770         try std.testing.expectEqual(@as(usize, 0), options.symbols.count());
   1771     }
   1772 }
   1773 
   1774 test "parse: /sl" {
   1775     try testParseError(&.{ "/sl", "0", "foo.rc" },
   1776         \\<cli>: error: percent out of range: 0 (parsed from '0')
   1777         \\ ... /sl 0 ...
   1778         \\     ~~~~^
   1779         \\<cli>: note: string length percent must be an integer between 1 and 100 (inclusive)
   1780         \\
   1781         \\
   1782     );
   1783     try testParseError(&.{ "/sl", "abcd", "foo.rc" },
   1784         \\<cli>: error: invalid percent format 'abcd'
   1785         \\ ... /sl abcd ...
   1786         \\     ~~~~^~~~
   1787         \\<cli>: note: string length percent must be an integer between 1 and 100 (inclusive)
   1788         \\
   1789         \\
   1790     );
   1791     {
   1792         var options = try testParse(&.{"foo.rc"});
   1793         defer options.deinit();
   1794 
   1795         try std.testing.expectEqual(@as(u15, lex.default_max_string_literal_codepoints), options.max_string_literal_codepoints);
   1796     }
   1797     {
   1798         var options = try testParse(&.{ "/sl100", "foo.rc" });
   1799         defer options.deinit();
   1800 
   1801         try std.testing.expectEqual(@as(u15, max_string_literal_length_100_percent), options.max_string_literal_codepoints);
   1802     }
   1803     {
   1804         var options = try testParse(&.{ "-SL33", "foo.rc" });
   1805         defer options.deinit();
   1806 
   1807         try std.testing.expectEqual(@as(u15, 2703), options.max_string_literal_codepoints);
   1808     }
   1809     {
   1810         var options = try testParse(&.{ "/sl15", "foo.rc" });
   1811         defer options.deinit();
   1812 
   1813         try std.testing.expectEqual(@as(u15, 1228), options.max_string_literal_codepoints);
   1814     }
   1815 }
   1816 
   1817 test "parse: unsupported MUI-related options" {
   1818     try testParseError(&.{ "/q", "blah", "/g1", "-G2", "blah", "/fm", "blah", "/g", "blah", "foo.rc" },
   1819         \\<cli>: error: the /q option is unsupported
   1820         \\ ... /q ...
   1821         \\     ~^
   1822         \\<cli>: error: the /g1 option is unsupported
   1823         \\ ... /g1 ...
   1824         \\     ~^~
   1825         \\<cli>: error: the -G2 option is unsupported
   1826         \\ ... -G2 ...
   1827         \\     ~^~
   1828         \\<cli>: error: the /fm option is unsupported
   1829         \\ ... /fm ...
   1830         \\     ~^~
   1831         \\<cli>: error: the /g option is unsupported
   1832         \\ ... /g ...
   1833         \\     ~^
   1834         \\
   1835     );
   1836 }
   1837 
   1838 test "parse: unsupported LCX/LCE-related options" {
   1839     try testParseError(&.{ "/t", "/tp:", "/tp:blah", "/tm", "/tc", "/tw", "-TEti", "/ta", "/tn", "blah", "foo.rc" },
   1840         \\<cli>: error: the /t option is unsupported
   1841         \\ ... /t ...
   1842         \\     ~^
   1843         \\<cli>: error: missing value for /tp: option
   1844         \\ ... /tp:  ...
   1845         \\     ~~~~^
   1846         \\<cli>: error: the /tp: option is unsupported
   1847         \\ ... /tp: ...
   1848         \\     ~^~~
   1849         \\<cli>: error: the /tp: option is unsupported
   1850         \\ ... /tp:blah ...
   1851         \\     ~^~~~~~~
   1852         \\<cli>: error: the /tm option is unsupported
   1853         \\ ... /tm ...
   1854         \\     ~^~
   1855         \\<cli>: error: the /tc option is unsupported
   1856         \\ ... /tc ...
   1857         \\     ~^~
   1858         \\<cli>: error: the /tw option is unsupported
   1859         \\ ... /tw ...
   1860         \\     ~^~
   1861         \\<cli>: error: the -TE option is unsupported
   1862         \\ ... -TEti ...
   1863         \\     ~^~
   1864         \\<cli>: error: the -ti option is unsupported
   1865         \\ ... -TEti ...
   1866         \\     ~  ^~
   1867         \\<cli>: error: the /ta option is unsupported
   1868         \\ ... /ta ...
   1869         \\     ~^~
   1870         \\<cli>: error: the /tn option is unsupported
   1871         \\ ... /tn ...
   1872         \\     ~^~
   1873         \\
   1874     );
   1875 }
   1876 
   1877 test "parse: output filename specified twice" {
   1878     try testParseError(&.{ "/fo", "foo.res", "foo.rc", "foo.res" },
   1879         \\<cli>: error: output filename already specified
   1880         \\ ... foo.res
   1881         \\     ^~~~~~~
   1882         \\<cli>: note: output filename previously specified here
   1883         \\ ... /fo foo.res ...
   1884         \\     ~~~~^~~~~~~
   1885         \\
   1886     );
   1887 }
   1888 
   1889 test "parse: input and output formats" {
   1890     {
   1891         try testParseError(&.{ "/:output-format", "rcpp", "foo.res" },
   1892             \\<cli>: error: input format 'res' cannot be converted to output format 'rcpp'
   1893             \\
   1894             \\<cli>: note: the input format was inferred from the input filename
   1895             \\ ... foo.res
   1896             \\     ^~~~~~~
   1897             \\<cli>: note: the output format was specified here
   1898             \\ ... /:output-format rcpp ...
   1899             \\     ~~~~~~~~~~~~~~~~^~~~
   1900             \\
   1901         );
   1902     }
   1903     {
   1904         try testParseError(&.{ "foo.res", "foo.rcpp" },
   1905             \\<cli>: error: input format 'res' cannot be converted to output format 'rcpp'
   1906             \\
   1907             \\<cli>: note: the input format was inferred from the input filename
   1908             \\ ... foo.res ...
   1909             \\     ^~~~~~~
   1910             \\<cli>: note: the output format was inferred from the output filename
   1911             \\ ... foo.rcpp
   1912             \\     ^~~~~~~~
   1913             \\
   1914         );
   1915     }
   1916     {
   1917         try testParseError(&.{ "/:input-format", "res", "foo" },
   1918             \\<cli>: error: input format 'res' cannot be converted to output format 'res'
   1919             \\
   1920             \\<cli>: note: the input format was specified here
   1921             \\ ... /:input-format res ...
   1922             \\     ~~~~~~~~~~~~~~~^~~
   1923             \\<cli>: note: the output format was unable to be inferred from the input filename, so the default was used
   1924             \\ ... foo
   1925             \\     ^~~
   1926             \\
   1927         );
   1928     }
   1929     {
   1930         try testParseError(&.{ "/p", "/:input-format", "res", "foo" },
   1931             \\<cli>: error: input format 'res' cannot be converted to output format 'res'
   1932             \\
   1933             \\<cli>: error: the /p option cannot be used with output format 'res'
   1934             \\ ... /p ...
   1935             \\     ^~
   1936             \\<cli>: note: the input format was specified here
   1937             \\ ... /:input-format res ...
   1938             \\     ~~~~~~~~~~~~~~~^~~
   1939             \\<cli>: note: the output format was unable to be inferred from the input filename, so the default was used
   1940             \\ ... foo
   1941             \\     ^~~
   1942             \\
   1943         );
   1944     }
   1945     {
   1946         try testParseError(&.{ "/:output-format", "coff", "/p", "foo.rc" },
   1947             \\<cli>: error: the /p option cannot be used with output format 'coff'
   1948             \\ ... /p ...
   1949             \\     ^~
   1950             \\<cli>: note: the output format was specified here
   1951             \\ ... /:output-format coff ...
   1952             \\     ~~~~~~~~~~~~~~~~^~~~
   1953             \\
   1954         );
   1955     }
   1956     {
   1957         try testParseError(&.{ "/fo", "foo.res", "/p", "foo.rc" },
   1958             \\<cli>: error: the /p option cannot be used with output format 'res'
   1959             \\ ... /p ...
   1960             \\     ^~
   1961             \\<cli>: note: the output format was inferred from the output filename
   1962             \\ ... /fo foo.res ...
   1963             \\     ~~~~^~~~~~~
   1964             \\
   1965         );
   1966     }
   1967     {
   1968         try testParseError(&.{ "/p", "foo.rc", "foo.o" },
   1969             \\<cli>: error: the /p option cannot be used with output format 'coff'
   1970             \\ ... /p ...
   1971             \\     ^~
   1972             \\<cli>: note: the output format was inferred from the output filename
   1973             \\ ... foo.o
   1974             \\     ^~~~~
   1975             \\
   1976         );
   1977     }
   1978     {
   1979         var options = try testParse(&.{"foo.rc"});
   1980         defer options.deinit();
   1981 
   1982         try std.testing.expectEqual(.rc, options.input_format);
   1983         try std.testing.expectEqual(.res, options.output_format);
   1984     }
   1985     {
   1986         var options = try testParse(&.{"foo.rcpp"});
   1987         defer options.deinit();
   1988 
   1989         try std.testing.expectEqual(.no, options.preprocess);
   1990         try std.testing.expectEqual(.rcpp, options.input_format);
   1991         try std.testing.expectEqual(.res, options.output_format);
   1992     }
   1993     {
   1994         var options = try testParse(&.{ "foo.rc", "foo.rcpp" });
   1995         defer options.deinit();
   1996 
   1997         try std.testing.expectEqual(.only, options.preprocess);
   1998         try std.testing.expectEqual(.rc, options.input_format);
   1999         try std.testing.expectEqual(.rcpp, options.output_format);
   2000     }
   2001     {
   2002         var options = try testParse(&.{ "foo.rc", "foo.obj" });
   2003         defer options.deinit();
   2004 
   2005         try std.testing.expectEqual(.rc, options.input_format);
   2006         try std.testing.expectEqual(.coff, options.output_format);
   2007     }
   2008     {
   2009         var options = try testParse(&.{ "/fo", "foo.o", "foo.rc" });
   2010         defer options.deinit();
   2011 
   2012         try std.testing.expectEqual(.rc, options.input_format);
   2013         try std.testing.expectEqual(.coff, options.output_format);
   2014     }
   2015     {
   2016         var options = try testParse(&.{"foo.res"});
   2017         defer options.deinit();
   2018 
   2019         try std.testing.expectEqual(.res, options.input_format);
   2020         try std.testing.expectEqual(.coff, options.output_format);
   2021     }
   2022     {
   2023         var options = try testParseWarning(&.{ "/:depfile", "foo.json", "foo.rc", "foo.rcpp" },
   2024             \\<cli>: warning: the /:depfile option was ignored because the output format is 'rcpp'
   2025             \\ ... /:depfile foo.json ...
   2026             \\     ~~~~~~~~~~^~~~~~~~
   2027             \\<cli>: note: the output format was inferred from the output filename
   2028             \\ ... foo.rcpp
   2029             \\     ^~~~~~~~
   2030             \\
   2031         );
   2032         defer options.deinit();
   2033 
   2034         try std.testing.expectEqual(.rc, options.input_format);
   2035         try std.testing.expectEqual(.rcpp, options.output_format);
   2036     }
   2037     {
   2038         var options = try testParseWarning(&.{ "/:depfile", "foo.json", "foo.res", "foo.o" },
   2039             \\<cli>: warning: the /:depfile option was ignored because the input format is 'res'
   2040             \\ ... /:depfile foo.json ...
   2041             \\     ~~~~~~~~~~^~~~~~~~
   2042             \\<cli>: note: the input format was inferred from the input filename
   2043             \\ ... foo.res ...
   2044             \\     ^~~~~~~
   2045             \\
   2046         );
   2047         defer options.deinit();
   2048 
   2049         try std.testing.expectEqual(.res, options.input_format);
   2050         try std.testing.expectEqual(.coff, options.output_format);
   2051     }
   2052 }
   2053 
   2054 test "maybeAppendRC" {
   2055     var tmp = std.testing.tmpDir(.{});
   2056     defer tmp.cleanup();
   2057 
   2058     var options = try testParse(&.{"foo"});
   2059     defer options.deinit();
   2060     try std.testing.expectEqualStrings("foo", options.input_source.filename);
   2061 
   2062     // Create the file so that it's found. In this scenario, .rc should not get
   2063     // appended.
   2064     var file = try tmp.dir.createFile("foo", .{});
   2065     file.close();
   2066     try options.maybeAppendRC(tmp.dir);
   2067     try std.testing.expectEqualStrings("foo", options.input_source.filename);
   2068 
   2069     // Now delete the file and try again. But this time change the input format
   2070     // to non-rc.
   2071     try tmp.dir.deleteFile("foo");
   2072     options.input_format = .res;
   2073     try options.maybeAppendRC(tmp.dir);
   2074     try std.testing.expectEqualStrings("foo", options.input_source.filename);
   2075 
   2076     // Finally, reset the input format to rc. Since the verbatim name is no longer found
   2077     // and the input filename does not have an extension, .rc should get appended.
   2078     options.input_format = .rc;
   2079     try options.maybeAppendRC(tmp.dir);
   2080     try std.testing.expectEqualStrings("foo.rc", options.input_source.filename);
   2081 }