diff --git a/lib/std/cli.zig b/lib/std/cli.zig new file mode 100644 index 000000000000..c3e3fd84d80a --- /dev/null +++ b/lib/std/cli.zig @@ -0,0 +1,1574 @@ +const std = @import("std.zig"); +const debug = std.debug; +const assert = debug.assert; +const testing = std.testing; +const ArrayList = std.ArrayList; +const isAlphabetic = std.ascii.isAlphabetic; +const Writer = std.Io.Writer; +const ArgIterator = std.process.ArgIterator; +const ArenaAllocator = std.heap.ArenaAllocator; +const StructField = std.builtin.Type.StructField; +const mem = std.mem; +const Allocator = mem.Allocator; + +pub const Options = struct { + /// Parsing/validation errors and the long `--help` documentation will be written to this writer. + /// By default, parsing/validation errors are written to stderr, and the long `--help` documentation is written to stdout. + /// Any error while writing is silently ignored. + writer: ?*Writer = null, + + /// The program name used in the help output, e.g. "my-command" in "usage: my-command [options] ...". + /// By default uses the last path component of the process's first argument (`argv[0]`). + /// When there is no `argv[0]` (such as with `parseSlice`), the default is `""`. + prog: ?[]const u8 = null, + + /// Call `std.process.exit` with an error status instead of returning `error.Usage` or `error.Help`. + /// The default is `true` for `parse` and `@"error"`, and `false` otherwise. + exit: ?bool = null, +}; + +pub const Error = error{ + /// Caused by unrecognized option names, values that cannot be parsed into the appropriate field type, + /// missing arguments for fields with no default value, and other similar parsing errors. + /// See also `options.exit`, which can supersede this error. + Usage, + /// The --help argument was given (and `options.exit` resolved to `false`). + Help, +} || Allocator.Error; + +/// Parses CLI args from a `std.process.ArgIterator` according to the configuration in `Args`. +/// `Args` is a struct that you define looking like this: +/// ``` +/// const Args = struct { +/// pub const description = "this program does a thing"; +/// named: struct { +/// verbose: bool = false, +/// output: [:0]const u8, +/// pub const output_help = "path to output file"; +/// }, +/// positional: struct { +/// input: []const u8, +/// args: []const []const u8 = &.{}, +/// }, +/// }; +/// ``` +/// Which results in this generated `--help` output: +/// ``` +/// usage: [options] --output=string input [args...] +/// +/// this program does a thing +/// +/// positional arguments: +/// input string. required +/// args string. can be specified multiple times +/// +/// named arguments: +/// --verbose default: --no-verbose +/// --output=string required. path to output file +/// --help print this help and exit +/// ``` +/// Either or both of `named` and `positional` may be omitted, which is effectively equivalent to declaring them as `struct {}`. +/// If `description` is declared, it is concatenated into the help output. +/// If any `pub const _help` accompanies a field `` in either `named` or `positional`, +/// it is included in that argument's help text. +/// +/// The sequence of arg strings from the `ArgIterator` is parsed to determine named and positional arguments. +/// +/// Each arg string takes one of these forms: +/// ``` +/// -- (1) +/// --no- (2) +/// --= (3) +/// --help (4) +/// - (5) always an error +/// -- (6) +/// (7) +/// ``` +/// Forms (1), (2), and (3) must correspond to a field `Args.named.`; see below for named argument handling. +/// Form (4) immediately prints the long help documentation and exits or returns `error.Help` depending on options.exit. +/// Form (6) signals that all following arg strings are positional. +/// Form (7) and all arg strings following form (6) are considered positional arguments, discussed below. +/// +/// Form (5) is always an error. +/// This API does not support single letter aliases like `-v` or `-lA` or named arguments prefixed by only a single hyphen like `-flag`. +/// Form (5) is defined by any arg string where the first byte is '-' and the second byte is `'A'...'Z', 'a'...'z'` +/// (and any following bytes are ignored). +/// A `-9` or other second byte outside the ascii-alpha range is Form (7). +/// +/// For forms (1), (2), and (3), let `T` be the type of `Args.named.`. +/// `T` may be any of the following: `bool`, any integer such as `i32`, any float such as `f64`, any `enum` with at least 1 member, +/// any string that `[:0]const u8` can coerce into such as `[]const u8`, +/// or a slice that `[]C` can coerce into such as `[]const C` where `C` is one of: +/// any integer, any float, or any string that `[:0]const u8` can coerce into. +/// Note that slice of bool and slice of enum are not allowed; see https://github.com/ziglang/zig/issues/24601 for discussion. +/// +/// If `T` is `bool`, then form (1) sets it to `true`, form (2) sets it to `false`, and form (3) is not allowed. +/// Otherwise, form (3) specifies the ``, form (1) must be immediately followed by another string arg which is the ``, +/// and form (2) is not allowed. +/// For non-bool `T` or for `C` in slice types, the `` is parsed from its string representation: +/// for integers using `std.fmt.parseInt` with base `0`; for floats using `std.fmt.parseFloat`; +/// for enums using `std.meta.stringToEnum`; and for strings no modification or copying is done. +/// +/// Each `Args.named.` may have a default value, which makes the `--` argument optional. +/// Slice arguments `[]const C` (where `C` is not `u8`) must have a default value, usually `&.{}`. +/// If a bool argument has no default value, then at least one of `--` or `--no-` must be given. +/// +/// Each positional arg string corresponds to a field in `Args.positional` in declaration order. +/// Each field in `Args.positional` may have a default value, making the corresponding argument optional. +/// Fields for required positional arguments must precede fields for optional arguments. +/// For each field, let `T` be its type. +/// Similar to `Args.named` described above, `T` may be any of the following: +/// any integer, any float, any `enum` with at least 1 member, or any string that `[:0]const u8` can coerce into. +/// Only the last declared field of `Args.positional` may alternatively have type `[]const C` where `C` is one of: +/// any integer, any float, any `enum` with at least 1 member, or any string that `[:0]const u8` can coerce into. +/// Similar to `Args.named`, a positional field declared with such a `[]const C` must have a default value, usually `&.{}`. +/// Such a `[]const C` field corresponds to all positional arguments after the positional arguments for the other fields. +/// +/// It's possible to override the automatically-generated long help documentation by declaring a public constant named `help` in `Args`. +/// The value must coerce to `[]const u8`. +/// +/// ``` +/// const Args = struct { +/// pub const help = +/// \\usage: your-command --your-usage goes-here +/// \\ +/// \\arguments: +/// \\ [...] +/// \\ --help +/// \\ +/// ; +/// named: struct { +/// // [...] +/// }, +/// }; +/// ``` +/// +/// The first arg returned by the `ArgIterator` (`argv[0]`) is skipped by all the above parsing logic. +/// If `options.prog` is `null`, then the final path component of `argv[0]` is used by default. +/// +/// If a parsing/validation error occurs or the `--help` arg is given, +/// this function calls `std.process.exit` with `1` and `0` respectively unless `options.exit` is set to `false`, +/// in which case parsing/validation errors return `error.Usage` and `--help` returns `error.Help`. +/// Allocator errors are always returned from the function. +/// +/// It is not possible to precisely deallocate the memory allocated by this function. +/// An `ArenaAllocator` is recommended to prevent memory leaks. +pub fn parse(comptime Args: type, arena: Allocator, options: Options) (Error || ArgIterator.InitError)!Args { + var iter: ArgIterator = try .initWithAllocator(arena); + // Do not call iter.deinit(). It holds the string data returned in the Args. + + const argv0 = iter.next(); + const prog = options.prog orelse if (argv0) |arg| std.fs.path.basename(arg) else ""; + return innerParse(Args, arena, &iter, prog, options.writer, options.exit orelse true); +} + +test parse { + const Args = struct { + named: struct { + /// Specified as `--output path.txt` or `--output=path.txt` + output: [:0]const u8 = "", + /// Supports `--level=9`, `--level -12`, `--level=0x7f`, etc. + level: i8 = -1, + /// Parsed as the name of the member `--color=never`. + color: enum { auto, never, always } = .auto, + + // The below parameters are actually passed into the `zig test` process, + // so we have to receive them here (as of zig 0.15.1). + seed: u32 = 0, + @"cache-dir": []const u8 = "", + listen: []const u8 = "", + }, + positional: struct { + /// First positional (non-named) argument: + input: [:0]const u8 = "", + /// Second positional argument is declared as optional: + repetitions: u32 = 1, + /// Receives the rest of the positional arguments. + @"the-rest": []const [:0]const u8 = &.{}, + }, + }; + + var arena: ArenaAllocator = .init(testing.allocator); + defer arena.deinit(); + const args = try std.cli.parse(Args, arena.allocator(), .{}); + + try testing.expectEqual(@as(i8, -1), args.named.level); +} + +/// Like `parse`, but allows specifying a custom arg iterator. +/// `iter` is typically a mutable pointer to a struct and must have a method: +/// ``` +/// pub fn next(self: *Self) ?String { ... } +/// ``` +/// Where `String` is `[]const u8` or `[:0]const u8` or something else that coerces to `[]const u8`. +/// If `String` does not coerce to `[:0]const u8`, then `Args` cannot have any `[:0]const u8` in its fields. +/// +/// The first string arg returned by the `iter` (`argv[0]`) is skipped by all the parsing logic. +/// If `options.prog` is `null`, then the final path component of `argv[0]` is used by default. +/// +/// If a parsing/validation error occurs or the `--help` arg is given, +/// this function returns `error.Usage` or `error.Help` respectively, +/// unless `options.exit` is set to `true`, in which case `std.process.exit` is called with `1` or `0` respectively. +/// Allocator errors are always returned from the function. +/// +/// An `ArenaAllocator` is recommended to cleanup the memory allocated from this function; +/// however, it's also possible to free all the memory by freeing every slice field `[]const C` (other than `u8`) +/// in the returned `args.named` and `args.positional`. +pub fn parseIter(comptime Args: type, arena: Allocator, iter: anytype, options: Options) Error!Args { + const argv0 = iter.next(); + const prog = options.prog orelse if (argv0) |arg| std.fs.path.basename(arg) else ""; + return innerParse(Args, arena, iter, prog, options.writer, options.exit orelse false); +} + +/// Like `parse`, but takes a slice of strings in place of using an `ArgIterator`. +/// `argv` must be either be a slice of `String` or a single-item pointer to an array of `String`, +/// where `String` is `[]const u8` or `[:0]const u8` or something else that coerces to `[]const u8`. +/// If `String` does not coerce to `[:0]const u8`, then `Args` cannot have `[:0]const u8` fields. +/// +/// Unlike `parse` and `parseIter`, this function does not skip the first item of `argv`. +/// Use `options.prog` instead. +/// +/// If a parsing/validation error occurs or the `--help` arg is given, +/// this function returns `error.Usage` or `error.Help` respectively, +/// unless `options.exit` is set to `true`, in which case `std.process.exit` is called with `1` or `0` respectively. +/// Allocator errors are always returned from the function. +/// +/// An `ArenaAllocator` is recommended to cleanup the memory allocated from this function; +/// however, it's also possible to free all the memory by freeing every slice field `[]const C` (other than `u8`) +/// in the returned `args.named` and `args.positional`. +pub fn parseSlice(comptime Args: type, arena: Allocator, argv: anytype, options: Options) Error!Args { + const argvInfo = @typeInfo(@TypeOf(argv)).pointer; + const String = if (argvInfo.size == .one) + @typeInfo(argvInfo.child).array.child + else if (argvInfo.size == .slice) + argvInfo.child + else + @compileError("expected argv to be `*const [_]String` or `[]const String` where `String` is `[]const u8` or similar"); + var iter = ArgIteratorSlice(String){ .slice = argv }; + return innerParse(Args, arena, &iter, options.prog orelse "", options.writer, options.exit orelse false); +} + +test parseSlice { + var arena: std.heap.ArenaAllocator = .init(testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + const Args = struct { + named: struct { + example_required: []const u8, + example_optional: []const u8 = "-", + level: i32 = -1, + flag: bool = true, + @"enum-option": enum { auto, always, never } = .auto, + }, + positional: struct { + args: []const []const u8 = &.{}, + }, + }; + const args = try parseSlice(Args, allocator, &[_][]const u8{ + "--example_required", "a.txt", + // --example_optional not given + "--level=0xff", "--no-flag", + "--enum-option", "always", + "positional1", "positional2", + "-12345678", "--", + "--positional4", "--positional=5", + }, .{}); + + try testing.expectEqualDeep(Args{ + .named = .{ + .example_required = "a.txt", + .example_optional = "-", + .level = 255, + .flag = false, + .@"enum-option" = .always, + }, + .positional = .{ .args = &.{ "positional1", "positional2", "-12345678", "--positional4", "--positional=5" } }, + }, args); +} + +fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: []const u8, writer: ?*Writer, exit_on_error: bool) Error!Args { + // argv0 has already been consumed. + + // Do all comptime checks up front so that we can be sure any compile error the user sees is the one we wrote. + const named_fields, const positional_fields = comptime checkArgsType(Args); + + var named_array_lists = arrayListsForFields(named_fields); + var positional_array_lists = arrayListsForFields(positional_fields); + + var result: Args = undefined; + var named_fields_seen = [_]bool{false} ** named_fields.len; + var positional_field_index: usize = 0; + + var the_rest_is_positional = false; + + while (iter.next()) |arg| { + if (!the_rest_is_positional and mem.eql(u8, arg, "--help")) { + if (@hasDecl(Args, "help")) { + // Custom help. + if (writer) |w| { + w.writeAll(Args.help) catch {}; + w.flush() catch {}; + } else { + var file_writer = std.fs.File.stdout().writer(&.{}); + file_writer.interface.writeAll(Args.help) catch {}; + file_writer.interface.flush() catch {}; + } + } else { + printGeneratedHelp(Args, writer, prog); + } + if (exit_on_error) { + std.process.exit(0); + } + return error.Help; + } + + if (!the_rest_is_positional and arg.len >= 2 and arg[0] == '-' and isAlphabetic(arg[1])) { + // Always invalid. + // Examples: -h, -flag, -I/path + return usageError(Args, writer, "unrecognized argument: {s}", .{arg}, prog, exit_on_error); + } + if (!the_rest_is_positional and mem.eql(u8, arg, "--")) { + // Stop recognizing named arguments. Everything else is positional. + the_rest_is_positional = true; + continue; + } + if (the_rest_is_positional or !(arg.len >= 3 and arg[0] == '-' and arg[1] == '-')) { + // Positional. + // Examples: "", "a", "-", "-1", "other" + if (positional_field_index >= positional_fields.len) return usageError(Args, writer, "unexpected positional argument: {s}", .{arg}, prog, exit_on_error); + inline for (positional_fields, 0..) |field, i| { + if (positional_field_index == i) { + if (getArrayChild(field.type)) |C| { + try @field(positional_array_lists, field.name).append(allocator, try parseValue(Args, C, arg, field.name, writer, prog, exit_on_error)); + // Don't increment positional_field_index. + } else { + @field(result.positional, field.name) = try parseValue(Args, field.type, arg, field.name, writer, prog, exit_on_error); + positional_field_index += 1; + } + break; + } + } else unreachable; + continue; + } + + // Named. + const arg_name, const immediate_value, const no_prefixed = blk: { + if (mem.startsWith(u8, arg, "--no-")) { + break :blk .{ arg["--no-".len..], null, true }; + } + if (mem.indexOfScalarPos(u8, arg, "--".len, '=')) |index| { + if (@typeInfo(@TypeOf(arg)).pointer.sentinel_ptr != null) { + break :blk .{ arg["--".len..index], arg[index + 1 .. :0], false }; + } else { + break :blk .{ arg["--".len..index], arg[index + 1 ..], false }; + } + } + break :blk .{ arg["--".len..], null, false }; + }; + + inline for (named_fields, 0..) |field, i| { + if (mem.eql(u8, field.name, arg_name)) { + named_fields_seen[i] = true; + if (field.type == bool) { + if (immediate_value != null) return usageError(Args, writer, "cannot specify value for bool argument: {s}", .{arg}, prog, exit_on_error); + @field(result.named, field.name) = !no_prefixed; + break; + } + if (no_prefixed) return usageError(Args, writer, "unrecognized argument: {s}", .{arg}, prog, exit_on_error); + + // All other argument types require a value. + const arg_value = immediate_value orelse iter.next() orelse return usageError(Args, writer, "expected argument after --{s}", .{field.name}, prog, exit_on_error); + + if (getArrayChild(field.type)) |C| { + try @field(named_array_lists, field.name).append(allocator, try parseValue(Args, C, arg_value, field.name, writer, prog, exit_on_error)); + } else { + @field(result.named, field.name) = try parseValue(Args, field.type, arg_value, field.name, writer, prog, exit_on_error); + } + break; + } + } else { + // Didn't match anything. + return usageError(Args, writer, "unrecognized argument: {s}", .{arg}, prog, exit_on_error); + } + } + + // Fill default values. + inline for (named_fields, 0..) |field, i| { + if (getArrayChild(field.type)) |_| { + // Array. + @field(result.named, field.name) = try @field(named_array_lists, field.name).toOwnedSlice(allocator); + } else { + // Scalar. + if (!named_fields_seen[i]) { + // Unspecified. + if (field.defaultValue()) |default| { + @field(result.named, field.name) = default; + } else { + if (field.type == bool) { + return usageError(Args, writer, "missing required argument: --" ++ field.name ++ " or --no-" ++ field.name, .{}, prog, exit_on_error); + } else { + return usageError(Args, writer, "missing required argument: --" ++ field.name, .{}, prog, exit_on_error); + } + } + } + } + } + inline for (positional_fields, 0..) |field, i| { + if (getArrayChild(field.type)) |_| { + // Array. + @field(result.positional, field.name) = try @field(positional_array_lists, field.name).toOwnedSlice(allocator); + } else { + // Scalar. + if (positional_field_index <= i) { + // Unspecified. + if (field.defaultValue()) |default| { + @field(result.positional, field.name) = default; + } else { + return usageError(Args, writer, "missing required argument: " ++ field.name, .{}, prog, exit_on_error); + } + } + } + } + + return result; +} + +/// arg_value is []const u8 or [:0]const u8. +fn parseValue(comptime Args: type, comptime T: type, arg_value: anytype, comptime field_name: []const u8, writer: ?*Writer, prog: []const u8, exit_on_error: bool) !T { + switch (@typeInfo(T)) { + .bool => comptime unreachable, // Handled elsewhere. + .float => { + return std.fmt.parseFloat(T, arg_value) catch |err| { + return usageError(Args, writer, "unable to parse --{s}={s}: {s}", .{ field_name, arg_value, @errorName(err) }, prog, exit_on_error); + }; + }, + .int => { + return std.fmt.parseInt(T, arg_value, 0) catch |err| { + return usageError(Args, writer, "unable to parse --{s}={s}: {s}", .{ field_name, arg_value, @errorName(err) }, prog, exit_on_error); + }; + }, + .@"enum" => { + return std.meta.stringToEnum(T, arg_value) orelse { + return usageError(Args, writer, "unrecognized value: --{s}={s}, expected one of: {s}", .{ field_name, arg_value, enumValuesExpr(T) }, prog, exit_on_error); + }; + }, + .pointer => |ptrInfo| { + comptime assert(ptrInfo.size == .slice); + comptime assert(ptrInfo.child == u8); + return arg_value; // To resolve compile errors between `[:0]const u8` and `[]const u8` on this line, ensure the passed-in args are `[:0]const u8`. + }, + else => comptime unreachable, + } +} + +fn checkArgsType(comptime Args: type) struct { []const StructField, []const StructField } { + var has_named = false; + var has_positional = false; + inline for (@typeInfo(Args).@"struct".fields) |field| { + if (mem.eql(u8, field.name, "named")) { + has_named = true; + } else if (mem.eql(u8, field.name, "positional")) { + has_positional = true; + } else @compileError("unrecognized Args name: " ++ field.name); + } + + const named_fields = if (has_named) @typeInfo(@FieldType(Args, "named")).@"struct".fields else &.{}; + const positional_fields = if (has_positional) @typeInfo(@FieldType(Args, "positional")).@"struct".fields else &.{}; + + // Named arguments are more lenient. + inline for (named_fields) |field| { + validateField(field); + } + + // Positional arguments have stricter rules. + var everything_still_required = true; + var everything_still_scalar = true; + inline for (positional_fields) |field| { + if (field.type == bool) @compileError("Args.positional cannot have bool fields: " ++ field.name); + validateField(field); + const is_scalar = getArrayChild(field.type) == null; + + const is_required = field.default_value_ptr == null; + + // There can only be one array parameter, and it must be last. + if (everything_still_scalar) { + if (!is_scalar) { + everything_still_scalar = false; + } + } else @compileError("a positional array argument must be last. found: " ++ field.name); + + // Required positional parameters must come first. + if (everything_still_required) { + if (!is_required) { + everything_still_required = false; + } + } else { + if (is_required) @compileError("cannot have a required positional argument after an optional one: " ++ field.name); + } + } + + return .{ named_fields, positional_fields }; +} + +fn validateField(field: StructField) void { + if (field.is_comptime) @compileError("comptime fields are not supported: " ++ field.name); + if (comptime mem.eql(u8, field.name, "help")) @compileError("A field named help is not allowed. add a `pub const help = \"...\";` to your `Args` to provide a custom help string."); + if (comptime mem.startsWith(u8, field.name, "no-")) @compileError("Field name starts with @\"no-\": " ++ field.name ++ ". Note: use a bool type field, and -- and --no- will turn it on and off."); + if (comptime mem.indexOfScalar(u8, field.name, '=') != null) @compileError("Field name contains @\"=\": " ++ field.name); + + switch (@typeInfo(field.type)) { + .bool => {}, + .float => {}, + .int => {}, + .@"enum" => { + if (@typeInfo(field.type).@"enum".fields.len == 0) @compileError("Empty enums not allowed"); + }, + .pointer => |ptrInfo| { + if (ptrInfo.size != .slice) @compileError("Unsupported field type: " ++ @typeName(field.type)); + if (ptrInfo.child == u8) { + // String. + } else { + // Array. + if (field.defaultValue()) |default| { + if (default.len != 0) @compileError("Array argument default value must have 0 len: " ++ field.name); + } else @compileError("Array arguments must have a default value: " ++ field.name); + switch (@typeInfo(ptrInfo.child)) { + .bool => @compileError("Unsupported field type: " ++ @typeName(field.type)), + .float => {}, + .int => {}, + .@"enum" => @compileError("Unsupported field type: " ++ @typeName(field.type)), + .pointer => |ptrInfo2| { + if (ptrInfo2.size == .slice and ptrInfo2.child == u8) { + // String. + } else { + @compileError("Unsupported field type: " ++ @typeName(field.type)); + } + }, + else => @compileError("Unsupported field type: " ++ @typeName(field.type)), + } + } + }, + else => @compileError("Unsupported field type: " ++ @typeName(field.type)), + } +} + +/// returns null if T is a scalar type. +fn getArrayChild(comptime T: type) ?type { + // This logic assumes the type has already passed validation. + return switch (@typeInfo(T)) { + .pointer => |ptrInfo| if (ptrInfo.child == u8) null else ptrInfo.child, + else => null, + }; +} + +fn arrayListsForFields(comptime fields: []const StructField) ArrayListsForFields(fields) { + var array_lists: ArrayListsForFields(fields) = undefined; + inline for (@typeInfo(@TypeOf(array_lists)).@"struct".fields) |field| { + @field(array_lists, field.name) = .{}; + } + return array_lists; +} +fn ArrayListsForFields(comptime fields: []const StructField) type { + // Declare and initialize an ArrayList(C) for every []const C field (other than u8). + comptime var array_list_fields: []const StructField = &.{}; + inline for (fields) |field| { + const info = @typeInfo(field.type); + if (info == .pointer) { + comptime assert(info.pointer.size == .slice); + if (info.pointer.child == u8) { + // String. skip. + } else { + // Array of scalar. + array_list_fields = array_list_fields ++ @as([]const StructField, &.{.{ + .name = field.name, + .type = ArrayList(info.pointer.child), + .default_value_ptr = null, + .is_comptime = false, + .alignment = @alignOf(ArrayList(info.pointer.child)), + }}); + } + } + } + return @Type(.{ .@"struct" = .{ .layout = .auto, .fields = array_list_fields, .decls = &.{}, .is_tuple = false } }); +} + +/// If you do your own validation after getting an `args` from `parse` or similar, +/// call this function to produce the same error behavior as if this API's validation failed. +/// An error message will be written to `options.writer` or stderr by default, and `error.Usage` is returned. +/// The given `msg` template is prefixed by `"error: "` and suffixed by a newline and a prompt to try passing in `--help`. +/// `options.prog` is not used by this function, but could be in the future. TODO: yes it is. +/// +/// This function calls `std.process.exit` with an error status unless `options.exit` is set to `false`, in which case it returns `error.Usage`. +/// This matches the default behavior of `parse`, not `parseIter` or `parseSlice`. +pub fn @"error"(comptime Args: type, comptime msg: []const u8, msg_args: anytype, options: Options) error{Usage} { + var buf: [0x1000]u8 = undefined; + const prog: ?[]const u8 = options.prog orelse blk: { + var fba: std.heap.FixedBufferAllocator = .init(&buf); + var iter = ArgIterator.initWithAllocator(fba.allocator()) catch break :blk null; + const argv0 = iter.next(); + break :blk if (argv0) |arg| std.fs.path.basename(arg) else null; + }; + return usageError(Args, options.writer, msg, msg_args, prog orelse "", options.exit orelse true); +} + +test @"error" { + const Args = struct { + named: struct { + output: []const u8 = "", + }, + positional: struct { + input: []const u8, + }, + }; + + var arena: std.heap.ArenaAllocator = .init(testing.allocator); + defer arena.deinit(); + const args = try parseSlice(Args, arena.allocator(), &[_][]const u8{ "--output=o.txt", "i.txt" }, .{}); + + if (std.fs.path.isAbsolute(args.named.output)) { + return std.cli.@"error"(Args, "--output must not be absolute: {s}", .{args.named.output}, .{ .exit = false }); + } +} + +fn ArgIteratorSlice(comptime String: type) type { + return struct { + slice: []const String, + index: usize = 0, + + pub fn next(self: *@This()) ?String { + if (self.index >= self.slice.len) return null; + const result = self.slice[self.index]; + self.index += 1; + return result; + } + }; +} + +fn enumValuesExpr(comptime Enum: type) []const u8 { + comptime var values_str: []const u8 = "{"; + inline for (@typeInfo(Enum).@"enum".fields) |enum_field| { + if (values_str.len > 1) { + values_str = values_str ++ ","; + } + values_str = values_str ++ enum_field.name; + } + values_str = values_str ++ "}"; + return values_str; +} + +fn usageError(comptime Args: type, writer: ?*Writer, comptime msg: []const u8, args: anytype, prog: []const u8, exit_on_error: bool) error{Usage} { + const named_fields, const positional_fields = comptime checkArgsType(Args); + const whole_msg = + "error: " ++ msg ++ "\n" ++ // + "usage: {s} " ++ comptime usageLineFmt(named_fields, positional_fields) ++ "\n" ++ + \\try --help for full help info + \\ + ; + if (writer) |w| { + w.print(whole_msg, args ++ .{prog}) catch {}; + } else { + std.debug.print(whole_msg, args ++ .{prog}); + } + if (exit_on_error) { + std.process.exit(1); + } + return error.Usage; +} + +/// returns a string with all "{" escaped for passing into std.fmt. +fn usageLineFmt(comptime named_fields: []const StructField, comptime positional_fields: []const StructField) []const u8 { + comptime var usage_parts: []const []const u8 = &.{}; + var at_least_one_optional_named_argument = false; + inline for (named_fields) |field| { + if (field.default_value_ptr != null) { + // Don't mention optional named arguments. + at_least_one_optional_named_argument = true; + continue; + } + usage_parts = usage_parts ++ .{switch (@typeInfo(field.type)) { + .bool => "--[no-]" ++ field.name, + .int, .float => "--" ++ field.name ++ "=" ++ @typeName(field.type), + .@"enum" => "--" ++ field.name ++ "=" ++ enumValuesExpr(field.type), + else => blk: { + comptime assert(@typeInfo(field.type).pointer.size == .slice and @typeInfo(field.type).pointer.child == u8); + break :blk "--" ++ field.name ++ "=string"; + }, + }}; + } + + if (at_least_one_optional_named_argument) { + // Prepend with an [options] placeholder. + usage_parts = [_][]const u8{"[options]"} ++ usage_parts; + } + + inline for (positional_fields) |field| { + if (field.default_value_ptr != null) { + if (getArrayChild(field.type) != null) { + // Array + usage_parts = usage_parts ++ .{"[" ++ field.name ++ "...]"}; + } else { + // Scalar + usage_parts = usage_parts ++ .{"[" ++ field.name ++ "]"}; + } + } else { + usage_parts = usage_parts ++ .{field.name}; + } + } + + comptime var usage_str: []const u8 = ""; + inline for (usage_parts) |part| { + if (usage_str.len > 0) { + usage_str = usage_str ++ " "; + } + usage_str = usage_str ++ part; + } + return escapeFmt(usage_str); +} +fn printGeneratedHelp(comptime Args: type, writer: ?*Writer, prog: []const u8) void { + const named_fields, const positional_fields = comptime checkArgsType(Args); + + comptime var arguments_table: []const []const []const u8 = &.{}; + + if (positional_fields.len > 0) { + arguments_table = arguments_table ++ .{ &[_][]const u8{""}, &[_][]const u8{"positional arguments:"} }; + } + inline for (positional_fields) |field| { + switch (@typeInfo(field.type)) { + .int, .float => { + arguments_table = arguments_table ++ .{&[_][]const u8{ + " " ++ field.name, + @typeName(field.type) ++ " " ++ + (if (field.defaultValue()) |default| + "default: " ++ std.fmt.comptimePrint("{}", .{default}) + else + "required") ++ argHelp(Args, "positional", field.name), + }}; + }, + .@"enum" => { + arguments_table = arguments_table ++ .{&[_][]const u8{ + " " ++ field.name, + comptime enumValuesExpr(field.type) ++ ". " ++ + (if (field.defaultValue()) |default| + "default: " ++ @tagName(default) + else + "required") ++ argHelp(Args, "positional", field.name), + }}; + }, + .pointer => |ptrInfo| { + if (ptrInfo.size == .slice and ptrInfo.child == u8) { + // String + arguments_table = arguments_table ++ .{&[_][]const u8{ + " " ++ field.name, + "string. " ++ + (if (field.defaultValue()) |default| + "default: " ++ quoteIfEmpty(default) + else + "required") ++ argHelp(Args, "positional", field.name), + }}; + } else { + // Array + const type_name = switch (@typeInfo(ptrInfo.child)) { + .bool => comptime unreachable, + .int, .float => @typeName(ptrInfo.child), + .@"enum" => comptime unreachable, + .pointer => "string", // The array-of-pointer that doesn't cause compile errors elsewhere. + else => comptime unreachable, + }; + arguments_table = arguments_table ++ .{&[_][]const u8{ + " " ++ field.name, + type_name ++ ". can be specified multiple times" ++ argHelp(Args, "positional", field.name), + }}; + } + }, + else => comptime unreachable, + } + } + + arguments_table = arguments_table ++ .{ &[_][]const u8{""}, &[_][]const u8{"named arguments:"} }; + inline for (named_fields) |field| { + switch (@typeInfo(field.type)) { + .bool => { + if (field.defaultValue()) |default| { + if (default) { + arguments_table = arguments_table ++ .{&[_][]const u8{ " --no-" ++ field.name, "default: --" ++ field.name ++ argHelp(Args, "named", field.name) }}; + } else { + arguments_table = arguments_table ++ .{&[_][]const u8{ " --" ++ field.name, "default: --no-" ++ field.name ++ argHelp(Args, "named", field.name) }}; + } + } else { + arguments_table = arguments_table ++ .{&[_][]const u8{ " --[no-]" ++ field.name, "required" ++ argHelp(Args, "named", field.name) }}; + } + }, + .int, .float => { + arguments_table = arguments_table ++ .{&[_][]const u8{ + " --" ++ field.name ++ "=" ++ @typeName(field.type), + (if (field.defaultValue()) |default| + "default: " ++ std.fmt.comptimePrint("{}", .{default}) + else + "required") ++ argHelp(Args, "named", field.name), + }}; + }, + .@"enum" => { + arguments_table = arguments_table ++ .{&[_][]const u8{ + " --" ++ field.name ++ "=enum", + comptime enumValuesExpr(field.type) ++ ". " ++ + (if (field.defaultValue()) |default| + "default: " ++ @tagName(default) + else + "required") ++ argHelp(Args, "named", field.name), + }}; + }, + .pointer => |ptrInfo| { + if (ptrInfo.size == .slice and ptrInfo.child == u8) { + // String + arguments_table = arguments_table ++ .{&[_][]const u8{ + " --" ++ field.name ++ "=string", + (if (field.defaultValue()) |default| + "default: " ++ quoteIfEmpty(default) + else + "required") ++ argHelp(Args, "named", field.name), + }}; + } else { + // Array + const type_name = switch (@typeInfo(ptrInfo.child)) { + .bool => comptime unreachable, + .int, .float => @typeName(ptrInfo.child), + .@"enum" => comptime unreachable, + .pointer => "string", // The array-of-pointer that doesn't cause compile errors elsewhere. + else => comptime unreachable, + }; + arguments_table = arguments_table ++ .{&[_][]const u8{ + " --" ++ field.name ++ "=" ++ type_name, + "can be specified multiple times" ++ argHelp(Args, "named", field.name), + }}; + } + }, + else => comptime unreachable, + } + } + + arguments_table = arguments_table ++ .{&[_][]const u8{ " --help", "print this help and exit" }}; + + comptime var width = 0; + inline for (arguments_table) |row| { + width = @max(width, row[0].len); + } + + comptime var help_str: []const u8 = ""; + if (@hasDecl(Args, "description")) { + help_str = "\n\n" ++ Args.description; + } + inline for (arguments_table) |row| { + help_str = help_str ++ "\n"; + inline for (row, 0..) |cell, c| { + help_str = help_str ++ cell; + if (c == 0 and row.len > 1) { + help_str = help_str ++ " " ** (width + 2 - cell.len); + } + } + } + + const msg = "usage: {s} " ++ comptime usageLineFmt(named_fields, positional_fields) ++ // + escapeFmt(help_str) ++ "\n"; + if (writer) |w| { + w.print(msg, .{prog}) catch {}; + w.flush() catch {}; + } else { + var buffer: [0x100]u8 = undefined; + var file_writer = std.fs.File.stdout().writer(&buffer); + file_writer.interface.print(msg, .{prog}) catch {}; + file_writer.interface.flush() catch {}; + } +} + +inline fn argHelp(comptime Args: type, comptime named_or_positional: []const u8, comptime field_name: []const u8) []const u8 { + const N = @FieldType(Args, named_or_positional); + comptime assert(@hasField(N, field_name)); + if (!@hasDecl(N, field_name ++ "_help")) return ""; + return ". " ++ @field(N, field_name ++ "_help"); +} + +inline fn quoteIfEmpty(comptime s: []const u8) []const u8 { + if (s.len == 0) return "''"; + return s; +} + +inline fn escapeFmt(comptime s: []const u8) []const u8 { + var result: []const u8 = ""; + comptime var cursor = 0; + for (s, 0..) |c, i| { + switch (c) { + '{' => { + result = result ++ s[cursor..i] ++ "{{"; + cursor = i + 1; + }, + '}' => { + result = result ++ s[cursor..i] ++ "}}"; + cursor = i + 1; + }, + else => {}, + } + } + result = result ++ s[cursor..]; + return result; +} + +var failing_writer: Writer = .failing; +const silent_options = Options{ .writer = &failing_writer, .exit = false }; + +test "bool" { + var arena: std.heap.ArenaAllocator = .init(testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + const Args = struct { + named: struct { + b: bool, + }, + }; + + try testing.expectEqualDeep(Args{ .named = .{ .b = true } }, try parseSlice(Args, allocator, &[_][]const u8{"--b"}, .{})); + try testing.expectEqualDeep(Args{ .named = .{ .b = false } }, try parseSlice(Args, allocator, &[_][]const u8{"--no-b"}, .{})); + try testing.expectEqualDeep(Args{ .named = .{ .b = true } }, try parseSlice(Args, allocator, &[_][]const u8{ "--no-b", "--b" }, .{})); + try testing.expectEqualDeep(Args{ .named = .{ .b = false } }, try parseSlice(Args, allocator, &[_][]const u8{ "--b", "--no-b" }, .{})); + + try testing.expectError(error.Usage, parseSlice(Args, allocator, &[_][]const u8{"--b=true"}, silent_options)); + try testing.expectError(error.Usage, parseSlice(Args, allocator, &[_][]const u8{"--b=false"}, silent_options)); +} + +test "string" { + var arena: std.heap.ArenaAllocator = .init(testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + const Args = struct { + named: struct { + a: []const u8, + b: [:0]const u8, + }, + }; + const args = try parseSlice(Args, allocator, &[_][:0]const u8{ + "--a", "a", + "--b", "b", + }, .{}); + + try testing.expectEqualDeep(Args{ + .named = .{ + .a = "a", + .b = "b", + }, + }, args); +} + +test "ints and floats" { + var arena: std.heap.ArenaAllocator = .init(testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + const Args = struct { + named: struct { + int_u32: u32, + int_i32: i32, + int_u8: u8, + int_u256: u256, + float_f32: f32, + float_f64: f64, + inf_f32: f32, + ninf_f64: f64, + }, + }; + const args = try parseSlice(Args, allocator, &[_][]const u8{ + "--int_u32", "0xffffffff", + "--int_i32", "-0x80000000", + "--int_u8", "0o310", + "--int_u256", "115792089237316195423570985008687907853269984665640564039457584007913129639935", + "--float_f32", "1.25", + "--float_f64", "-0xab.cdef012345p-12", + "--inf_f32", "inf", + "--ninf_f64", "-INF", + }, .{}); + + try testing.expectEqualDeep(Args{ + .named = .{ + .int_u32 = 0xffffffff, + .int_i32 = -0x80000000, + .int_u8 = 0o310, + .int_u256 = 115792089237316195423570985008687907853269984665640564039457584007913129639935, + .float_f32 = 1.25, + .float_f64 = -0xab.cdef012345p-12, + .inf_f32 = std.math.inf(f32), + .ninf_f64 = -std.math.inf(f64), + }, + }, args); + + const Args2 = struct { + named: struct { + nan: f64, + }, + }; + const args2 = try parseSlice(Args2, allocator, &[_][]const u8{ + "--nan", "nAN", + }, .{}); + + try testing.expect(std.math.isNan(args2.named.nan)); +} + +test "array" { + var arena: std.heap.ArenaAllocator = .init(testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + const Args = struct { + named: struct { + path: []const []const u8 = &.{}, + id: []const i32 = &.{}, + }, + positional: struct { + args: []const []const u8 = &.{}, + }, + }; + + try testing.expectEqualDeep(Args{ + .named = .{ + .path = &[_][]const u8{ "a", "b", "a" }, + .id = &[_]i32{ 1, -12 }, + }, + .positional = .{ + .args = &[_][]const u8{ "x", "y" }, + }, + }, try parseSlice(Args, allocator, &[_][]const u8{ + "--path", "a", + "--path", "b", + "--path", "a", + "--id", "1", + "--id", "-12", + "x", "y", + }, .{})); +} + +test "enum" { + var arena: std.heap.ArenaAllocator = .init(testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + const Args = struct { + named: struct { + color: enum { + always, + never, + auto, + }, + guess: enum { + @"the-only-option", + }, + signal: enum(u8) { + KILL = 9, + TERM = 15, + VTALRM = 26, + }, + }, + }; + const args = try parseSlice(Args, allocator, &[_][]const u8{ + "--color", "always", + "--guess", "the-only-option", + "--signal", "TERM", + }, .{}); + + try testing.expectEqualDeep(Args{ + .named = .{ + .color = .always, + .guess = .@"the-only-option", + .signal = .TERM, + }, + }, args); +} + +test "defaults" { + var arena: std.heap.ArenaAllocator = .init(testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + const Args = struct { + named: struct { + level: i8 = -1, + ratio: f32 = 0.5, + path: []const u8 = "-", + color: enum { + always, + never, + auto, + } = .auto, + file: []const []const u8 = &.{}, + force: bool = false, + cleanup: bool = true, + }, + }; + + try testing.expectEqualDeep(Args{ + .named = .{}, + }, try parseSlice(Args, allocator, &[_][]const u8{}, .{})); + try testing.expectEqualDeep(Args{ + .named = .{ + .color = .always, + }, + }, try parseSlice(Args, allocator, &[_][]const u8{ "--color", "always" }, .{})); + try testing.expectEqualDeep(Args{ + .named = .{ + .file = &[_][]const u8{"file.txt"}, + }, + }, try parseSlice(Args, allocator, &[_][]const u8{ "--file", "file.txt" }, .{})); + + try testing.expectEqualDeep(Args{ + .named = .{ + .force = true, + .cleanup = false, + }, + }, try parseSlice(Args, allocator, &[_][]const u8{ "--force", "--no-cleanup" }, .{})); +} + +test "positional" { + var arena: std.heap.ArenaAllocator = .init(testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + // defaults + { + const Args = struct { + positional: struct { + level: i8 = -1, + ratio: f32 = 0.5, + path: []const u8 = "-", + color: enum { + always, + never, + auto, + } = .auto, + file: []const []const u8 = &.{}, + }, + }; + + try testing.expectEqualDeep(Args{ + .positional = .{}, + }, try parseSlice(Args, allocator, &[_][]const u8{}, .{})); + try testing.expectEqualDeep(Args{ + .positional = .{ + .level = 1, + .ratio = 2, + .path = "a.txt", + .color = .always, + .file = &[_][]const u8{ "file1", "file2" }, + }, + }, try parseSlice(Args, allocator, &[_][]const u8{ "1", "2", "a.txt", "always", "file1", "file2" }, .{})); + } + + // required + { + const Args = struct { + positional: struct { + level: i8, + ratio: f32, + path: []const u8, + color: enum { + always, + never, + auto, + }, + file: []const []const u8 = &.{}, + }, + }; + + try testing.expectError(error.Usage, parseSlice(Args, allocator, &[_][]const u8{}, silent_options)); + try testing.expectError(error.Usage, parseSlice(Args, allocator, &[_][]const u8{ "1", "2", "a.txt" }, silent_options)); + try testing.expectEqualDeep(Args{ + .positional = .{ + .level = 1, + .ratio = 2, + .path = "a.txt", + .color = .always, + }, + }, try parseSlice(Args, allocator, &[_][]const u8{ "1", "2", "a.txt", "always" }, .{})); + } +} + +test "usage errors" { + var arena: std.heap.ArenaAllocator = .init(testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + var aw: Writer.Allocating = .init(allocator); + const options = Options{ .prog = "test-prog", .writer = &aw.writer }; + + // unrecognized argument + aw.clearRetainingCapacity(); + try testing.expectError(error.Usage, parseSlice(struct { + named: struct { + name: []const u8 = "", + }, + }, allocator, &[_][]const u8{"--bogus"}, options)); + try testing.expect(mem.indexOf(u8, aw.written(), "--bogus") != null); + + // expected argument + aw.clearRetainingCapacity(); + try testing.expectError(error.Usage, parseSlice(struct { + named: struct { + name: []const u8 = "", + }, + }, allocator, &[_][]const u8{"--name"}, options)); + try testing.expect(mem.indexOf(u8, aw.written(), "--name") != null); + + // --no- for non-bool. + aw.clearRetainingCapacity(); + try testing.expectError(error.Usage, parseSlice(struct { + named: struct { + name: []const u8 = "", + }, + }, allocator, &[_][]const u8{"--no-name"}, options)); + try testing.expect(mem.indexOf(u8, aw.written(), "--no-name") != null); + + // --name=false for bool + aw.clearRetainingCapacity(); + try testing.expectError(error.Usage, parseSlice(struct { + named: struct { + name: bool = false, + }, + }, allocator, &[_][]const u8{"--name=true"}, options)); + try testing.expect(mem.indexOf(u8, aw.written(), "--name") != null); + + // missing required argument + aw.clearRetainingCapacity(); + try testing.expectError(error.Usage, parseSlice(struct { + named: struct { + name: []const u8, + }, + }, allocator, &[_][]const u8{}, options)); + try testing.expect(mem.indexOf(u8, aw.written(), "--name") != null); + + // parse int error + aw.clearRetainingCapacity(); + try testing.expectError(error.Usage, parseSlice(struct { + named: struct { + name: i32, + }, + }, allocator, &[_][]const u8{"--name=abc"}, options)); + try testing.expect(mem.indexOf(u8, aw.written(), "--name") != null); + aw.clearRetainingCapacity(); + try testing.expectError(error.Usage, parseSlice(struct { + named: struct { + name: []const i32 = &.{}, + }, + }, allocator, &[_][]const u8{"--name=abc"}, options)); + try testing.expect(mem.indexOf(u8, aw.written(), "--name") != null); + + // parse float error + aw.clearRetainingCapacity(); + try testing.expectError(error.Usage, parseSlice(struct { + named: struct { + name: f32, + }, + }, allocator, &[_][]const u8{"--name=abc"}, options)); + try testing.expect(mem.indexOf(u8, aw.written(), "--name") != null); + aw.clearRetainingCapacity(); + try testing.expectError(error.Usage, parseSlice(struct { + named: struct { + name: []const f32 = &.{}, + }, + }, allocator, &[_][]const u8{"--name=abc"}, options)); + try testing.expect(mem.indexOf(u8, aw.written(), "--name") != null); + + // parse enum error + aw.clearRetainingCapacity(); + try testing.expectError(error.Usage, parseSlice(struct { + named: struct { + name: enum { auto, never, always }, + }, + }, allocator, &[_][]const u8{"--name=abc"}, options)); + try testing.expect(mem.indexOf(u8, aw.written(), "--name") != null); + try testing.expect(mem.indexOf(u8, aw.written(), "abc") != null); + // Error should suggest the set of options. + try testing.expect(mem.indexOf(u8, aw.written(), "always") != null); + + // reject single-letter alias-looking arguments + aw.clearRetainingCapacity(); + try testing.expectError(error.Usage, parseSlice(struct { + named: struct { + z: bool = false, + }, + positional: struct { + args: []const []const u8 = &.{}, + }, + }, allocator, &[_][]const u8{"-z"}, options)); + try testing.expect(mem.indexOf(u8, aw.written(), "-z") != null); + + // expected required positional argument + aw.clearRetainingCapacity(); + try testing.expectError(error.Usage, parseSlice(struct { + positional: struct { + input_file: []const u8, + }, + }, allocator, &[_][]const u8{}, options)); + try testing.expect(mem.indexOf(u8, aw.written(), "input_file") != null); + aw.clearRetainingCapacity(); + try testing.expectError(error.Usage, parseSlice(struct { + positional: struct { + input_file: []const u8, + output_file: []const u8 = "", + }, + }, allocator, &[_][]const u8{}, options)); + try testing.expect(mem.indexOf(u8, aw.written(), "input_file") != null); + aw.clearRetainingCapacity(); + try testing.expectError(error.Usage, parseSlice(struct { + positional: struct { + input_file: []const u8, + output_file: []const u8, + other: []const u8 = "", + }, + }, allocator, &[_][]const u8{"input.txt"}, options)); + try testing.expect(mem.indexOf(u8, aw.written(), "output_file") != null); +} + +test "help" { + var arena: std.heap.ArenaAllocator = .init(testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + var aw: Writer.Allocating = .init(allocator); + const options = Options{ .prog = "test-prog", .writer = &aw.writer }; + + try testing.expectError(error.Help, parseSlice(struct { + named: struct { + str: []const u8, + int: i32, + flag: bool, + }, + }, allocator, &[_][]const u8{"--help"}, options)); + // Because the help output is primarily for humans, don't get too strict in the unit test. + // Only verify that we see the important stuff that should definitely be there somewhere, + // but otherwise allow maintainers to adjust the layout, formatting, notation, etc. without causing friction here. + try testing.expect(mem.indexOf(u8, aw.written(), "test-prog") != null); + try testing.expect(mem.indexOf(u8, aw.written(), "--str=string") != null); + try testing.expect(mem.indexOf(u8, aw.written(), "--int") != null); + try testing.expect(mem.indexOf(u8, aw.written(), "--[no-]flag") != null); + try testing.expect(mem.indexOf(u8, aw.written(), "--help") != null); + + aw.clearRetainingCapacity(); + try testing.expectError(error.Help, parseSlice(struct { + named: struct { + color: enum { never, auto, always } = .auto, + }, + }, allocator, &[_][]const u8{"--help"}, options)); + // All allowed values for an enum should be spelled out. + try testing.expect(mem.indexOf(u8, aw.written(), "--color") != null); + try testing.expect(mem.indexOf(u8, aw.written(), "never") != null); + try testing.expect(mem.indexOf(u8, aw.written(), "auto") != null); + try testing.expect(mem.indexOf(u8, aw.written(), "always") != null); + + // Test that arrays are represented differently from scalars somehow. + aw.clearRetainingCapacity(); + try testing.expectError(error.Help, parseSlice(struct { + named: struct { + name: []const u8, + }, + }, allocator, &[_][]const u8{"--help"}, options)); + const scalar_help = try aw.toOwnedSlice(); + try testing.expectError(error.Help, parseSlice(struct { + named: struct { + name: []const []const u8 = &.{}, + }, + }, allocator, &[_][]const u8{"--help"}, options)); + try testing.expect(!mem.eql(u8, scalar_help, aw.written())); + + // Default values should be rendered somehow. + aw.clearRetainingCapacity(); + try testing.expectError(error.Help, parseSlice(struct { + named: struct { + str: []const u8 = "hello", + int: i32 = 3, + f: f32 = 1.25, + }, + }, allocator, &[_][]const u8{"--help"}, options)); + try testing.expect(mem.indexOf(u8, aw.written(), "hello") != null); + try testing.expect(mem.indexOf(u8, aw.written(), "3") != null); + try testing.expect(mem.indexOf(u8, aw.written(), "1.25") != null); + + // Test that bool arguments express the default somehow. + aw.clearRetainingCapacity(); + try testing.expectError(error.Help, parseSlice(struct { + named: struct { + b: bool, + }, + }, allocator, &[_][]const u8{"--help"}, options)); + const bool_required_help = try aw.toOwnedSlice(); + try testing.expectError(error.Help, parseSlice(struct { + named: struct { + b: bool = true, + }, + }, allocator, &[_][]const u8{"--help"}, options)); + const default_true_help = try aw.toOwnedSlice(); + try testing.expectError(error.Help, parseSlice(struct { + named: struct { + b: bool = false, + }, + }, allocator, &[_][]const u8{"--help"}, options)); + const default_false_help = try aw.toOwnedSlice(); + try testing.expect(!mem.eql(u8, bool_required_help, default_true_help)); + try testing.expect(!mem.eql(u8, bool_required_help, default_false_help)); + try testing.expect(!mem.eql(u8, default_true_help, default_false_help)); + + // Test that enum arguments express the default somehow. + aw.clearRetainingCapacity(); + try testing.expectError(error.Help, parseSlice(struct { + named: struct { + color: enum { never, auto, always }, + }, + }, allocator, &[_][]const u8{"--help"}, options)); + const enum_required_help = try aw.toOwnedSlice(); + try testing.expectError(error.Help, parseSlice(struct { + named: struct { + color: enum { never, auto, always } = .auto, + }, + }, allocator, &[_][]const u8{"--help"}, options)); + const default_auto_help = try aw.toOwnedSlice(); + try testing.expectError(error.Help, parseSlice(struct { + named: struct { + color: enum { never, auto, always } = .never, + }, + }, allocator, &[_][]const u8{"--help"}, options)); + const default_never_help = try aw.toOwnedSlice(); + try testing.expect(!mem.eql(u8, enum_required_help, default_auto_help)); + try testing.expect(!mem.eql(u8, enum_required_help, default_never_help)); + try testing.expect(!mem.eql(u8, default_auto_help, default_never_help)); +} + +test "minimal" { + const Args = struct {}; + + var arena: std.heap.ArenaAllocator = .init(testing.allocator); + defer arena.deinit(); + _ = try parseSlice(Args, arena.allocator(), &[_][]const u8{}, .{}); +} + +test "manual deinit" { + const Args = struct { + named: struct { + str_arr: []const []const u8 = &.{}, + int_arr: []const i32 = &.{}, + empty_arr: []const []const u8 = &.{}, + }, + positional: struct { + args: []const []const u8 = &.{}, + }, + }; + + const args = try parseSlice(Args, testing.allocator, &[_][]const u8{ + "--str_arr=hello1", "--str_arr", "hello2", + "--int_arr=123456", "--int_arr", "789012", + "positional-12345", "--", "positi", + }, .{}); + + try testing.expectEqualDeep(Args{ + .named = .{ + .str_arr = &.{ "hello1", "hello2" }, + .int_arr = &.{ 123456, 789012 }, + }, + .positional = .{ + .args = &.{ "positional-12345", "positi" }, + }, + }, args); + + // Surgically cleanup memory. + testing.allocator.free(args.named.str_arr); + testing.allocator.free(args.named.int_arr); + testing.allocator.free(args.named.empty_arr); + testing.allocator.free(args.positional.args); + // Should be no memory leak errors now. +} + +test "actually calling error" { + const Args = struct { + named: struct { + output: []const u8 = "", + }, + positional: struct { + args: []const []const u8 = &.{}, + }, + }; + + var arena: std.heap.ArenaAllocator = .init(testing.allocator); + defer arena.deinit(); + const args = try parseSlice(Args, arena.allocator(), &[_][]const u8{ + "--output=/absolute/path", "too", "many", "other", "args", + }, .{}); + + try testing.expectEqual(error.Usage, @"error"(Args, "--output must not be absolute: {s}", .{args.named.output}, silent_options)); + try testing.expectEqual(error.Usage, @"error"(Args, "expected exactly 1 positional arg", .{}, silent_options)); +} + +test "custom help" { + var arena: std.heap.ArenaAllocator = .init(testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + var aw: Writer.Allocating = .init(allocator); + const options = Options{ .prog = "unused-prog", .writer = &aw.writer }; + + const Args = struct { + pub const help = + \\usage: the-zip-thing --output path [options] input.zip + \\ + \\arguments: + \\ --output path where to write the output stuff + \\ --[no-]force overwrite output if already exists + \\ input.zip the zip file to read + \\ --help print this help and exit + \\ + ; + named: struct { + output: []const u8, + force: bool = false, + }, + positional: struct { + args: []const []const u8 = &.{}, + }, + }; + try testing.expectError(error.Help, parseSlice(Args, allocator, &[_][]const u8{"--help"}, options)); + try testing.expectEqualStrings(Args.help, aw.written()); +} + +test "description" { + var arena: std.heap.ArenaAllocator = .init(testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + var aw: Writer.Allocating = .init(allocator); + const options = Options{ .prog = "unused-prog", .writer = &aw.writer }; + + const Args = struct { + pub const description = + \\This is a description + ; + }; + try testing.expectError(error.Help, parseSlice(Args, allocator, &[_][]const u8{"--help"}, options)); + try testing.expect(mem.indexOf(u8, aw.written(), Args.description) != null); +} + +test "field help" { + var arena: std.heap.ArenaAllocator = .init(testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + var aw: Writer.Allocating = .init(allocator); + const options = Options{ .prog = "unused-prog", .writer = &aw.writer }; + + const Args = struct { + named: struct { + output: []const u8, + pub const output_help = "help for output"; + }, + positional: struct { + args: []const []const u8 = &.{}, + pub const args_help = "help for args"; + }, + }; + try testing.expectError(error.Help, parseSlice(Args, allocator, &[_][]const u8{"--help"}, options)); + try testing.expect(mem.indexOf(u8, aw.written(), @FieldType(Args, "named").output_help) != null); + try testing.expect(mem.indexOf(u8, aw.written(), @FieldType(Args, "positional").args_help) != null); +} diff --git a/lib/std/std.zig b/lib/std/std.zig index 32c7b6d3995b..3b2d0f10d706 100644 --- a/lib/std/std.zig +++ b/lib/std/std.zig @@ -63,6 +63,7 @@ pub const base64 = @import("base64.zig"); pub const bit_set = @import("bit_set.zig"); pub const builtin = @import("builtin.zig"); pub const c = @import("c.zig"); +pub const cli = @import("cli.zig"); pub const coff = @import("coff.zig"); pub const compress = @import("compress.zig"); pub const static_string_map = @import("static_string_map.zig"); diff --git a/tools/docgen.zig b/tools/docgen.zig index 18311b0d5480..656d6f6ba3d2 100644 --- a/tools/docgen.zig +++ b/tools/docgen.zig @@ -16,16 +16,17 @@ const max_doc_file_size = 10 * 1024 * 1024; const obj_ext = builtin.object_format.fileExt(builtin.cpu.arch); -const usage = - \\Usage: docgen [options] input output - \\ - \\ Generates an HTML document from a docgen template. - \\ - \\Options: - \\ --code-dir dir Path to directory containing code example outputs - \\ -h, --help Print this help and exit - \\ -; +const Args = struct { + pub const description = "Generates an HTML document from a docgen template."; + named: struct { + @"code-dir": [:0]const u8, + pub const @"code-dir_help" = "Path to directory containing code example outputs"; + }, + positional: struct { + input: [:0]const u8, + output: [:0]const u8, + }, +}; pub fn main() !void { var arena_instance = std.heap.ArenaAllocator.init(std.heap.page_allocator); @@ -33,38 +34,10 @@ pub fn main() !void { const arena = arena_instance.allocator(); - var args_it = try process.argsWithAllocator(arena); - if (!args_it.skip()) @panic("expected self arg"); - - var opt_code_dir: ?[]const u8 = null; - var opt_input: ?[]const u8 = null; - var opt_output: ?[]const u8 = null; - - while (args_it.next()) |arg| { - if (mem.startsWith(u8, arg, "-")) { - if (mem.eql(u8, arg, "-h") or mem.eql(u8, arg, "--help")) { - try fs.File.stdout().writeAll(usage); - process.exit(0); - } else if (mem.eql(u8, arg, "--code-dir")) { - if (args_it.next()) |param| { - opt_code_dir = param; - } else { - fatal("expected parameter after --code-dir", .{}); - } - } else { - fatal("unrecognized option: '{s}'", .{arg}); - } - } else if (opt_input == null) { - opt_input = arg; - } else if (opt_output == null) { - opt_output = arg; - } else { - fatal("unexpected positional argument: '{s}'", .{arg}); - } - } - const input_path = opt_input orelse fatal("missing input file", .{}); - const output_path = opt_output orelse fatal("missing output file", .{}); - const code_dir_path = opt_code_dir orelse fatal("missing --code-dir argument", .{}); + const args = try std.cli.parse(Args, arena, .{}); + const input_path = args.positional.input; + const output_path = args.positional.output; + const code_dir_path = args.named.@"code-dir"; var in_file = try fs.cwd().openFile(input_path, .{}); defer in_file.close(); diff --git a/tools/dump-cov.zig b/tools/dump-cov.zig index 249783b92741..2d93a2580008 100644 --- a/tools/dump-cov.zig +++ b/tools/dump-cov.zig @@ -16,9 +16,15 @@ pub fn main() !void { defer arena_instance.deinit(); const arena = arena_instance.allocator(); - const args = try std.process.argsAlloc(arena); - const exe_file_name = args[1]; - const cov_file_name = args[2]; + const args = try std.cli.parse(struct { + named: struct {}, + positional: struct { + exe_file: [:0]const u8, + cov_file: [:0]const u8, + }, + }, arena, .{}); + const exe_file_name = args.positional.exe_file; + const cov_file_name = args.positional.cov_file; const exe_path: Path = .{ .root_dir = std.Build.Cache.Directory.cwd(), diff --git a/tools/fetch_them_macos_headers.zig b/tools/fetch_them_macos_headers.zig index 3c070e849ff3..b2e4a5172bff 100644 --- a/tools/fetch_them_macos_headers.zig +++ b/tools/fetch_them_macos_headers.zig @@ -55,36 +55,24 @@ const Target = struct { const headers_source_prefix: []const u8 = "headers"; -const usage = - \\fetch_them_macos_headers [options] [cc args] - \\ - \\Options: - \\ --sysroot Path to macOS SDK - \\ - \\General Options: - \\-h, --help Print this help and exit -; +const Args = struct { + named: struct { + sysroot: []const u8 = "", + pub const sysroot_help = "Path to macOS SDK"; + }, + positional: struct { + cc_args: []const [:0]const u8 = &.{}, + }, +}; pub fn main() anyerror!void { var arena = std.heap.ArenaAllocator.init(gpa); defer arena.deinit(); const allocator = arena.allocator(); - const args = try std.process.argsAlloc(allocator); - - var argv = std.array_list.Managed([]const u8).init(allocator); - var sysroot: ?[]const u8 = null; - - var args_iter = ArgsIterator{ .args = args[1..] }; - while (args_iter.next()) |arg| { - if (mem.eql(u8, arg, "--help") or mem.eql(u8, arg, "-h")) { - return info(usage, .{}); - } else if (mem.eql(u8, arg, "--sysroot")) { - sysroot = args_iter.nextOrFatal(); - } else try argv.append(arg); - } + const args = try std.cli.parse(Args, allocator, .{}); - const sysroot_path = sysroot orelse blk: { + const sysroot_path = if (args.named.sysroot.len > 0) args.named.sysroot else blk: { const target = try std.zig.system.resolveTargetQuery(.{}); break :blk std.zig.system.darwin.getSdk(allocator, &target) orelse fatal("no SDK found; you can provide one explicitly with '--sysroot' flag", .{}); @@ -121,13 +109,13 @@ pub fn main() anyerror!void { .arch = arch, .os_ver = os_ver, }; - try fetchTarget(allocator, argv.items, sysroot_path, target, version, tmp); + try fetchTarget(allocator, args.positional.cc_args, sysroot_path, target, version, tmp); } } fn fetchTarget( arena: Allocator, - args: []const []const u8, + cc_args: []const []const u8, sysroot: []const u8, target: Target, ver: Version, @@ -165,7 +153,7 @@ fn fetchTarget( "-MF", headers_list_path, }); - try cc_argv.appendSlice(args); + try cc_argv.appendSlice(cc_args); const res = try std.process.Child.run(.{ .allocator = arena, @@ -229,24 +217,6 @@ fn fetchTarget( } } -const ArgsIterator = struct { - args: []const []const u8, - i: usize = 0, - - fn next(it: *@This()) ?[]const u8 { - if (it.i >= it.args.len) { - return null; - } - defer it.i += 1; - return it.args[it.i]; - } - - fn nextOrFatal(it: *@This()) []const u8 { - const arg = it.next() orelse fatal("expected parameter after '{s}'", .{it.args[it.i - 1]}); - return arg; - } -}; - const Version = struct { major: u16, minor: u8, diff --git a/tools/gen_macos_headers_c.zig b/tools/gen_macos_headers_c.zig index f95023adb756..7d04032e41ff 100644 --- a/tools/gen_macos_headers_c.zig +++ b/tools/gen_macos_headers_c.zig @@ -8,32 +8,18 @@ const Allocator = std.mem.Allocator; var general_purpose_allocator = std.heap.GeneralPurposeAllocator(.{}){}; const gpa = general_purpose_allocator.allocator(); -const usage = - \\gen_macos_headers_c [dir] - \\ - \\General Options: - \\-h, --help Print this help and exit -; - pub fn main() anyerror!void { var arena_allocator = std.heap.ArenaAllocator.init(gpa); defer arena_allocator.deinit(); const arena = arena_allocator.allocator(); - const args = try std.process.argsAlloc(arena); - if (args.len == 1) fatal("no command or option specified", .{}); - - var positionals = std.array_list.Managed([]const u8).init(arena); - - for (args[1..]) |arg| { - if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) { - return info(usage, .{}); - } else try positionals.append(arg); - } - - if (positionals.items.len != 1) fatal("expected one positional argument: [dir]", .{}); + const args = try std.cli.parse(struct { + positional: struct { + dir: []const u8, + }, + }, arena, .{}); - var dir = try std.fs.cwd().openDir(positionals.items[0], .{ .no_follow = true }); + var dir = try std.fs.cwd().openDir(args.positional.dir, .{ .no_follow = true }); defer dir.close(); var paths = std.array_list.Managed([]const u8).init(arena); try findHeaders(arena, dir, "", &paths); diff --git a/tools/gen_outline_atomics.zig b/tools/gen_outline_atomics.zig index 1dade6661019..a5d379d477c9 100644 --- a/tools/gen_outline_atomics.zig +++ b/tools/gen_outline_atomics.zig @@ -15,7 +15,7 @@ pub fn main() !void { defer arena_instance.deinit(); const arena = arena_instance.allocator(); - //const args = try std.process.argsAlloc(arena); + _ = try std.cli.parse(struct {}, arena, .{}); var stdout_buffer: [2000]u8 = undefined; var stdout_writer = std.fs.File.stdout().writerStreaming(&stdout_buffer); diff --git a/tools/gen_spirv_spec.zig b/tools/gen_spirv_spec.zig index 702866824c49..1f481ae5ed70 100644 --- a/tools/gen_spirv_spec.zig +++ b/tools/gen_spirv_spec.zig @@ -58,12 +58,20 @@ const allocator = arena.allocator(); pub fn main() !void { defer arena.deinit(); - const args = try std.process.argsAlloc(allocator); - if (args.len != 3) { - usageAndExit(args[0], 1); - } - - const json_path = try std.fs.path.join(allocator, &.{ args[1], "include/spirv/unified1/" }); + const args = try std.cli.parse(struct { + pub const description = + \\Generates Zig bindings for SPIR-V specifications found in the SPIRV-Headers + \\repository. The result, printed to stdout, should be used to update + \\files in src/codegen/spirv. Don't forget to format the output. + ; + positional: struct { + pub const @"path/to/SPIRV-Headers_help" = "should point to a clone of https://github.com/KhronosGroup/SPIRV-Headers/"; + @"path/to/SPIRV-Headers": [:0]const u8, + @"path/to/zig/src/codegen/spirv/extinst.zig.grammar.json": [:0]const u8, + }, + }, allocator, .{}); + + const json_path = try std.fs.path.join(allocator, &.{ args.positional.@"path/to/SPIRV-Headers", "include/spirv/unified1/" }); const dir = try std.fs.cwd().openDir(json_path, .{ .iterate = true }); const core_spec = try readRegistry(CoreRegistry, dir, "spirv.core.grammar.json"); @@ -80,7 +88,7 @@ pub fn main() !void { try readExtRegistry(&exts, dir, entry.name); } - try readExtRegistry(&exts, std.fs.cwd(), args[2]); + try readExtRegistry(&exts, std.fs.cwd(), args.positional.@"path/to/zig/src/codegen/spirv/extinst.zig.grammar.json"); var allocating: std.Io.Writer.Allocating = .init(allocator); defer allocating.deinit(); @@ -929,19 +937,3 @@ fn parseHexInt(text: []const u8) !u31 { return error.InvalidHexInt; return try std.fmt.parseInt(u31, text[prefix.len..], 16); } - -fn usageAndExit(arg0: []const u8, code: u8) noreturn { - const stderr = std.debug.lockStderrWriter(&.{}); - stderr.print( - \\Usage: {s} - \\ - \\Generates Zig bindings for SPIR-V specifications found in the SPIRV-Headers - \\repository. The result, printed to stdout, should be used to update - \\files in src/codegen/spirv. Don't forget to format the output. - \\ - \\ should point to a clone of - \\https://github.com/KhronosGroup/SPIRV-Headers/ - \\ - , .{arg0}) catch std.process.exit(1); - std.process.exit(code); -} diff --git a/tools/gen_stubs.zig b/tools/gen_stubs.zig index 1aadda232d6f..5ad6be25acb6 100644 --- a/tools/gen_stubs.zig +++ b/tools/gen_stubs.zig @@ -284,8 +284,12 @@ pub fn main() !void { defer arena_instance.deinit(); const arena = arena_instance.allocator(); - const args = try std.process.argsAlloc(arena); - const build_all_path = args[1]; + const args = try std.cli.parse(struct { + positional: struct { + build_all_path: [:0]const u8, + }, + }, arena, .{}); + const build_all_path = args.positional.build_all_path; var build_all_dir = try std.fs.cwd().openDir(build_all_path, .{}); diff --git a/tools/generate_JSONTestSuite.zig b/tools/generate_JSONTestSuite.zig index 2c6fee5bdd5e..c8e9c3e3297f 100644 --- a/tools/generate_JSONTestSuite.zig +++ b/tools/generate_JSONTestSuite.zig @@ -1,4 +1,6 @@ -// zig run this file inside the test_parsing/ directory of this repo: https://github.com/nst/JSONTestSuite +const Args = struct { + pub const description = "zig run this file inside the test_parsing/ directory of this repo: https://github.com/nst/JSONTestSuite"; +}; const std = @import("std"); @@ -6,6 +8,8 @@ pub fn main() !void { var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init; var allocator = gpa.allocator(); + _ = try std.cli.parse(Args, allocator, .{}); + var stdout_buffer: [2000]u8 = undefined; var stdout_writer = std.fs.File.stdout().writerStreaming(&stdout_buffer); const output = &stdout_writer.interface; diff --git a/tools/generate_c_size_and_align_checks.zig b/tools/generate_c_size_and_align_checks.zig index 8c278407e483..c833dbdf9519 100644 --- a/tools/generate_c_size_and_align_checks.zig +++ b/tools/generate_c_size_and_align_checks.zig @@ -25,21 +25,22 @@ fn cName(ty: std.Target.CType) []const u8 { }; } -var general_purpose_allocator: std.heap.GeneralPurposeAllocator(.{}) = .init; - pub fn main() !void { - const gpa = general_purpose_allocator.allocator(); + var general_purpose_allocator: std.heap.GeneralPurposeAllocator(.{}) = .init; defer std.debug.assert(general_purpose_allocator.deinit() == .ok); + const gpa = general_purpose_allocator.allocator(); - const args = try std.process.argsAlloc(gpa); - defer std.process.argsFree(gpa, args); + var arena_instance = std.heap.ArenaAllocator.init(gpa); + defer arena_instance.deinit(); + const arena = arena_instance.allocator(); - if (args.len != 2) { - std.debug.print("Usage: {s} [target_triple]\n", .{args[0]}); - std.process.exit(1); - } + const args = try std.cli.parse(struct { + positional: struct { + target_triple: [:0]const u8, + }, + }, arena, .{}); - const query = try std.Target.Query.parse(.{ .arch_os_abi = args[1] }); + const query = try std.Target.Query.parse(.{ .arch_os_abi = args.positional.target_triple }); const target = try std.zig.system.resolveTargetQuery(query); var buffer: [2000]u8 = undefined; diff --git a/tools/generate_linux_syscalls.zig b/tools/generate_linux_syscalls.zig index 2705618a7dbb..18e216d5595e 100644 --- a/tools/generate_linux_syscalls.zig +++ b/tools/generate_linux_syscalls.zig @@ -11,6 +11,16 @@ //! //! Everything after `name` is ignored for the purposes of this tool. +const Args = struct { + pub const description = + \\Generates the list of Linux syscalls for each supported cpu arch, using the Linux development tree. + \\Prints to stdout Zig code which you can use to replace the file lib/std/os/linux/syscalls.zig. + ; + positional: struct { + @"/path/to/linux": [:0]const u8, + }, +}; + const std = @import("std"); const Io = std.Io; const mem = std.mem; @@ -175,12 +185,8 @@ pub fn main() !void { defer arena.deinit(); const gpa = arena.allocator(); - const args = try std.process.argsAlloc(gpa); - if (args.len < 2 or mem.eql(u8, args[1], "--help")) { - usage(std.debug.lockStderrWriter(&.{}), args[0]) catch std.process.exit(2); - std.process.exit(1); - } - const linux_path = args[1]; + const args = try std.cli.parse(Args, gpa, .{}); + const linux_path = args.positional.@"/path/to/linux"; var stdout_buffer: [2048]u8 = undefined; var stdout_writer = std.fs.File.stdout().writerStreaming(&stdout_buffer); @@ -247,14 +253,3 @@ pub fn main() !void { try Io.Writer.flush(stdout); } - -fn usage(w: *std.Io.Writer, arg0: []const u8) std.Io.Writer.Error!void { - try w.print( - \\Usage: {s} /path/to/zig /path/to/linux - \\Alternative Usage: zig run /path/to/git/zig/tools/generate_linux_syscalls.zig -- /path/to/zig /path/to/linux - \\ - \\Generates the list of Linux syscalls for each supported cpu arch, using the Linux development tree. - \\Prints to stdout Zig code which you can use to replace the file lib/std/os/linux/syscalls.zig. - \\ - , .{arg0}); -} diff --git a/tools/incr-check.zig b/tools/incr-check.zig index ed7443046a11..074f124df440 100644 --- a/tools/incr-check.zig +++ b/tools/incr-check.zig @@ -2,8 +2,6 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const Cache = std.Build.Cache; -const usage = "usage: incr-check [--zig-lib-dir lib] [--debug-zcu] [--debug-dwarf] [--debug-link] [--preserve-tmp] [--zig-cc-binary /path/to/zig]"; - pub fn main() !void { const fatal = std.process.fatal; @@ -11,46 +9,29 @@ pub fn main() !void { defer arena_instance.deinit(); const arena = arena_instance.allocator(); - var opt_zig_exe: ?[]const u8 = null; - var opt_input_file_name: ?[]const u8 = null; - var opt_lib_dir: ?[]const u8 = null; - var opt_cc_zig: ?[]const u8 = null; - var debug_zcu = false; - var debug_dwarf = false; - var debug_link = false; - var preserve_tmp = false; - - var arg_it = try std.process.argsWithAllocator(arena); - _ = arg_it.skip(); - while (arg_it.next()) |arg| { - if (arg.len > 0 and arg[0] == '-') { - if (std.mem.eql(u8, arg, "--zig-lib-dir")) { - opt_lib_dir = arg_it.next() orelse fatal("expected arg after '--zig-lib-dir'\n{s}", .{usage}); - } else if (std.mem.eql(u8, arg, "--debug-zcu")) { - debug_zcu = true; - } else if (std.mem.eql(u8, arg, "--debug-dwarf")) { - debug_dwarf = true; - } else if (std.mem.eql(u8, arg, "--debug-link")) { - debug_link = true; - } else if (std.mem.eql(u8, arg, "--preserve-tmp")) { - preserve_tmp = true; - } else if (std.mem.eql(u8, arg, "--zig-cc-binary")) { - opt_cc_zig = arg_it.next() orelse fatal("expect arg after '--zig-cc-binary'\n{s}", .{usage}); - } else { - fatal("unknown option '{s}'\n{s}", .{ arg, usage }); - } - continue; - } - if (opt_zig_exe == null) { - opt_zig_exe = arg; - } else if (opt_input_file_name == null) { - opt_input_file_name = arg; - } else { - fatal("unknown argument '{s}'\n{s}", .{ arg, usage }); - } - } - const zig_exe = opt_zig_exe orelse fatal("missing path to zig\n{s}", .{usage}); - const input_file_name = opt_input_file_name orelse fatal("missing input file\n{s}", .{usage}); + const args = try std.cli.parse(struct { + positional: struct { + @"zig-binary-path": []const u8, + @"input-file": []const u8, + }, + named: struct { + @"zig-lib-dir": []const u8 = "", + @"debug-zcu": bool = false, + @"debug-dwarf": bool = false, + @"debug-link": bool = false, + preserve_tmp: bool = false, + @"zig-cc-binary": []const u8 = "", + }, + }, arena, .{}); + + const opt_lib_dir: ?[]const u8 = if (args.named.@"zig-lib-dir".len > 0) args.named.@"zig-lib-dir" else null; + const opt_cc_zig: ?[]const u8 = if (args.named.@"zig-cc-binary".len > 0) args.named.@"zig-cc-binary" else null; + const debug_zcu = args.named.@"debug-zcu"; + const debug_dwarf = args.named.@"debug-dwarf"; + const debug_link = args.named.@"debug-link"; + const preserve_tmp = args.named.preserve_tmp; + const zig_exe = args.positional.@"zig-binary-path"; + const input_file_name = args.positional.@"input-file"; const input_file_bytes = try std.fs.cwd().readFileAlloc(input_file_name, arena, .limited(std.math.maxInt(u32))); const case = try Case.parse(arena, input_file_bytes); diff --git a/tools/migrate_langref.zig b/tools/migrate_langref.zig index d880db1c2575..77ffd717fb79 100644 --- a/tools/migrate_langref.zig +++ b/tools/migrate_langref.zig @@ -13,9 +13,14 @@ pub fn main() !void { defer arena_instance.deinit(); const arena = arena_instance.allocator(); - const args = try std.process.argsAlloc(arena); - const input_file = args[1]; - const output_file = args[2]; + const args = try std.cli.parse(struct { + positional: struct { + input_file: [:0]const u8, + output_file: [:0]const u8, + }, + }, arena, .{}); + const input_file = args.positional.input_file; + const output_file = args.positional.output_file; var in_file = try fs.cwd().openFile(input_file, .{ .mode = .read_only }); defer in_file.close(); diff --git a/tools/process_headers.zig b/tools/process_headers.zig index eb671193bb0d..9e8b5e178ba4 100644 --- a/tools/process_headers.zig +++ b/tools/process_headers.zig @@ -119,52 +119,24 @@ const HashToContents = std.StringHashMap(Contents); const TargetToHash = std.StringArrayHashMap([]const u8); const PathTable = std.StringHashMap(*TargetToHash); -const LibCVendor = enum { - musl, - glibc, - freebsd, - netbsd, -}; - pub fn main() !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); const allocator = arena.allocator(); - const args = try std.process.argsAlloc(allocator); - var search_paths = std.array_list.Managed([]const u8).init(allocator); - var opt_out_dir: ?[]const u8 = null; - var opt_abi: ?[]const u8 = null; - var arg_i: usize = 1; - while (arg_i < args.len) : (arg_i += 1) { - if (std.mem.eql(u8, args[arg_i], "--help")) - usageAndExit(args[0]); - if (arg_i + 1 >= args.len) { - std.debug.print("expected argument after '{s}'\n", .{args[arg_i]}); - usageAndExit(args[0]); - } + const args = try std.cli.parse(struct { + named: struct { + @"search-path": []const []const u8 = &.{}, + out: []const u8, + abi: enum { musl, glibc, freebsd, netbsd }, - if (std.mem.eql(u8, args[arg_i], "--search-path")) { - try search_paths.append(args[arg_i + 1]); - } else if (std.mem.eql(u8, args[arg_i], "--out")) { - assert(opt_out_dir == null); - opt_out_dir = args[arg_i + 1]; - } else if (std.mem.eql(u8, args[arg_i], "--abi")) { - assert(opt_abi == null); - opt_abi = args[arg_i + 1]; - } else { - std.debug.print("unrecognized argument: {s}\n", .{args[arg_i]}); - usageAndExit(args[0]); - } - - arg_i += 1; - } - - const out_dir = opt_out_dir orelse usageAndExit(args[0]); - const abi_name = opt_abi orelse usageAndExit(args[0]); - const vendor = std.meta.stringToEnum(LibCVendor, abi_name) orelse { - std.debug.print("unrecognized C ABI: {s}\n", .{abi_name}); - usageAndExit(args[0]); - }; + pub const @"search-path_help" = "subdirectories of search paths look like, e.g. x86_64-linux-gnu"; + pub const out_help = "a dir that will be created, and populated with the results"; + }, + }, allocator, .{}); + const search_paths = args.named.@"search-path"; + const out_dir = args.named.out; + const vendor = args.named.abi; + const abi_name = @tagName(vendor); const generic_name = try std.fmt.allocPrint(allocator, "generic-{s}", .{abi_name}); const libc_targets = switch (vendor) { @@ -225,7 +197,7 @@ pub fn main() !void { @tagName(libc_target.abi), }); - search: for (search_paths.items) |search_path| { + search: for (search_paths) |search_path| { const sub_path = switch (vendor) { .glibc, .freebsd, @@ -362,12 +334,3 @@ pub fn main() !void { } } } - -fn usageAndExit(arg0: []const u8) noreturn { - std.debug.print("Usage: {s} [--search-path ] --out --abi \n", .{arg0}); - std.debug.print("--search-path can be used any number of times.\n", .{}); - std.debug.print(" subdirectories of search paths look like, e.g. x86_64-linux-gnu\n", .{}); - std.debug.print("--out is a dir that will be created, and populated with the results\n", .{}); - std.debug.print("--abi is either glibc, musl, freebsd, or netbsd\n", .{}); - std.process.exit(1); -} diff --git a/tools/update-linux-headers.zig b/tools/update-linux-headers.zig index bf9edf0753c2..99df00760ab4 100644 --- a/tools/update-linux-headers.zig +++ b/tools/update-linux-headers.zig @@ -142,33 +142,18 @@ const PathTable = std.StringHashMap(*TargetToHash); pub fn main() !void { var arena_state = std.heap.ArenaAllocator.init(std.heap.page_allocator); const arena = arena_state.allocator(); - const args = try std.process.argsAlloc(arena); - var search_paths = std.array_list.Managed([]const u8).init(arena); - var opt_out_dir: ?[]const u8 = null; - var arg_i: usize = 1; - while (arg_i < args.len) : (arg_i += 1) { - if (std.mem.eql(u8, args[arg_i], "--help")) - usageAndExit(args[0]); - if (arg_i + 1 >= args.len) { - std.debug.print("expected argument after '{s}'\n", .{args[arg_i]}); - usageAndExit(args[0]); - } - - if (std.mem.eql(u8, args[arg_i], "--search-path")) { - try search_paths.append(args[arg_i + 1]); - } else if (std.mem.eql(u8, args[arg_i], "--out")) { - assert(opt_out_dir == null); - opt_out_dir = args[arg_i + 1]; - } else { - std.debug.print("unrecognized argument: {s}\n", .{args[arg_i]}); - usageAndExit(args[0]); - } + const args = try std.cli.parse(struct { + named: struct { + @"search-path": []const []const u8 = &.{}, + out: []const u8, - arg_i += 1; - } - - const out_dir = opt_out_dir orelse usageAndExit(args[0]); + pub const @"search-path_help" = "subdirectories of search paths look like, e.g. x86_64-linux-gnu"; + pub const out_help = "a dir that will be created, and populated with the results"; + }, + }, arena, .{}); + const search_paths = args.named.@"search-path"; + const out_dir = args.named.out; const generic_name = "any-linux-any"; var path_table = PathTable.init(arena); @@ -182,7 +167,7 @@ pub fn main() !void { const dest_target = DestTarget{ .arch = linux_target.arch, }; - search: for (search_paths.items) |search_path| { + search: for (search_paths) |search_path| { const target_include_dir = try std.fs.path.join(arena, &.{ search_path, linux_target.name, "include", }); @@ -320,11 +305,3 @@ pub fn main() !void { try std.fs.cwd().deleteFile(full_path); } } - -fn usageAndExit(arg0: []const u8) noreturn { - std.debug.print("Usage: {s} [--search-path ] --out --abi \n", .{arg0}); - std.debug.print("--search-path can be used any number of times.\n", .{}); - std.debug.print(" subdirectories of search paths look like, e.g. x86_64-linux-gnu\n", .{}); - std.debug.print("--out is a dir that will be created, and populated with the results\n", .{}); - std.process.exit(1); -} diff --git a/tools/update_clang_options.zig b/tools/update_clang_options.zig index 9b054478a246..e40e55bc7406 100644 --- a/tools/update_clang_options.zig +++ b/tools/update_clang_options.zig @@ -630,29 +630,22 @@ const cpu_targets = struct { pub fn main() anyerror!void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); - const allocator = arena.allocator(); - const args = try std.process.argsAlloc(allocator); var stdout_buffer: [4000]u8 = undefined; var stdout_writer = fs.File.stdout().writerStreaming(&stdout_buffer); const stdout = &stdout_writer.interface; - if (args.len <= 1) printUsageAndExit(args[0]); - - if (std.mem.eql(u8, args[1], "--help")) { - printUsage(stdout, args[0]) catch std.process.exit(2); - stdout.flush() catch std.process.exit(2); - std.process.exit(0); - } - - if (args.len < 3) printUsageAndExit(args[0]); - - const llvm_tblgen_exe = args[1]; - if (std.mem.startsWith(u8, llvm_tblgen_exe, "-")) printUsageAndExit(args[0]); + const args = try std.cli.parse(struct { + pub const description = "Prints to stdout Zig code which you can use to replace the file src/clang_options_data.zig."; + positional: struct { + @"/path/to/llvm-tblgen": [:0]const u8, + @"/path/to/git/llvm/llvm-project": [:0]const u8, + }, + }, allocator, .{}); - const llvm_src_root = args[2]; - if (std.mem.startsWith(u8, llvm_src_root, "-")) printUsageAndExit(args[0]); + const llvm_tblgen_exe = args.positional.@"/path/to/llvm-tblgen"; + const llvm_src_root = args.positional.@"/path/to/git/llvm/llvm-project"; var llvm_to_zig_cpu_features = std.StringHashMap([]const u8).init(allocator); @@ -959,18 +952,3 @@ fn objectLessThan(context: void, a: *json.ObjectMap, b: *json.ObjectMap) bool { const b_key = b.get("!name").?.string; return std.mem.lessThan(u8, a_key, b_key); } - -fn printUsageAndExit(arg0: []const u8) noreturn { - printUsage(std.debug.lockStderrWriter(&.{}), arg0) catch std.process.exit(2); - std.process.exit(1); -} - -fn printUsage(w: *std.Io.Writer, arg0: []const u8) std.Io.Writer.Error!void { - try w.print( - \\Usage: {s} /path/to/llvm-tblgen /path/to/git/llvm/llvm-project - \\Alternative Usage: zig run /path/to/git/zig/tools/update_clang_options.zig -- /path/to/llvm-tblgen /path/to/git/llvm/llvm-project - \\ - \\Prints to stdout Zig code which you can use to replace the file src/clang_options_data.zig. - \\ - , .{arg0}); -} diff --git a/tools/update_cpu_features.zig b/tools/update_cpu_features.zig index b6e0d6495ec3..9dfd74080164 100644 --- a/tools/update_cpu_features.zig +++ b/tools/update_cpu_features.zig @@ -1567,38 +1567,23 @@ pub fn main() anyerror!void { defer arena_state.deinit(); const arena = arena_state.allocator(); - var args = try std.process.argsWithAllocator(arena); - const args0 = args.next().?; - - const llvm_tblgen_exe = args.next() orelse - usageAndExit(args0, 1); - - if (std.mem.eql(u8, llvm_tblgen_exe, "--help")) { - usageAndExit(args0, 0); - } - if (std.mem.startsWith(u8, llvm_tblgen_exe, "-")) { - usageAndExit(args0, 1); - } - - const llvm_src_root = args.next() orelse - usageAndExit(args0, 1); - - if (std.mem.startsWith(u8, llvm_src_root, "-")) { - usageAndExit(args0, 1); - } - - const zig_src_root = args.next() orelse - usageAndExit(args0, 1); - - if (std.mem.startsWith(u8, zig_src_root, "-")) { - usageAndExit(args0, 1); - } - - var filter: ?[]const u8 = null; - if (args.next()) |arg| filter = arg; - - // there shouldn't be any more argument after the optional filter - if (args.skip()) usageAndExit(args0, 1); + const args = try std.cli.parse(struct { + pub const description = + \\Updates lib/std/target/.zig from llvm/lib/Target//.td . + \\ + \\On a less beefy system, or when debugging, compile with -fsingle-threaded. + ; + positional: struct { + @"/path/to/llvm-tblgen": [:0]const u8, + @"/path/git/llvm-project": [:0]const u8, + @"/path/git/zig": [:0]const u8, + zig_name_filter: []const u8 = "", + }, + }, arena, .{}); + const llvm_tblgen_exe = args.positional.@"/path/to/llvm-tblgen"; + const llvm_src_root = args.positional.@"/path/git/llvm-project"; + const zig_src_root = args.positional.@"/path/git/zig"; + const filter: ?[]const u8 = if (args.positional.zig_name_filter.len > 0) args.positional.zig_name_filter else null; var zig_src_dir = try fs.cwd().openDir(zig_src_root, .{}); defer zig_src_dir.close(); @@ -2104,19 +2089,6 @@ fn processOneTarget(job: Job) void { render_progress.end(); } -fn usageAndExit(arg0: []const u8, code: u8) noreturn { - const stderr = std.debug.lockStderrWriter(&.{}); - stderr.print( - \\Usage: {s} /path/to/llvm-tblgen /path/git/llvm-project /path/git/zig [zig_name filter] - \\ - \\Updates lib/std/target/.zig from llvm/lib/Target//.td . - \\ - \\On a less beefy system, or when debugging, compile with -fsingle-threaded. - \\ - , .{arg0}) catch std.process.exit(1); - std.process.exit(code); -} - fn featureLessThan(_: void, a: Feature, b: Feature) bool { return std.ascii.lessThanIgnoreCase(a.zig_name, b.zig_name); } diff --git a/tools/update_crc_catalog.zig b/tools/update_crc_catalog.zig index a973a1b75a52..a25f23a3f210 100644 --- a/tools/update_crc_catalog.zig +++ b/tools/update_crc_catalog.zig @@ -10,11 +10,12 @@ pub fn main() anyerror!void { defer arena_state.deinit(); const arena = arena_state.allocator(); - const args = try std.process.argsAlloc(arena); - if (args.len <= 1) printUsageAndExit(args[0]); - - const zig_src_root = args[1]; - if (mem.startsWith(u8, zig_src_root, "-")) printUsageAndExit(args[0]); + const args = try std.cli.parse(struct { + positional: struct { + @"/path/git/zig": [:0]const u8, + }, + }, arena, .{}); + const zig_src_root = args.positional.@"/path/git/zig"; var zig_src_dir = try fs.cwd().openDir(zig_src_root, .{}); defer zig_src_dir.close(); @@ -188,15 +189,3 @@ pub fn main() anyerror!void { try code_writer.flush(); try test_writer.flush(); } - -fn printUsageAndExit(arg0: []const u8) noreturn { - printUsage(std.debug.lockStderrWriter(&.{}), arg0) catch std.process.exit(2); - std.process.exit(1); -} - -fn printUsage(w: *std.Io.Writer, arg0: []const u8) std.Io.Writer.Error!void { - return w.print( - \\Usage: {s} /path/git/zig - \\ - , .{arg0}); -} diff --git a/tools/update_freebsd_libc.zig b/tools/update_freebsd_libc.zig index 420147e0dfb4..9cb972d63247 100644 --- a/tools/update_freebsd_libc.zig +++ b/tools/update_freebsd_libc.zig @@ -16,9 +16,14 @@ pub fn main() !void { defer arena_instance.deinit(); const arena = arena_instance.allocator(); - const args = try std.process.argsAlloc(arena); - const freebsd_src_path = args[1]; - const zig_src_path = args[2]; + const args = try std.cli.parse(struct { + positional: struct { + freebsd_src_path: [:0]const u8, + zig_src_path: [:0]const u8, + }, + }, arena, .{}); + const freebsd_src_path = args.positional.freebsd_src_path; + const zig_src_path = args.positional.zig_src_path; const dest_dir_path = try std.fmt.allocPrint(arena, "{s}/lib/libc/freebsd", .{zig_src_path}); diff --git a/tools/update_glibc.zig b/tools/update_glibc.zig index 297c5c65b562..315db83a8a3b 100644 --- a/tools/update_glibc.zig +++ b/tools/update_glibc.zig @@ -41,9 +41,14 @@ pub fn main() !void { defer arena_instance.deinit(); const arena = arena_instance.allocator(); - const args = try std.process.argsAlloc(arena); - const glibc_src_path = args[1]; - const zig_src_path = args[2]; + const args = try std.cli.parse(struct { + positional: struct { + glibc_src_path: [:0]const u8, + zig_src_path: [:0]const u8, + }, + }, arena, .{}); + const glibc_src_path = args.positional.glibc_src_path; + const zig_src_path = args.positional.zig_src_path; const dest_dir_path = try std.fmt.allocPrint(arena, "{s}/lib/libc/glibc", .{zig_src_path}); diff --git a/tools/update_mingw.zig b/tools/update_mingw.zig index 9c67b27375bb..dbae3756e614 100644 --- a/tools/update_mingw.zig +++ b/tools/update_mingw.zig @@ -5,9 +5,14 @@ pub fn main() !void { defer arena_instance.deinit(); const arena = arena_instance.allocator(); - const args = try std.process.argsAlloc(arena); - const zig_src_lib_path = args[1]; - const mingw_src_path = args[2]; + const args = try std.cli.parse(struct { + positional: struct { + zig_src_lib_path: [:0]const u8, + mingw_src_path: [:0]const u8, + }, + }, arena, .{}); + const zig_src_lib_path = args.positional.zig_src_lib_path; + const mingw_src_path = args.positional.mingw_src_path; const dest_mingw_crt_path = try std.fs.path.join(arena, &.{ zig_src_lib_path, "libc", "mingw", diff --git a/tools/update_netbsd_libc.zig b/tools/update_netbsd_libc.zig index 7bfe99b09494..1cd04f1a88f2 100644 --- a/tools/update_netbsd_libc.zig +++ b/tools/update_netbsd_libc.zig @@ -16,9 +16,14 @@ pub fn main() !void { defer arena_instance.deinit(); const arena = arena_instance.allocator(); - const args = try std.process.argsAlloc(arena); - const netbsd_src_path = args[1]; - const zig_src_path = args[2]; + const args = try std.cli.parse(struct { + positional: struct { + netbsd_src_path: [:0]const u8, + zig_src_path: [:0]const u8, + }, + }, arena, .{}); + const netbsd_src_path = args.positional.netbsd_src_path; + const zig_src_path = args.positional.zig_src_path; const dest_dir_path = try std.fmt.allocPrint(arena, "{s}/lib/libc/netbsd", .{zig_src_path});