Zig 0.17.0-dev (Split by item)

This is an example of documentation generated by ZigDoc, an alternative to Zig's built-in Auto Doc feature. See also examples in other modes/formats. The project being documented here (as the example) is the Zig library itself.

parse

Note: Does not run Options.maybeAppendRC automatically. If that behavior is desired, it must be called separately.

cli.parse
pub fn parse(allocator: Allocator, io: Io, args: []const []const u8, diagnostics: *Diagnostics) ParseError!Options

File

Code

pub fn parse(allocator: Allocator, io: Io, args: []const []const u8, diagnostics: *Diagnostics) ParseError!Options {
    var options = Options{ .allocator = allocator };
    errdefer options.deinit();

    var output_filename: ?[]const u8 = null;
    var output_filename_context: union(enum) {
        unspecified: void,
        positional: usize,
        arg: Arg.Context,
    } = .{ .unspecified = {} };
    var output_format: ?Options.OutputFormat = null;
    var output_format_context: Arg.Context = undefined;
    var input_format: ?Options.InputFormat = null;
    var input_format_context: Arg.Context = undefined;
    var input_filename_arg_i: usize = undefined;
    var preprocess_only_context: Arg.Context = undefined;
    var depfile_context: Arg.Context = undefined;

    var arg_i: usize = 0;
    next_arg: while (arg_i < args.len) {
        var arg = Arg.fromString(args[arg_i]) orelse break;
        if (arg.name().len == 0) {
            switch (arg.prefix) {
                // -- on its own ends arg parsing
                .long => {
                    arg_i += 1;
                    break;
                },
                // - or / on its own is an error
                else => {
                    var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.optionAndAfterSpan() };
                    try err_details.msg.print(allocator, "invalid option: {s}", .{arg.prefixSlice()});
                    try diagnostics.append(err_details);
                    arg_i += 1;
                    continue :next_arg;
                },
            }
        }

        const args_remaining = args.len - arg_i;
        if (args_remaining <= 2 and arg.looksLikeFilepath(io)) {
            var err_details = Diagnostics.ErrorDetails{ .type = .note, .print_args = true, .arg_index = arg_i };
            try err_details.msg.appendSlice(allocator, "this argument was inferred to be a filepath, so argument parsing was terminated");
            try diagnostics.append(err_details);

            break;
        }

        while (arg.name().len > 0) {
            const arg_name = arg.name();
            // Note: These cases should be in order from longest to shortest, since
            //       shorter options that are a substring of a longer one could make
            //       the longer option's branch unreachable.
            if (std.ascii.startsWithIgnoreCase(arg_name, ":no-preprocess")) {
                options.preprocess = .no;
                arg.name_offset += ":no-preprocess".len;
            } else if (std.ascii.startsWithIgnoreCase(arg_name, ":output-format")) {
                const value = arg.value(":output-format".len, arg_i, args) catch {
                    var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
                    try err_details.msg.print(allocator, "missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(":output-format".len) });
                    try diagnostics.append(err_details);
                    arg_i += 1;
                    break :next_arg;
                };
                output_format = std.meta.stringToEnum(Options.OutputFormat, value.slice) orelse blk: {
                    var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
                    try err_details.msg.print(allocator, "invalid output format setting: {s} ", .{value.slice});
                    try diagnostics.append(err_details);
                    break :blk output_format;
                };
                output_format_context = .{ .index = arg_i, .option_len = ":output-format".len, .arg = arg, .value = value };
                arg_i += value.index_increment;
                continue :next_arg;
            } else if (std.ascii.startsWithIgnoreCase(arg_name, ":auto-includes")) {
                const value = arg.value(":auto-includes".len, arg_i, args) catch {
                    var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
                    try err_details.msg.print(allocator, "missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(":auto-includes".len) });
                    try diagnostics.append(err_details);
                    arg_i += 1;
                    break :next_arg;
                };
                options.auto_includes = std.meta.stringToEnum(Options.AutoIncludes, value.slice) orelse blk: {
                    var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
                    try err_details.msg.print(allocator, "invalid auto includes setting: {s} ", .{value.slice});
                    try diagnostics.append(err_details);
                    break :blk options.auto_includes;
                };
                arg_i += value.index_increment;
                continue :next_arg;
            } else if (std.ascii.startsWithIgnoreCase(arg_name, ":input-format")) {
                const value = arg.value(":input-format".len, arg_i, args) catch {
                    var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
                    try err_details.msg.print(allocator, "missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(":input-format".len) });
                    try diagnostics.append(err_details);
                    arg_i += 1;
                    break :next_arg;
                };
                input_format = std.meta.stringToEnum(Options.InputFormat, value.slice) orelse blk: {
                    var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
                    try err_details.msg.print(allocator, "invalid input format setting: {s} ", .{value.slice});
                    try diagnostics.append(err_details);
                    break :blk input_format;
                };
                input_format_context = .{ .index = arg_i, .option_len = ":input-format".len, .arg = arg, .value = value };
                arg_i += value.index_increment;
                continue :next_arg;
            } else if (std.ascii.startsWithIgnoreCase(arg_name, ":depfile-fmt")) {
                const value = arg.value(":depfile-fmt".len, arg_i, args) catch {
                    var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
                    try err_details.msg.print(allocator, "missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(":depfile-fmt".len) });
                    try diagnostics.append(err_details);
                    arg_i += 1;
                    break :next_arg;
                };
                options.depfile_fmt = std.meta.stringToEnum(Options.DepfileFormat, value.slice) orelse blk: {
                    var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
                    try err_details.msg.print(allocator, "invalid depfile format setting: {s} ", .{value.slice});
                    try diagnostics.append(err_details);
                    break :blk options.depfile_fmt;
                };
                arg_i += value.index_increment;
                continue :next_arg;
            } else if (std.ascii.startsWithIgnoreCase(arg_name, ":depfile")) {
                const value = arg.value(":depfile".len, arg_i, args) catch {
                    var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
                    try err_details.msg.print(allocator, "missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(":depfile".len) });
                    try diagnostics.append(err_details);
                    arg_i += 1;
                    break :next_arg;
                };
                if (options.depfile_path) |overwritten_path| {
                    allocator.free(overwritten_path);
                    options.depfile_path = null;
                }
                const path = try allocator.dupe(u8, value.slice);
                errdefer allocator.free(path);
                options.depfile_path = path;
                depfile_context = .{ .index = arg_i, .option_len = ":depfile".len, .arg = arg, .value = value };
                arg_i += value.index_increment;
                continue :next_arg;
            } else if (std.ascii.startsWithIgnoreCase(arg_name, ":target")) {
                const value = arg.value(":target".len, arg_i, args) catch {
                    var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
                    try err_details.msg.print(allocator, "missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(":target".len) });
                    try diagnostics.append(err_details);
                    arg_i += 1;
                    break :next_arg;
                };
                // Take the substring up to the first dash so that a full target triple
                // can be used, e.g. x86_64-windows-gnu becomes x86_64
                var target_it = std.mem.splitScalar(u8, value.slice, '-');
                const arch_str = target_it.first();
                const arch = cvtres.supported_targets.Arch.fromStringIgnoreCase(arch_str) orelse {
                    var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
                    try err_details.msg.print(allocator, "invalid or unsupported target architecture: {s}", .{arch_str});
                    try diagnostics.append(err_details);
                    arg_i += value.index_increment;
                    continue :next_arg;
                };
                options.coff_options.target = arch.toCoffMachineType();
                arg_i += value.index_increment;
                continue :next_arg;
            } else if (std.ascii.startsWithIgnoreCase(arg_name, "nologo")) {
                // No-op, we don't display any 'logo' to suppress
                arg.name_offset += "nologo".len;
            } else if (std.ascii.startsWithIgnoreCase(arg_name, ":debug")) {
                options.debug = true;
                arg.name_offset += ":debug".len;
            }
            // Unsupported LCX/LCE options that need a value (within the same arg only)
            else if (std.ascii.startsWithIgnoreCase(arg_name, "tp:")) {
                const rest = arg.full[arg.name_offset + 3 ..];
                if (rest.len == 0) {
                    var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = .{
                        .name_offset = arg.name_offset,
                        .prefix_len = arg.prefixSlice().len,
                        .value_offset = arg.name_offset + 3,
                    } };
                    try err_details.msg.print(allocator, "missing value for {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(3) });
                    try diagnostics.append(err_details);
                }
                var err_details = Diagnostics.ErrorDetails{ .type = .err, .arg_index = arg_i, .arg_span = arg.optionAndAfterSpan() };
                try err_details.msg.print(allocator, "the {s}{s} option is unsupported", .{ arg.prefixSlice(), arg.optionWithoutPrefix(3) });
                try diagnostics.append(err_details);
                arg_i += 1;
                continue :next_arg;
            }
            // Unsupported LCX/LCE options that need a value
            else if (std.ascii.startsWithIgnoreCase(arg_name, "tn")) {
                const value = arg.value(2, arg_i, args) catch no_value: {
                    var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
                    try err_details.msg.print(allocator, "missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) });
                    try diagnostics.append(err_details);
                    // dummy zero-length slice starting where the value would have been
                    const value_start = arg.name_offset + 2;
                    break :no_value Arg.Value{ .slice = arg.full[value_start..value_start] };
                };
                var err_details = Diagnostics.ErrorDetails{ .type = .err, .arg_index = arg_i, .arg_span = arg.optionAndAfterSpan() };
                try err_details.msg.print(allocator, "the {s}{s} option is unsupported", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) });
                try diagnostics.append(err_details);
                arg_i += value.index_increment;
                continue :next_arg;
            }
            // Unsupported MUI options that need a value
            else if (std.ascii.startsWithIgnoreCase(arg_name, "fm") or
                std.ascii.startsWithIgnoreCase(arg_name, "gn") or
                std.ascii.startsWithIgnoreCase(arg_name, "g2"))
            {
                const value = arg.value(2, arg_i, args) catch no_value: {
                    var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
                    try err_details.msg.print(allocator, "missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) });
                    try diagnostics.append(err_details);
                    // dummy zero-length slice starting where the value would have been
                    const value_start = arg.name_offset + 2;
                    break :no_value Arg.Value{ .slice = arg.full[value_start..value_start] };
                };
                var err_details = Diagnostics.ErrorDetails{ .type = .err, .arg_index = arg_i, .arg_span = arg.optionAndAfterSpan() };
                try err_details.msg.print(allocator, "the {s}{s} option is unsupported", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) });
                try diagnostics.append(err_details);
                arg_i += value.index_increment;
                continue :next_arg;
            }
            // Unsupported MUI options that do not need a value
            else if (std.ascii.startsWithIgnoreCase(arg_name, "g1")) {
                var err_details = Diagnostics.ErrorDetails{ .type = .err, .arg_index = arg_i, .arg_span = arg.optionSpan(2) };
                try err_details.msg.print(allocator, "the {s}{s} option is unsupported", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) });
                try diagnostics.append(err_details);
                arg.name_offset += 2;
            }
            // Unsupported LCX/LCE options that do not need a value
            else if (std.ascii.startsWithIgnoreCase(arg_name, "tm") or
                std.ascii.startsWithIgnoreCase(arg_name, "tc") or
                std.ascii.startsWithIgnoreCase(arg_name, "tw") or
                std.ascii.startsWithIgnoreCase(arg_name, "te") or
                std.ascii.startsWithIgnoreCase(arg_name, "ti") or
                std.ascii.startsWithIgnoreCase(arg_name, "ta"))
            {
                var err_details = Diagnostics.ErrorDetails{ .type = .err, .arg_index = arg_i, .arg_span = arg.optionSpan(2) };
                try err_details.msg.print(allocator, "the {s}{s} option is unsupported", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) });
                try diagnostics.append(err_details);
                arg.name_offset += 2;
            } else if (std.ascii.startsWithIgnoreCase(arg_name, "fo")) {
                const value = arg.value(2, arg_i, args) catch {
                    var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
                    try err_details.msg.print(allocator, "missing output path after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) });
                    try diagnostics.append(err_details);
                    arg_i += 1;
                    break :next_arg;
                };
                output_filename_context = .{ .arg = .{ .index = arg_i, .option_len = "fo".len, .arg = arg, .value = value } };
                output_filename = value.slice;
                arg_i += value.index_increment;
                continue :next_arg;
            } else if (std.ascii.startsWithIgnoreCase(arg_name, "sl")) {
                const value = arg.value(2, arg_i, args) catch {
                    var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
                    try err_details.msg.print(allocator, "missing language tag after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) });
                    try diagnostics.append(err_details);
                    arg_i += 1;
                    break :next_arg;
                };
                const percent_str = value.slice;
                const percent: u32 = parsePercent(percent_str) catch {
                    var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
                    try err_details.msg.print(allocator, "invalid percent format '{s}'", .{percent_str});
                    try diagnostics.append(err_details);
                    var note_details = Diagnostics.ErrorDetails{ .type = .note, .print_args = false, .arg_index = arg_i };
                    try note_details.msg.appendSlice(allocator, "string length percent must be an integer between 1 and 100 (inclusive)");
                    try diagnostics.append(note_details);
                    arg_i += value.index_increment;
                    continue :next_arg;
                };
                if (percent == 0 or percent > 100) {
                    var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
                    try err_details.msg.print(allocator, "percent out of range: {} (parsed from '{s}')", .{ percent, percent_str });
                    try diagnostics.append(err_details);
                    var note_details = Diagnostics.ErrorDetails{ .type = .note, .print_args = false, .arg_index = arg_i };
                    try note_details.msg.appendSlice(allocator, "string length percent must be an integer between 1 and 100 (inclusive)");
                    try diagnostics.append(note_details);
                    arg_i += value.index_increment;
                    continue :next_arg;
                }
                const percent_float = @as(f32, @floatFromInt(percent)) / 100;
                options.max_string_literal_codepoints = @intFromFloat(percent_float * max_string_literal_length_100_percent);
                arg_i += value.index_increment;
                continue :next_arg;
            } else if (std.ascii.startsWithIgnoreCase(arg_name, "ln")) {
                const value = arg.value(2, arg_i, args) catch {
                    var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
                    try err_details.msg.print(allocator, "missing language tag after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) });
                    try diagnostics.append(err_details);
                    arg_i += 1;
                    break :next_arg;
                };
                const tag = value.slice;
                options.default_language_id = lang.tagToInt(tag) catch {
                    var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
                    try err_details.msg.print(allocator, "invalid language tag: {s}", .{tag});
                    try diagnostics.append(err_details);
                    arg_i += value.index_increment;
                    continue :next_arg;
                };
                if (options.default_language_id.? == lang.LOCALE_CUSTOM_UNSPECIFIED) {
                    var err_details = Diagnostics.ErrorDetails{ .type = .warning, .arg_index = arg_i, .arg_span = value.argSpan(arg) };
                    try err_details.msg.print(allocator, "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 });
                    try diagnostics.append(err_details);
                }
                arg_i += value.index_increment;
                continue :next_arg;
            } else if (std.ascii.startsWithIgnoreCase(arg_name, "l")) {
                const value = arg.value(1, arg_i, args) catch {
                    var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
                    try err_details.msg.print(allocator, "missing language ID after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
                    try diagnostics.append(err_details);
                    arg_i += 1;
                    break :next_arg;
                };
                const num_str = value.slice;
                options.default_language_id = lang.parseInt(num_str) catch {
                    var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
                    try err_details.msg.print(allocator, "invalid language ID: {s}", .{num_str});
                    try diagnostics.append(err_details);
                    arg_i += value.index_increment;
                    continue :next_arg;
                };
                arg_i += value.index_increment;
                continue :next_arg;
            } else if (std.ascii.startsWithIgnoreCase(arg_name, "h") or std.mem.startsWith(u8, arg_name, "?")) {
                options.print_help_and_exit = true;
                // If there's been an error to this point, then we still want to fail
                if (diagnostics.hasError()) return error.ParseError;
                return options;
            }
            // 1 char unsupported MUI options that need a value
            else if (std.ascii.startsWithIgnoreCase(arg_name, "q") or
                std.ascii.startsWithIgnoreCase(arg_name, "g"))
            {
                const value = arg.value(1, arg_i, args) catch no_value: {
                    var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
                    try err_details.msg.print(allocator, "missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
                    try diagnostics.append(err_details);
                    // dummy zero-length slice starting where the value would have been
                    const value_start = arg.name_offset + 1;
                    break :no_value Arg.Value{ .slice = arg.full[value_start..value_start] };
                };
                var err_details = Diagnostics.ErrorDetails{ .type = .err, .arg_index = arg_i, .arg_span = arg.optionAndAfterSpan() };
                try err_details.msg.print(allocator, "the {s}{s} option is unsupported", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
                try diagnostics.append(err_details);
                arg_i += value.index_increment;
                continue :next_arg;
            }
            // Undocumented (and unsupported) options that need a value
            //  /z has to do something with font substitution
            //  /s has something to do with HWB resources being inserted into the .res
            else if (std.ascii.startsWithIgnoreCase(arg_name, "z") or
                std.ascii.startsWithIgnoreCase(arg_name, "s"))
            {
                const value = arg.value(1, arg_i, args) catch no_value: {
                    var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
                    try err_details.msg.print(allocator, "missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
                    try diagnostics.append(err_details);
                    // dummy zero-length slice starting where the value would have been
                    const value_start = arg.name_offset + 1;
                    break :no_value Arg.Value{ .slice = arg.full[value_start..value_start] };
                };
                var err_details = Diagnostics.ErrorDetails{ .type = .err, .arg_index = arg_i, .arg_span = arg.optionAndAfterSpan() };
                try err_details.msg.print(allocator, "the {s}{s} option is unsupported", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
                try diagnostics.append(err_details);
                arg_i += value.index_increment;
                continue :next_arg;
            }
            // 1 char unsupported LCX/LCE options that do not need a value
            else if (std.ascii.startsWithIgnoreCase(arg_name, "t")) {
                var err_details = Diagnostics.ErrorDetails{ .type = .err, .arg_index = arg_i, .arg_span = arg.optionSpan(1) };
                try err_details.msg.print(allocator, "the {s}{s} option is unsupported", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
                try diagnostics.append(err_details);
                arg.name_offset += 1;
            } else if (std.ascii.startsWithIgnoreCase(arg_name, "c")) {
                const value = arg.value(1, arg_i, args) catch {
                    var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
                    try err_details.msg.print(allocator, "missing code page ID after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
                    try diagnostics.append(err_details);
                    arg_i += 1;
                    break :next_arg;
                };
                const num_str = value.slice;
                const code_page_id = std.fmt.parseUnsigned(u16, num_str, 10) catch {
                    var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
                    try err_details.msg.print(allocator, "invalid code page ID: {s}", .{num_str});
                    try diagnostics.append(err_details);
                    arg_i += value.index_increment;
                    continue :next_arg;
                };
                options.default_code_page = code_pages.getByIdentifierEnsureSupported(code_page_id) catch |err| switch (err) {
                    error.InvalidCodePage => {
                        var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
                        try err_details.msg.print(allocator, "invalid or unknown code page ID: {}", .{code_page_id});
                        try diagnostics.append(err_details);
                        arg_i += value.index_increment;
                        continue :next_arg;
                    },
                    error.UnsupportedCodePage => {
                        var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
                        try err_details.msg.print(allocator, "unsupported code page: {s} (id={})", .{
                            @tagName(code_pages.getByIdentifier(code_page_id) catch unreachable),
                            code_page_id,
                        });
                        try diagnostics.append(err_details);
                        arg_i += value.index_increment;
                        continue :next_arg;
                    },
                };
                arg_i += value.index_increment;
                continue :next_arg;
            } else if (std.ascii.startsWithIgnoreCase(arg_name, "v")) {
                options.verbose = true;
                arg.name_offset += 1;
            } else if (std.ascii.startsWithIgnoreCase(arg_name, "x")) {
                options.ignore_include_env_var = true;
                arg.name_offset += 1;
            } else if (std.ascii.startsWithIgnoreCase(arg_name, "p")) {
                options.preprocess = .only;
                preprocess_only_context = .{ .index = arg_i, .option_len = "p".len, .arg = arg, .value = undefined };
                arg.name_offset += 1;
            } else if (std.ascii.startsWithIgnoreCase(arg_name, "i")) {
                const value = arg.value(1, arg_i, args) catch {
                    var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
                    try err_details.msg.print(allocator, "missing include path after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
                    try diagnostics.append(err_details);
                    arg_i += 1;
                    break :next_arg;
                };
                const path = value.slice;
                const duped = try allocator.dupe(u8, path);
                errdefer allocator.free(duped);
                try options.extra_include_paths.append(options.allocator, duped);
                arg_i += value.index_increment;
                continue :next_arg;
            } else if (std.ascii.startsWithIgnoreCase(arg_name, "r")) {
                // From https://learn.microsoft.com/en-us/windows/win32/menurc/using-rc-the-rc-command-line-
                // "Ignored. Provided for compatibility with existing makefiles."
                arg.name_offset += 1;
            } else if (std.ascii.startsWithIgnoreCase(arg_name, "n")) {
                options.null_terminate_string_table_strings = true;
                arg.name_offset += 1;
            } else if (std.ascii.startsWithIgnoreCase(arg_name, "y")) {
                options.silent_duplicate_control_ids = true;
                arg.name_offset += 1;
            } else if (std.ascii.startsWithIgnoreCase(arg_name, "w")) {
                options.warn_instead_of_error_on_invalid_code_page = true;
                arg.name_offset += 1;
            } else if (std.ascii.startsWithIgnoreCase(arg_name, "a")) {
                // Undocumented option with unknown function
                // TODO: More investigation to figure out what it does (if anything)
                var err_details = Diagnostics.ErrorDetails{ .type = .warning, .arg_index = arg_i, .arg_span = arg.optionSpan(1) };
                try err_details.msg.print(allocator, "option {s}{s} has no effect (it is undocumented and its function is unknown in the Win32 RC compiler)", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
                try diagnostics.append(err_details);
                arg.name_offset += 1;
            } else if (std.ascii.startsWithIgnoreCase(arg_name, "d")) {
                const value = arg.value(1, arg_i, args) catch {
                    var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
                    try err_details.msg.print(allocator, "missing symbol to define after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
                    try diagnostics.append(err_details);
                    arg_i += 1;
                    break :next_arg;
                };
                var tokenizer = std.mem.tokenizeScalar(u8, value.slice, '=');
                // guaranteed to exist since an empty value.slice would invoke
                // the 'missing symbol to define' branch above
                const symbol = tokenizer.next().?;
                const symbol_value = tokenizer.next() orelse "1";

                if (isValidIdentifier(symbol)) {
                    try options.define(symbol, symbol_value);
                } else {
                    var err_details = Diagnostics.ErrorDetails{ .type = .warning, .arg_index = arg_i, .arg_span = value.argSpan(arg) };
                    try err_details.msg.print(allocator, "symbol \"{s}\" is not a valid identifier and therefore cannot be defined", .{symbol});
                    try diagnostics.append(err_details);
                }
                arg_i += value.index_increment;
                continue :next_arg;
            } else if (std.ascii.startsWithIgnoreCase(arg_name, "u")) {
                const value = arg.value(1, arg_i, args) catch {
                    var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
                    try err_details.msg.print(allocator, "missing symbol to undefine after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
                    try diagnostics.append(err_details);
                    arg_i += 1;
                    break :next_arg;
                };
                const symbol = value.slice;
                if (isValidIdentifier(symbol)) {
                    try options.undefine(symbol);
                } else {
                    var err_details = Diagnostics.ErrorDetails{ .type = .warning, .arg_index = arg_i, .arg_span = value.argSpan(arg) };
                    try err_details.msg.print(allocator, "symbol \"{s}\" is not a valid identifier and therefore cannot be undefined", .{symbol});
                    try diagnostics.append(err_details);
                }
                arg_i += value.index_increment;
                continue :next_arg;
            } else {
                var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.optionAndAfterSpan() };
                try err_details.msg.print(allocator, "invalid option: {s}{s}", .{ arg.prefixSlice(), arg.name() });
                try diagnostics.append(err_details);
                arg_i += 1;
                continue :next_arg;
            }
        } else {
            // The while loop exited via its conditional, meaning we are done with
            // the current arg and can move on the the next
            arg_i += 1;
            continue;
        }
    }

    const positionals = args[arg_i..];

    if (positionals.len == 0) {
        var err_details = Diagnostics.ErrorDetails{ .print_args = false, .arg_index = arg_i };
        try err_details.msg.appendSlice(allocator, "missing input filename");
        try diagnostics.append(err_details);

        if (args.len > 0) {
            const last_arg = args[args.len - 1];
            if (arg_i > 0 and last_arg.len > 0 and last_arg[0] == '/' and isSupportedInputExtension(std.fs.path.extension(last_arg))) {
                var note_details = Diagnostics.ErrorDetails{ .type = .note, .print_args = true, .arg_index = arg_i - 1 };
                try note_details.msg.appendSlice(allocator, "if this argument was intended to be the input filename, adding -- in front of it will exclude it from option parsing");
                try diagnostics.append(note_details);
            }
        }

        // This is a fatal enough problem to justify an early return, since
        // things after this rely on the value of the input filename.
        return error.ParseError;
    }
    options.input_source = .{ .filename = try allocator.dupe(u8, positionals[0]) };
    input_filename_arg_i = arg_i;

    const InputFormatSource = enum {
        inferred_from_input_filename,
        input_format_arg,
    };

    var input_format_source: InputFormatSource = undefined;
    if (input_format == null) {
        const ext = std.fs.path.extension(options.input_source.filename);
        if (std.ascii.eqlIgnoreCase(ext, ".res")) {
            input_format = .res;
        } else if (std.ascii.eqlIgnoreCase(ext, ".rcpp")) {
            input_format = .rcpp;
        } else {
            input_format = .rc;
        }
        input_format_source = .inferred_from_input_filename;
    } else {
        input_format_source = .input_format_arg;
    }

    if (positionals.len > 1) {
        if (output_filename != null) {
            var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i + 1 };
            try err_details.msg.appendSlice(allocator, "output filename already specified");
            try diagnostics.append(err_details);
            var note_details = Diagnostics.ErrorDetails{
                .type = .note,
                .arg_index = output_filename_context.arg.index,
                .arg_span = output_filename_context.arg.value.argSpan(output_filename_context.arg.arg),
            };
            try note_details.msg.appendSlice(allocator, "output filename previously specified here");
            try diagnostics.append(note_details);
        } else {
            output_filename = positionals[1];
            output_filename_context = .{ .positional = arg_i + 1 };
        }
    }

    const OutputFormatSource = enum {
        inferred_from_input_filename,
        inferred_from_output_filename,
        output_format_arg,
        unable_to_infer_from_input_filename,
        unable_to_infer_from_output_filename,
        inferred_from_preprocess_only,
    };

    var output_format_source: OutputFormatSource = undefined;
    if (output_filename == null) {
        if (output_format == null) {
            output_format_source = .inferred_from_input_filename;
            const input_ext = std.fs.path.extension(options.input_source.filename);
            if (std.ascii.eqlIgnoreCase(input_ext, ".res")) {
                output_format = .coff;
            } else if (options.preprocess == .only and (input_format.? == .rc or std.ascii.eqlIgnoreCase(input_ext, ".rc"))) {
                output_format = .rcpp;
                output_format_source = .inferred_from_preprocess_only;
            } else {
                if (!std.ascii.eqlIgnoreCase(input_ext, ".res")) {
                    output_format_source = .unable_to_infer_from_input_filename;
                }
                output_format = .res;
            }
        } else {
            output_format_source = .output_format_arg;
        }
        options.output_source = .{ .filename = try filepathWithExtension(allocator, options.input_source.filename, output_format.?.extension()) };
    } else {
        options.output_source = .{ .filename = try allocator.dupe(u8, output_filename.?) };
        if (output_format == null) {
            output_format_source = .inferred_from_output_filename;
            const ext = std.fs.path.extension(options.output_source.filename);
            if (std.ascii.eqlIgnoreCase(ext, ".obj") or std.ascii.eqlIgnoreCase(ext, ".o")) {
                output_format = .coff;
            } else if (std.ascii.eqlIgnoreCase(ext, ".rcpp")) {
                output_format = .rcpp;
            } else {
                if (!std.ascii.eqlIgnoreCase(ext, ".res")) {
                    output_format_source = .unable_to_infer_from_output_filename;
                }
                output_format = .res;
            }
        } else {
            output_format_source = .output_format_arg;
        }
    }

    options.input_format = input_format.?;
    options.output_format = output_format.?;

    // Check for incompatible options
    var print_input_format_source_note: bool = false;
    var print_output_format_source_note: bool = false;
    if (options.depfile_path != null and (options.input_format == .res or options.output_format == .rcpp)) {
        var err_details = Diagnostics.ErrorDetails{ .type = .warning, .arg_index = depfile_context.index, .arg_span = depfile_context.value.argSpan(depfile_context.arg) };
        if (options.input_format == .res) {
            try err_details.msg.print(allocator, "the {s}{s} option was ignored because the input format is '{s}'", .{
                depfile_context.arg.prefixSlice(),
                depfile_context.arg.optionWithoutPrefix(depfile_context.option_len),
                @tagName(options.input_format),
            });
            print_input_format_source_note = true;
        } else if (options.output_format == .rcpp) {
            try err_details.msg.print(allocator, "the {s}{s} option was ignored because the output format is '{s}'", .{
                depfile_context.arg.prefixSlice(),
                depfile_context.arg.optionWithoutPrefix(depfile_context.option_len),
                @tagName(options.output_format),
            });
            print_output_format_source_note = true;
        }
        try diagnostics.append(err_details);
    }
    if (!isSupportedTransformation(options.input_format, options.output_format)) {
        var err_details = Diagnostics.ErrorDetails{ .arg_index = input_filename_arg_i, .print_args = false };
        try err_details.msg.print(allocator, "input format '{s}' cannot be converted to output format '{s}'", .{ @tagName(options.input_format), @tagName(options.output_format) });
        try diagnostics.append(err_details);
        print_input_format_source_note = true;
        print_output_format_source_note = true;
    }
    if (options.preprocess == .only and options.output_format != .rcpp) {
        var err_details = Diagnostics.ErrorDetails{ .arg_index = preprocess_only_context.index };
        try err_details.msg.print(allocator, "the {s}{s} option cannot be used with output format '{s}'", .{
            preprocess_only_context.arg.prefixSlice(),
            preprocess_only_context.arg.optionWithoutPrefix(preprocess_only_context.option_len),
            @tagName(options.output_format),
        });
        try diagnostics.append(err_details);
        print_output_format_source_note = true;
    }
    if (print_input_format_source_note) {
        switch (input_format_source) {
            .inferred_from_input_filename => {
                var err_details = Diagnostics.ErrorDetails{ .type = .note, .arg_index = input_filename_arg_i };
                try err_details.msg.appendSlice(allocator, "the input format was inferred from the input filename");
                try diagnostics.append(err_details);
            },
            .input_format_arg => {
                var err_details = Diagnostics.ErrorDetails{
                    .type = .note,
                    .arg_index = input_format_context.index,
                    .arg_span = input_format_context.value.argSpan(input_format_context.arg),
                };
                try err_details.msg.appendSlice(allocator, "the input format was specified here");
                try diagnostics.append(err_details);
            },
        }
    }
    if (print_output_format_source_note) {
        switch (output_format_source) {
            .inferred_from_input_filename, .unable_to_infer_from_input_filename => {
                var err_details = Diagnostics.ErrorDetails{ .type = .note, .arg_index = input_filename_arg_i };
                if (output_format_source == .inferred_from_input_filename) {
                    try err_details.msg.appendSlice(allocator, "the output format was inferred from the input filename");
                } else {
                    try err_details.msg.appendSlice(allocator, "the output format was unable to be inferred from the input filename, so the default was used");
                }
                try diagnostics.append(err_details);
            },
            .inferred_from_output_filename, .unable_to_infer_from_output_filename => {
                var err_details: Diagnostics.ErrorDetails = switch (output_filename_context) {
                    .positional => |i| .{ .type = .note, .arg_index = i },
                    .arg => |ctx| .{ .type = .note, .arg_index = ctx.index, .arg_span = ctx.value.argSpan(ctx.arg) },
                    .unspecified => unreachable,
                };
                if (output_format_source == .inferred_from_output_filename) {
                    try err_details.msg.appendSlice(allocator, "the output format was inferred from the output filename");
                } else {
                    try err_details.msg.appendSlice(allocator, "the output format was unable to be inferred from the output filename, so the default was used");
                }
                try diagnostics.append(err_details);
            },
            .output_format_arg => {
                var err_details = Diagnostics.ErrorDetails{
                    .type = .note,
                    .arg_index = output_format_context.index,
                    .arg_span = output_format_context.value.argSpan(output_format_context.arg),
                };
                try err_details.msg.appendSlice(allocator, "the output format was specified here");
                try diagnostics.append(err_details);
            },
            .inferred_from_preprocess_only => {
                var err_details = Diagnostics.ErrorDetails{ .type = .note, .arg_index = preprocess_only_context.index };
                try err_details.msg.print(allocator, "the output format was inferred from the usage of the {s}{s} option", .{
                    preprocess_only_context.arg.prefixSlice(),
                    preprocess_only_context.arg.optionWithoutPrefix(preprocess_only_context.option_len),
                });
                try diagnostics.append(err_details);
            },
        }
    }

    if (diagnostics.hasError()) {
        return error.ParseError;
    }

    // Implied settings from input/output formats
    if (options.output_format == .rcpp) options.preprocess = .only;
    if (options.input_format == .res) options.output_format = .coff;
    if (options.input_format == .rcpp) options.preprocess = .no;

    return options;
}