Note: Does not run Options.maybeAppendRC automatically. If that behavior is desired,
it must be called separately.
pub fn parse(allocator: Allocator, io: Io, args: []const []const u8, diagnostics: *Diagnostics) ParseError!Options
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;
}