From 622264b02b222db72eddcb616f2dd1f0fb3c8720 Mon Sep 17 00:00:00 2001 From: Josh Wolfe Date: Fri, 15 Aug 2025 14:42:58 -0400 Subject: [PATCH 01/13] [std] add minimal CLI parser `std.cli` --- lib/std/cli.zig | 1155 +++++++++++++++++++++++++++++++++++++++++++++++ lib/std/std.zig | 1 + 2 files changed, 1156 insertions(+) create mode 100644 lib/std/cli.zig diff --git a/lib/std/cli.zig b/lib/std/cli.zig new file mode 100644 index 000000000000..83e832745e31 --- /dev/null +++ b/lib/std/cli.zig @@ -0,0 +1,1155 @@ +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 mem = std.mem; +const Allocator = mem.Allocator; + +pub const Options = struct { + /// When returning error.Usage, print a short error message to this writer, defaults to stderr. + /// When returning error.Help, print the long help documentation to this writer, defaults 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, +}; + +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. + Usage, + /// The --help argument was given. + 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 { +/// named: struct { +/// // ... +/// }, +/// positional: []const []const u8 = &.{}, +/// }; +/// ``` +/// The `named` and `positional` fields are required, although `named` need not have any subfields. +/// `positional` may instead have type `[]const [:0]const u8`. +/// +/// 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 returns `error.Help`. +/// Form (6) signals that all following arg strings are positional. +/// Form (7) and all arg strings following form (6) are appended into the `positional` array in order. +/// +/// 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. +/// +/// 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 { +/// // [...] +/// }, +/// positional: []const []const u8 = &.{}, +/// }; +/// ``` +/// +/// 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. +/// +/// 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!Args { + var iter: ArgIterator = try .initWithAllocator(arena); + // Do not call iter.deinit(). It holds the string data returned in the Args. + return parseIter(Args, arena, &iter, options); +} + +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, + /// --seed=0x is actually passed in by the `zig test` system (as of 0.14.1), which we receive here. + seed: u32 = 0, + }, + /// Receives the rest of the arguments. + positional: []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 `[:0]const u8` 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. +/// +/// 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` as well as freeing `args.positional`. +pub fn parseIter(comptime Args: type, arena: Allocator, iter: anytype, options: Options) Error!Args { + const prog = options.prog orelse if (iter.next()) |arg0| std.fs.path.basename(arg0) else ""; + return innerParse(Args, arena, iter, prog, options.writer); +} + +/// 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 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. +/// +/// 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` as well as freeing `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); +} + +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: []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 = &.{ "positional1", "positional2", "-12345678", "--positional4", "--positional=5" }, + }, args); +} + +fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: []const u8, writer: ?*Writer) Error!Args { + // arg0 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. + comptime checkArgsType(Args); + + var result: Args = undefined; + var positional: ArrayList(@typeInfo(@TypeOf(result.positional)).pointer.child) = .{}; + + const ArgsNamed = @TypeOf(result.named); + const named_info = @typeInfo(ArgsNamed).@"struct"; + + // Declare and initialize an ArrayList(C) for every []const C field (other than u8). + var fields_seen = [_]bool{false} ** named_info.fields.len; + comptime var array_list_fields: []const std.builtin.Type.StructField = &.{}; + inline for (named_info.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 std.builtin.Type.StructField, &.{.{ + .name = field.name, + .type = ArrayList(info.pointer.child), + .default_value_ptr = null, + .is_comptime = false, + .alignment = @alignOf(ArrayList(info.pointer.child)), + }}); + } + } + } + var array_lists: @Type(.{ .@"struct" = .{ .layout = .auto, .fields = array_list_fields, .decls = &.{}, .is_tuple = false } }) = undefined; + inline for (@typeInfo(@TypeOf(array_lists)).@"struct".fields) |field| { + @field(array_lists, field.name) = .{}; + } + + while (iter.next()) |arg| { + if (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(writer, prog, named_info); + } + return error.Help; + } + + if (arg.len >= 2 and arg[0] == '-' and isAlphabetic(arg[1])) { + // Always invalid. + // Examples: -h, -flag, -I/path + return usageError(writer, "unrecognized argument: {s}", .{arg}); + } + if (mem.eql(u8, arg, "--")) { + // Stop recognizing named arguments. Everything else is positional. + while (iter.next()) |arg2| { + try positional.append(allocator, arg2); // To resolve compile errors between `[:0]const u8` and `[]const u8` on this line, ensure the passed-in args are `[:0]const u8`. + } + break; + } + if (!(arg.len >= 3 and arg[0] == '-' and arg[1] == '-')) { + // Positional. + // Examples: "", "a", "-", "-1", + try positional.append(allocator, arg); + 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_info.fields, 0..) |field, i| { + if (mem.eql(u8, field.name, arg_name)) { + if (field.type == bool) { + if (immediate_value != null) return usageError(writer, "cannot specify value for bool argument: {s}", .{arg}); + @field(result.named, field.name) = !no_prefixed; + fields_seen[i] = true; + break; + } + if (no_prefixed) return usageError(writer, "unrecognized argument: {s}", .{arg}); + + // All other argument types require a value. + const arg_value = immediate_value orelse iter.next() orelse return usageError(writer, "expected argument after --{s}", .{field.name}); + + switch (@typeInfo(field.type)) { + .bool => unreachable, // Handled above. + .float => { + @field(result.named, field.name) = std.fmt.parseFloat(field.type, arg_value) catch |err| { + return usageError(writer, "unable to parse --{s}={s}: {s}", .{ field.name, arg_value, @errorName(err) }); + }; + }, + .int => { + @field(result.named, field.name) = std.fmt.parseInt(field.type, arg_value, 0) catch |err| { + return usageError(writer, "unable to parse --{s}={s}: {s}", .{ field.name, arg_value, @errorName(err) }); + }; + }, + .@"enum" => { + @field(result.named, field.name) = std.meta.stringToEnum(field.type, arg_value) orelse { + return usageError(writer, "unrecognized value: --{s}={s}, expected one of: {s}", .{ field.name, arg_value, enumValuesExpr(field.type) }); + }; + }, + .pointer => |ptrInfo| { + comptime assert(ptrInfo.size == .slice); + if (ptrInfo.child == u8) { + @field(result.named, field.name) = 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 { + const array_list = &@field(array_lists, field.name); + switch (@typeInfo(ptrInfo.child)) { + .bool => comptime unreachable, // Nicer compile error emitted in checkArgsType(). + .float => { + try array_list.append(allocator, std.fmt.parseFloat(ptrInfo.child, arg_value) catch |err| { + return usageError(writer, "unable to parse --{s}={s}: {s}", .{ field.name, arg_value, @errorName(err) }); + }); + }, + .int => { + try array_list.append(allocator, std.fmt.parseInt(ptrInfo.child, arg_value, 0) catch |err| { + return usageError(writer, "unable to parse --{s}={s}: {s}", .{ field.name, arg_value, @errorName(err) }); + }); + }, + .@"enum" => comptime unreachable, + .pointer => |ptrInfo2| { + comptime assert(ptrInfo2.size == .slice); + if (ptrInfo2.child == u8) { + // String. + try array_list.append(allocator, 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; + }, + else => comptime unreachable, + } + } + }, + else => comptime unreachable, + } + fields_seen[i] = true; + break; + } + } else { + // Didn't match anything. + return usageError(writer, "unrecognized argument: {s}", .{arg}); + } + } + + // Fill default values. + inline for (named_info.fields, 0..) |field, i| { + if (!fields_seen[i]) { + if (field.defaultValue()) |default| { + @field(result.named, field.name) = default; + } else { + if (field.type == bool) { + return usageError(writer, "missing required argument: --" ++ field.name ++ " or --no-" ++ field.name, .{}); + } else { + return usageError(writer, "missing required argument: --" ++ field.name, .{}); + } + } + } + } + + // Finalize the array lists. + result.positional = try positional.toOwnedSlice(allocator); + inline for (@typeInfo(@TypeOf(array_lists)).@"struct".fields) |field| { + @field(result.named, field.name) = try @field(array_lists, field.name).toOwnedSlice(allocator); + } + + return result; +} + +fn checkArgsType(comptime Args: type) void { + const args_fields = @typeInfo(Args).@"struct".fields; + if (!(args_fields.len == 2 and mem.eql(u8, args_fields[0].name, "named") and mem.eql(u8, args_fields[1].name, "positional"))) @compileError("expected Args to have exactly these fields in this order: named, positional"); + if (args_fields[1].default_value_ptr == null) @compileError("Args.positional must have a default value"); + + inline for (@typeInfo(args_fields[0].type).@"struct".fields) |field| { + 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. to provide custom help formatting, give options.writer and handle error.Help"); + 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.default_value_ptr == null) @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) @compileError("Unsupported field type: " ++ @typeName(field.type)); + if (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)), + } + } +} + +/// 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. +pub fn @"error"(comptime msg: []const u8, args: anytype, options: Options) error{Usage} { + return usageError(options.writer, msg, args); +} + +test @"error" { + const Args = struct { + named: struct { + output: []const u8 = "", + }, + positional: []const []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.isAbsolutePosix(args.named.output)) { + return std.cli.@"error"("--output must not be absolute: {s}", .{args.named.output}, .{}); + } + if (args.positional.len > 1) { + return std.cli.@"error"("expected exactly 1 positional arg", .{}, .{}); + } +} + +fn usageError(writer: ?*Writer, comptime msg: []const u8, args: anytype) error{Usage} { + const whole_msg = + "error: " ++ msg ++ "\n" ++ + \\try --help for full help info + \\ + ; + if (writer) |w| { + w.print(whole_msg, args) catch {}; + } else { + std.debug.print(whole_msg, args); + } + return error.Usage; +} + +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 printGeneratedHelp(writer: ?*Writer, prog: []const u8, comptime named_info: std.builtin.Type.Struct) void { + const msg = // + \\usage: {s} [options] [arg...] + \\ + \\arguments:{s} + \\ --help + \\ + ; + comptime var arguments_str: []const u8 = ""; + inline for (named_info.fields) |field| { + switch (@typeInfo(field.type)) { + .bool => { + if (field.defaultValue()) |default| { + if (default) { + arguments_str = arguments_str ++ "\n --no-" ++ field.name ++ " default: --" ++ field.name; + } else { + arguments_str = arguments_str ++ "\n --" ++ field.name ++ " default: --no-" ++ field.name; + } + } else { + arguments_str = arguments_str ++ "\n --" ++ field.name ++ " or --no-" ++ field.name ++ " required"; + } + }, + .int, .float => { + arguments_str = arguments_str ++ "\n --" ++ field.name ++ " " ++ @typeName(field.type); + if (field.defaultValue()) |default| { + arguments_str = arguments_str ++ " default: " ++ std.fmt.comptimePrint("{}", .{default}); + } else { + arguments_str = arguments_str ++ " required"; + } + }, + .@"enum" => { + arguments_str = arguments_str ++ "\n --" ++ field.name ++ " " ++ comptime enumValuesExpr(field.type); + if (field.defaultValue()) |default| { + arguments_str = arguments_str ++ " default: " ++ quoteIfEmpty(@tagName(default)); + } else { + arguments_str = arguments_str ++ " required"; + } + }, + .pointer => |ptrInfo| { + if (ptrInfo.size == .slice and ptrInfo.child == u8) { + // String. + arguments_str = arguments_str ++ "\n --" ++ field.name ++ " string"; + if (field.defaultValue()) |default| { + arguments_str = arguments_str ++ " default: " ++ quoteIfEmpty(default); + } else { + arguments_str = arguments_str ++ " required"; + } + } 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_str = arguments_str ++ "\n " ++ // + "--" ++ field.name ++ " " ++ type_name ++ " " ++ // + "[--" ++ field.name ++ " " ++ type_name ++ " ...]"; + } + }, + else => @compileError("Unsupported field type: " ++ @typeName(field.type)), + } + } + if (writer) |w| { + w.print(msg, .{ prog, arguments_str }) 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, arguments_str }) catch {}; + file_writer.interface.flush() catch {}; + } +} + +inline fn quoteIfEmpty(comptime s: []const u8) []const u8 { + if (s.len == 0) return "''"; + return s; +} + +var failing_writer: Writer = .failing; +const silent_options = Options{ .writer = &failing_writer }; + +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 = "", + }, + positional: []const []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 = "", + }, + positional: []const []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 = "", + }, + positional: []const []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, + }, + positional: []const []const u8 = &.{}, + }, 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, + }, + positional: []const []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, + }, + positional: []const []const u8 = &.{}, + }, 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 = &.{}, + }, + positional: []const []const u8 = &.{}, + }, 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, + }, + positional: []const []const u8 = &.{}, + }, 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 = &.{}, + }, + positional: []const []const u8 = &.{}, + }, 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 }, + }, + positional: []const []const u8 = &.{}, + }, 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: []const []const u8 = &.{}, + }, allocator, &[_][]const u8{"-z"}, options)); + try testing.expect(mem.indexOf(u8, aw.written(), "-z") != null); +} + +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, + }, + positional: []const []const u8 = &.{}, + }; + 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), + }, + .positional = &.{}, + }, args); + + const Args2 = struct { + named: struct { + nan: f64, + }, + positional: []const []const u8 = &.{}, + }; + const args2 = try parseSlice(Args2, allocator, &[_][]const u8{ + "--nan", "nAN", + }, .{}); + + try testing.expect(std.math.isNan(args2.named.nan)); +} + +test "bool" { + var arena: std.heap.ArenaAllocator = .init(testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + const Args = struct { + named: struct { + b: bool, + }, + positional: []const []const u8 = &.{}, + }; + + 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, + }, + positional: []const []const u8 = &.{}, + }; + const args = try parseSlice(Args, allocator, &[_][:0]const u8{ + "--a", "a", + "--b", "b", + }, .{}); + + try testing.expectEqualDeep(Args{ + .named = .{ + .a = "a", + .b = "b", + }, + .positional = &.{}, + }, args); +} + +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: []const []const u8 = &.{}, + }; + const args = try parseSlice(Args, allocator, &[_][]const u8{ + "--path", "a", + "--path", "b", + "--path", "a", + "--id", "1", + "--id", "-12", + }, .{}); + + try testing.expectEqualDeep(Args{ + .named = .{ + .path = &[_][]const u8{ "a", "b", "a" }, + .id = &[_]i32{ 1, -12 }, + }, + .positional = &.{}, + }, args); +} + +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, + }, + }, + positional: []const []const u8 = &.{}, + }; + 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, + }, + .positional = &.{}, + }, 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, + }, + positional: []const []const u8 = &.{}, + }; + + try testing.expectEqualDeep(Args{ + .named = .{}, + .positional = &.{}, + }, try parseSlice(Args, allocator, &[_][]const u8{}, .{})); + try testing.expectEqualDeep(Args{ + .named = .{ + .color = .always, + }, + .positional = &.{}, + }, try parseSlice(Args, allocator, &[_][]const u8{ "--color", "always" }, .{})); + try testing.expectEqualDeep(Args{ + .named = .{ + .file = &[_][]const u8{"file.txt"}, + }, + .positional = &.{}, + }, try parseSlice(Args, allocator, &[_][]const u8{ "--file", "file.txt" }, .{})); + + try testing.expectEqualDeep(Args{ + .named = .{ + .force = true, + .cleanup = false, + }, + .positional = &.{}, + }, try parseSlice(Args, allocator, &[_][]const u8{ "--force", "--no-cleanup" }, .{})); +} + +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, + }, + positional: []const []const u8 = &.{}, + }, 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(), "--flag") != 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, + }, + positional: []const []const u8 = &.{}, + }, 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, + }, + positional: []const []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 = &.{}, + }, + positional: []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, + }, + positional: []const []const u8 = &.{}, + }, 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, + }, + positional: []const []const u8 = &.{}, + }, allocator, &[_][]const u8{"--help"}, options)); + const bool_required_help = try aw.toOwnedSlice(); + try testing.expectError(error.Help, parseSlice(struct { + named: struct { + b: bool = true, + }, + positional: []const []const u8 = &.{}, + }, allocator, &[_][]const u8{"--help"}, options)); + const default_true_help = try aw.toOwnedSlice(); + try testing.expectError(error.Help, parseSlice(struct { + named: struct { + b: bool = false, + }, + positional: []const []const u8 = &.{}, + }, 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 }, + }, + positional: []const []const u8 = &.{}, + }, 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, + }, + positional: []const []const u8 = &.{}, + }, 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, + }, + positional: []const []const u8 = &.{}, + }, 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 { + named: struct {}, + positional: []const []const u8 = &.{}, + }; + + var arena: std.heap.ArenaAllocator = .init(testing.allocator); + defer arena.deinit(); + const args = try parseSlice(Args, arena.allocator(), &[_][]const u8{}, .{}); + + try testing.expectEqual(@as(usize, 0), args.positional.len); +} + +test "manual deinit" { + const Args = struct { + named: struct { + str_arr: []const []const u8 = &.{}, + int_arr: []const i32 = &.{}, + empty_arr: []const []const u8 = &.{}, + }, + positional: []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 = &.{ "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); + // Should be no memory leak errors now. +} + +test "actually calling error" { + const Args = struct { + named: struct { + output: []const u8 = "", + }, + positional: []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, std.cli.@"error"("--output must not be absolute: {s}", .{args.named.output}, silent_options)); + try testing.expectEqual(error.Usage, std.cli.@"error"("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: []const []const u8 = &.{}, + }; + try testing.expectError(error.Help, parseSlice(Args, allocator, &[_][]const u8{"--help"}, options)); + try testing.expectEqualStrings(Args.help, aw.written()); +} 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"); From 8cb3eb3a56bcaed627447c968cbeb55cbb050c58 Mon Sep 17 00:00:00 2001 From: Josh Wolfe Date: Sat, 16 Aug 2025 18:49:55 -0400 Subject: [PATCH 02/13] add more test params i guess --- lib/std/cli.zig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/std/cli.zig b/lib/std/cli.zig index 83e832745e31..8c15dd409425 100644 --- a/lib/std/cli.zig +++ b/lib/std/cli.zig @@ -126,6 +126,8 @@ test parse { color: enum { auto, never, always } = .auto, /// --seed=0x is actually passed in by the `zig test` system (as of 0.14.1), which we receive here. seed: u32 = 0, + @"cache-dir": []const u8 = "", + listen: []const u8 = "", }, /// Receives the rest of the arguments. positional: []const [:0]const u8 = &.{}, From 7b386cc93f9f8ca723720d0905f24506b8b9f852 Mon Sep 17 00:00:00 2001 From: Josh Wolfe Date: Sun, 17 Aug 2025 09:00:12 -0400 Subject: [PATCH 03/13] add options.exit_on_error --- lib/std/cli.zig | 84 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 58 insertions(+), 26 deletions(-) diff --git a/lib/std/cli.zig b/lib/std/cli.zig index 8c15dd409425..c1e10e79be90 100644 --- a/lib/std/cli.zig +++ b/lib/std/cli.zig @@ -11,8 +11,8 @@ const mem = std.mem; const Allocator = mem.Allocator; pub const Options = struct { - /// When returning error.Usage, print a short error message to this writer, defaults to stderr. - /// When returning error.Help, print the long help documentation to this writer, defaults to stdout. + /// 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, @@ -20,6 +20,10 @@ pub const Options = struct { /// 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{ @@ -56,7 +60,7 @@ pub const Error = error{ /// (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 returns `error.Help`. +/// 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 appended into the `positional` array in order. /// @@ -107,12 +111,20 @@ pub const Error = error{ /// 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 an error status 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!Args { var iter: ArgIterator = try .initWithAllocator(arena); // Do not call iter.deinit(). It holds the string data returned in the Args. - return parseIter(Args, arena, &iter, options); + + 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 { @@ -151,12 +163,18 @@ test parse { /// 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 an error status instead. +/// 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` as well as freeing `args.positional`. pub fn parseIter(comptime Args: type, arena: Allocator, iter: anytype, options: Options) Error!Args { - const prog = options.prog orelse if (iter.next()) |arg0| std.fs.path.basename(arg0) else ""; - return innerParse(Args, arena, iter, prog, options.writer); + 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`. @@ -167,6 +185,11 @@ pub fn parseIter(comptime Args: type, arena: Allocator, iter: anytype, options: /// 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 an error status instead. +/// 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` as well as freeing `args.positional`. @@ -179,7 +202,7 @@ pub fn parseSlice(comptime Args: type, arena: Allocator, argv: anytype, options: 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); + return innerParse(Args, arena, &iter, options.prog orelse "", options.writer, options.exit orelse false); } test parseSlice { @@ -219,8 +242,8 @@ test parseSlice { }, args); } -fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: []const u8, writer: ?*Writer) Error!Args { - // arg0 has already been consumed. +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. comptime checkArgsType(Args); @@ -272,13 +295,16 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: [] } else { printGeneratedHelp(writer, prog, named_info); } + if (exit_on_error) { + std.process.exit(1); + } return error.Help; } if (arg.len >= 2 and arg[0] == '-' and isAlphabetic(arg[1])) { // Always invalid. // Examples: -h, -flag, -I/path - return usageError(writer, "unrecognized argument: {s}", .{arg}); + return usageError(writer, "unrecognized argument: {s}", .{arg}, exit_on_error); } if (mem.eql(u8, arg, "--")) { // Stop recognizing named arguments. Everything else is positional. @@ -312,31 +338,31 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: [] inline for (named_info.fields, 0..) |field, i| { if (mem.eql(u8, field.name, arg_name)) { if (field.type == bool) { - if (immediate_value != null) return usageError(writer, "cannot specify value for bool argument: {s}", .{arg}); + if (immediate_value != null) return usageError(writer, "cannot specify value for bool argument: {s}", .{arg}, exit_on_error); @field(result.named, field.name) = !no_prefixed; fields_seen[i] = true; break; } - if (no_prefixed) return usageError(writer, "unrecognized argument: {s}", .{arg}); + if (no_prefixed) return usageError(writer, "unrecognized argument: {s}", .{arg}, exit_on_error); // All other argument types require a value. - const arg_value = immediate_value orelse iter.next() orelse return usageError(writer, "expected argument after --{s}", .{field.name}); + const arg_value = immediate_value orelse iter.next() orelse return usageError(writer, "expected argument after --{s}", .{field.name}, exit_on_error); switch (@typeInfo(field.type)) { .bool => unreachable, // Handled above. .float => { @field(result.named, field.name) = std.fmt.parseFloat(field.type, arg_value) catch |err| { - return usageError(writer, "unable to parse --{s}={s}: {s}", .{ field.name, arg_value, @errorName(err) }); + return usageError(writer, "unable to parse --{s}={s}: {s}", .{ field.name, arg_value, @errorName(err) }, exit_on_error); }; }, .int => { @field(result.named, field.name) = std.fmt.parseInt(field.type, arg_value, 0) catch |err| { - return usageError(writer, "unable to parse --{s}={s}: {s}", .{ field.name, arg_value, @errorName(err) }); + return usageError(writer, "unable to parse --{s}={s}: {s}", .{ field.name, arg_value, @errorName(err) }, exit_on_error); }; }, .@"enum" => { @field(result.named, field.name) = std.meta.stringToEnum(field.type, arg_value) orelse { - return usageError(writer, "unrecognized value: --{s}={s}, expected one of: {s}", .{ field.name, arg_value, enumValuesExpr(field.type) }); + return usageError(writer, "unrecognized value: --{s}={s}, expected one of: {s}", .{ field.name, arg_value, enumValuesExpr(field.type) }, exit_on_error); }; }, .pointer => |ptrInfo| { @@ -349,12 +375,12 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: [] .bool => comptime unreachable, // Nicer compile error emitted in checkArgsType(). .float => { try array_list.append(allocator, std.fmt.parseFloat(ptrInfo.child, arg_value) catch |err| { - return usageError(writer, "unable to parse --{s}={s}: {s}", .{ field.name, arg_value, @errorName(err) }); + return usageError(writer, "unable to parse --{s}={s}: {s}", .{ field.name, arg_value, @errorName(err) }, exit_on_error); }); }, .int => { try array_list.append(allocator, std.fmt.parseInt(ptrInfo.child, arg_value, 0) catch |err| { - return usageError(writer, "unable to parse --{s}={s}: {s}", .{ field.name, arg_value, @errorName(err) }); + return usageError(writer, "unable to parse --{s}={s}: {s}", .{ field.name, arg_value, @errorName(err) }, exit_on_error); }); }, .@"enum" => comptime unreachable, @@ -376,7 +402,7 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: [] } } else { // Didn't match anything. - return usageError(writer, "unrecognized argument: {s}", .{arg}); + return usageError(writer, "unrecognized argument: {s}", .{arg}, exit_on_error); } } @@ -387,9 +413,9 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: [] @field(result.named, field.name) = default; } else { if (field.type == bool) { - return usageError(writer, "missing required argument: --" ++ field.name ++ " or --no-" ++ field.name, .{}); + return usageError(writer, "missing required argument: --" ++ field.name ++ " or --no-" ++ field.name, .{}, exit_on_error); } else { - return usageError(writer, "missing required argument: --" ++ field.name, .{}); + return usageError(writer, "missing required argument: --" ++ field.name, .{}, exit_on_error); } } } @@ -456,8 +482,11 @@ fn checkArgsType(comptime Args: type) void { /// 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. +/// +/// 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 msg: []const u8, args: anytype, options: Options) error{Usage} { - return usageError(options.writer, msg, args); + return usageError(options.writer, msg, args, options.exit orelse true); } test @"error" { @@ -472,15 +501,15 @@ test @"error" { defer arena.deinit(); const args = try parseSlice(Args, arena.allocator(), &[_][]const u8{ "--output=o.txt", "i.txt" }, .{}); - if (std.fs.path.isAbsolutePosix(args.named.output)) { - return std.cli.@"error"("--output must not be absolute: {s}", .{args.named.output}, .{}); + if (std.fs.path.isAbsolute(args.named.output)) { + return std.cli.@"error"("--output must not be absolute: {s}", .{args.named.output}, .{ .exit_on_error = false }); } if (args.positional.len > 1) { - return std.cli.@"error"("expected exactly 1 positional arg", .{}, .{}); + return std.cli.@"error"("expected exactly 1 positional arg", .{}, .{ .exit_on_error = false }); } } -fn usageError(writer: ?*Writer, comptime msg: []const u8, args: anytype) error{Usage} { +fn usageError(writer: ?*Writer, comptime msg: []const u8, args: anytype, exit_on_error: bool) error{Usage} { const whole_msg = "error: " ++ msg ++ "\n" ++ \\try --help for full help info @@ -491,6 +520,9 @@ fn usageError(writer: ?*Writer, comptime msg: []const u8, args: anytype) error{U } else { std.debug.print(whole_msg, args); } + if (exit_on_error) { + std.process.exit(1); + } return error.Usage; } From f27e5649a5bf2969ba04e1415aaf1985bf070211 Mon Sep 17 00:00:00 2001 From: Josh Wolfe Date: Sun, 17 Aug 2025 10:01:37 -0400 Subject: [PATCH 04/13] exit(0) on --help --- lib/std/cli.zig | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/std/cli.zig b/lib/std/cli.zig index c1e10e79be90..a02a7d7e151a 100644 --- a/lib/std/cli.zig +++ b/lib/std/cli.zig @@ -112,7 +112,7 @@ pub const Error = error{ /// 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 an error status unless `options.exit` is set to `false`, +/// 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. /// @@ -165,7 +165,7 @@ test parse { /// /// 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 an error status instead. +/// 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; @@ -187,7 +187,7 @@ pub fn parseIter(comptime Args: type, arena: Allocator, iter: anytype, options: /// /// 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 an error status instead. +/// 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; @@ -296,7 +296,7 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: [] printGeneratedHelp(writer, prog, named_info); } if (exit_on_error) { - std.process.exit(1); + std.process.exit(0); } return error.Help; } @@ -437,7 +437,7 @@ fn checkArgsType(comptime Args: type) void { inline for (@typeInfo(args_fields[0].type).@"struct".fields) |field| { 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. to provide custom help formatting, give options.writer and handle error.Help"); + 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); @@ -502,10 +502,10 @@ test @"error" { 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"("--output must not be absolute: {s}", .{args.named.output}, .{ .exit_on_error = false }); + return std.cli.@"error"("--output must not be absolute: {s}", .{args.named.output}, .{ .exit = false }); } if (args.positional.len > 1) { - return std.cli.@"error"("expected exactly 1 positional arg", .{}, .{ .exit_on_error = false }); + return std.cli.@"error"("expected exactly 1 positional arg", .{}, .{ .exit = false }); } } @@ -633,7 +633,7 @@ inline fn quoteIfEmpty(comptime s: []const u8) []const u8 { } var failing_writer: Writer = .failing; -const silent_options = Options{ .writer = &failing_writer }; +const silent_options = Options{ .writer = &failing_writer, .exit = false }; test "usage errors" { var arena: std.heap.ArenaAllocator = .init(testing.allocator); From c6f3e4bd5aacef63fa98bf025de13c29192b84c7 Mon Sep 17 00:00:00 2001 From: Josh Wolfe Date: Sun, 17 Aug 2025 16:38:50 -0400 Subject: [PATCH 05/13] use correct error type --- lib/std/cli.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/std/cli.zig b/lib/std/cli.zig index a02a7d7e151a..bf4cf118b23f 100644 --- a/lib/std/cli.zig +++ b/lib/std/cli.zig @@ -118,7 +118,7 @@ pub const Error = error{ /// /// 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!Args { +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. From f2753f5910b320b53c26fa6e0e145bc934119cb8 Mon Sep 17 00:00:00 2001 From: Josh Wolfe Date: Sun, 24 Aug 2025 21:59:49 -0400 Subject: [PATCH 06/13] change positional to a struct --- lib/std/cli.zig | 871 ++++++++++++++++++++++++++++-------------------- 1 file changed, 514 insertions(+), 357 deletions(-) diff --git a/lib/std/cli.zig b/lib/std/cli.zig index bf4cf118b23f..5da13dbdfc73 100644 --- a/lib/std/cli.zig +++ b/lib/std/cli.zig @@ -7,6 +7,7 @@ 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; @@ -29,23 +30,25 @@ pub const Options = struct { 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. + /// 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: +/// `Args` is a struct that you define looking like this: /// ``` /// const Args = struct { /// named: struct { /// // ... /// }, -/// positional: []const []const u8 = &.{}, +/// positional: struct { +/// // ... +/// }, /// }; /// ``` -/// The `named` and `positional` fields are required, although `named` need not have any subfields. -/// `positional` may instead have type `[]const [:0]const u8`. +/// Either or both of `named` and `positional` may be omitted, which is effectively equivalent to them having no fields. /// /// The sequence of arg strings from the `ArgIterator` is parsed to determine named and positional arguments. /// @@ -62,7 +65,7 @@ pub const Error = error{ /// 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 appended into the `positional` array in order. +/// 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`. @@ -88,6 +91,17 @@ pub const Error = error{ /// 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`. /// @@ -104,7 +118,6 @@ pub const Error = error{ /// named: struct { /// // [...] /// }, -/// positional: []const []const u8 = &.{}, /// }; /// ``` /// @@ -136,13 +149,21 @@ test parse { level: i8 = -1, /// Parsed as the name of the member `--color=never`. color: enum { auto, never, always } = .auto, - /// --seed=0x is actually passed in by the `zig test` system (as of 0.14.1), which we receive here. + + // 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 = "", }, - /// Receives the rest of the arguments. - positional: []const [:0]const u8 = &.{}, + positional: struct { + /// First positional (non-named) argument: + input: [:0]const u8 = "", + /// Second positional argument is declared as optional: + reptitions: u32 = 1, + /// Receives the rest of the positional arguments. + @"the-rest": []const [:0]const u8 = &.{}, + }, }; var arena: ArenaAllocator = .init(testing.allocator); @@ -157,8 +178,8 @@ test parse { /// ``` /// 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 `[:0]const u8` fields. +/// 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. @@ -170,7 +191,7 @@ test parse { /// /// 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` as well as freeing `args.positional`. +/// 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 ""; @@ -179,7 +200,7 @@ pub fn parseIter(comptime Args: type, arena: Allocator, iter: anytype, options: /// 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 that coerces to `[]const u8`. +/// 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`. @@ -192,7 +213,7 @@ pub fn parseIter(comptime Args: type, arena: Allocator, iter: anytype, options: /// /// 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` as well as freeing `args.positional`. +/// 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) @@ -218,7 +239,9 @@ test parseSlice { flag: bool = true, @"enum-option": enum { auto, always, never } = .auto, }, - positional: []const []const u8 = &.{}, + positional: struct { + args: []const []const u8 = &.{}, + }, }; const args = try parseSlice(Args, allocator, &[_][]const u8{ "--example_required", "a.txt", @@ -238,7 +261,7 @@ test parseSlice { .flag = false, .@"enum-option" = .always, }, - .positional = &.{ "positional1", "positional2", "-12345678", "--positional4", "--positional=5" }, + .positional = .{ .args = &.{ "positional1", "positional2", "-12345678", "--positional4", "--positional=5" } }, }, args); } @@ -246,42 +269,19 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: [] // 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. - comptime checkArgsType(Args); + 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 positional: ArrayList(@typeInfo(@TypeOf(result.positional)).pointer.child) = .{}; + var named_fields_seen = [_]bool{false} ** named_fields.len; + var positional_field_index: usize = 0; - const ArgsNamed = @TypeOf(result.named); - const named_info = @typeInfo(ArgsNamed).@"struct"; - - // Declare and initialize an ArrayList(C) for every []const C field (other than u8). - var fields_seen = [_]bool{false} ** named_info.fields.len; - comptime var array_list_fields: []const std.builtin.Type.StructField = &.{}; - inline for (named_info.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 std.builtin.Type.StructField, &.{.{ - .name = field.name, - .type = ArrayList(info.pointer.child), - .default_value_ptr = null, - .is_comptime = false, - .alignment = @alignOf(ArrayList(info.pointer.child)), - }}); - } - } - } - var array_lists: @Type(.{ .@"struct" = .{ .layout = .auto, .fields = array_list_fields, .decls = &.{}, .is_tuple = false } }) = undefined; - inline for (@typeInfo(@TypeOf(array_lists)).@"struct".fields) |field| { - @field(array_lists, field.name) = .{}; - } + var the_rest_is_positional = false; while (iter.next()) |arg| { - if (mem.eql(u8, arg, "--help")) { + if (!the_rest_is_positional and mem.eql(u8, arg, "--help")) { if (@hasDecl(Args, "help")) { // Custom help. if (writer) |w| { @@ -293,7 +293,7 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: [] file_writer.interface.flush() catch {}; } } else { - printGeneratedHelp(writer, prog, named_info); + printGeneratedHelp(writer, prog, named_fields); } if (exit_on_error) { std.process.exit(0); @@ -301,22 +301,32 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: [] return error.Help; } - if (arg.len >= 2 and arg[0] == '-' and isAlphabetic(arg[1])) { + 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(writer, "unrecognized argument: {s}", .{arg}, exit_on_error); } - if (mem.eql(u8, arg, "--")) { + if (!the_rest_is_positional and mem.eql(u8, arg, "--")) { // Stop recognizing named arguments. Everything else is positional. - while (iter.next()) |arg2| { - try positional.append(allocator, arg2); // To resolve compile errors between `[:0]const u8` and `[]const u8` on this line, ensure the passed-in args are `[:0]const u8`. - } - break; + the_rest_is_positional = true; + continue; } - if (!(arg.len >= 3 and arg[0] == '-' and arg[1] == '-')) { + if (the_rest_is_positional or !(arg.len >= 3 and arg[0] == '-' and arg[1] == '-')) { // Positional. - // Examples: "", "a", "-", "-1", - try positional.append(allocator, arg); + // Examples: "", "a", "-", "-1", "other" + if (positional_field_index >= positional_fields.len) return usageError(writer, "unexpected positional argument: {s}", .{arg}, 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(C, arg, field.name, writer, exit_on_error)); + // Don't increment positional_field_index. + } else { + @field(result.positional, field.name) = try parseValue(field.type, arg, field.name, writer, exit_on_error); + positional_field_index += 1; + } + break; + } + } else unreachable; continue; } @@ -335,12 +345,12 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: [] break :blk .{ arg["--".len..], null, false }; }; - inline for (named_info.fields, 0..) |field, i| { + 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(writer, "cannot specify value for bool argument: {s}", .{arg}, exit_on_error); @field(result.named, field.name) = !no_prefixed; - fields_seen[i] = true; break; } if (no_prefixed) return usageError(writer, "unrecognized argument: {s}", .{arg}, exit_on_error); @@ -348,56 +358,11 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: [] // All other argument types require a value. const arg_value = immediate_value orelse iter.next() orelse return usageError(writer, "expected argument after --{s}", .{field.name}, exit_on_error); - switch (@typeInfo(field.type)) { - .bool => unreachable, // Handled above. - .float => { - @field(result.named, field.name) = std.fmt.parseFloat(field.type, arg_value) catch |err| { - return usageError(writer, "unable to parse --{s}={s}: {s}", .{ field.name, arg_value, @errorName(err) }, exit_on_error); - }; - }, - .int => { - @field(result.named, field.name) = std.fmt.parseInt(field.type, arg_value, 0) catch |err| { - return usageError(writer, "unable to parse --{s}={s}: {s}", .{ field.name, arg_value, @errorName(err) }, exit_on_error); - }; - }, - .@"enum" => { - @field(result.named, field.name) = std.meta.stringToEnum(field.type, arg_value) orelse { - return usageError(writer, "unrecognized value: --{s}={s}, expected one of: {s}", .{ field.name, arg_value, enumValuesExpr(field.type) }, exit_on_error); - }; - }, - .pointer => |ptrInfo| { - comptime assert(ptrInfo.size == .slice); - if (ptrInfo.child == u8) { - @field(result.named, field.name) = 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 { - const array_list = &@field(array_lists, field.name); - switch (@typeInfo(ptrInfo.child)) { - .bool => comptime unreachable, // Nicer compile error emitted in checkArgsType(). - .float => { - try array_list.append(allocator, std.fmt.parseFloat(ptrInfo.child, arg_value) catch |err| { - return usageError(writer, "unable to parse --{s}={s}: {s}", .{ field.name, arg_value, @errorName(err) }, exit_on_error); - }); - }, - .int => { - try array_list.append(allocator, std.fmt.parseInt(ptrInfo.child, arg_value, 0) catch |err| { - return usageError(writer, "unable to parse --{s}={s}: {s}", .{ field.name, arg_value, @errorName(err) }, exit_on_error); - }); - }, - .@"enum" => comptime unreachable, - .pointer => |ptrInfo2| { - comptime assert(ptrInfo2.size == .slice); - if (ptrInfo2.child == u8) { - // String. - try array_list.append(allocator, 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; - }, - else => comptime unreachable, - } - } - }, - else => comptime unreachable, + if (getArrayChild(field.type)) |C| { + try @field(named_array_lists, field.name).append(allocator, try parseValue(C, arg_value, field.name, writer, exit_on_error)); + } else { + @field(result.named, field.name) = try parseValue(field.type, arg_value, field.name, writer, exit_on_error); } - fields_seen[i] = true; break; } } else { @@ -407,74 +372,202 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: [] } // Fill default values. - inline for (named_info.fields, 0..) |field, i| { - if (!fields_seen[i]) { - if (field.defaultValue()) |default| { - @field(result.named, field.name) = default; - } else { - if (field.type == bool) { - return usageError(writer, "missing required argument: --" ++ field.name ++ " or --no-" ++ field.name, .{}, exit_on_error); + 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 { - return usageError(writer, "missing required argument: --" ++ field.name, .{}, exit_on_error); + if (field.type == bool) { + return usageError(writer, "missing required argument: --" ++ field.name ++ " or --no-" ++ field.name, .{}, exit_on_error); + } else { + return usageError(writer, "missing required argument: --" ++ field.name, .{}, exit_on_error); + } } } } } - - // Finalize the array lists. - result.positional = try positional.toOwnedSlice(allocator); - inline for (@typeInfo(@TypeOf(array_lists)).@"struct".fields) |field| { - @field(result.named, field.name) = try @field(array_lists, field.name).toOwnedSlice(allocator); + 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(writer, "missing required argument: " ++ field.name, .{}, exit_on_error); + } + } + } } return result; } -fn checkArgsType(comptime Args: type) void { - const args_fields = @typeInfo(Args).@"struct".fields; - if (!(args_fields.len == 2 and mem.eql(u8, args_fields[0].name, "named") and mem.eql(u8, args_fields[1].name, "positional"))) @compileError("expected Args to have exactly these fields in this order: named, positional"); - if (args_fields[1].default_value_ptr == null) @compileError("Args.positional must have a default value"); +/// arg_value is []const u8 or [:0]const u8. +fn parseValue(comptime T: type, arg_value: anytype, comptime field_name: []const u8, writer: ?*Writer, 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(writer, "unable to parse --{s}={s}: {s}", .{ field_name, arg_value, @errorName(err) }, exit_on_error); + }; + }, + .int => { + return std.fmt.parseInt(T, arg_value, 0) catch |err| { + return usageError(writer, "unable to parse --{s}={s}: {s}", .{ field_name, arg_value, @errorName(err) }, exit_on_error); + }; + }, + .@"enum" => { + return std.meta.stringToEnum(T, arg_value) orelse { + return usageError(writer, "unrecognized value: --{s}={s}, expected one of: {s}", .{ field_name, arg_value, enumValuesExpr(T) }, 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, + } +} - inline for (@typeInfo(args_fields[0].type).@"struct".fields) |field| { - 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); +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); + } - 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.default_value_ptr == null) @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) @compileError("Unsupported field type: " ++ @typeName(field.type)); - if (ptrInfo2.child == u8) { - // String. - } else { - @compileError("Unsupported field type: " ++ @typeName(field.type)); - } - }, - else => @compileError("Unsupported field type: " ++ @typeName(field.type)), - } + const named_fields = if (has_named) @typeInfo(@TypeOf(@as(Args, undefined).named)).@"struct".fields else &.{}; + const positional_fields = if (has_positional) @typeInfo(@TypeOf(@as(Args, undefined).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.default_value_ptr == null) @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) @compileError("Unsupported field type: " ++ @typeName(field.type)); + if (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)), + } + }, + 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, @@ -494,7 +587,9 @@ test @"error" { named: struct { output: []const u8 = "", }, - positional: []const []const u8 = &.{}, + positional: struct { + input: []const u8, + }, }; var arena: std.heap.ArenaAllocator = .init(testing.allocator); @@ -504,9 +599,6 @@ test @"error" { if (std.fs.path.isAbsolute(args.named.output)) { return std.cli.@"error"("--output must not be absolute: {s}", .{args.named.output}, .{ .exit = false }); } - if (args.positional.len > 1) { - return std.cli.@"error"("expected exactly 1 positional arg", .{}, .{ .exit = false }); - } } fn usageError(writer: ?*Writer, comptime msg: []const u8, args: anytype, exit_on_error: bool) error{Usage} { @@ -552,7 +644,7 @@ fn enumValuesExpr(comptime Enum: type) []const u8 { return values_str; } -fn printGeneratedHelp(writer: ?*Writer, prog: []const u8, comptime named_info: std.builtin.Type.Struct) void { +fn printGeneratedHelp(writer: ?*Writer, prog: []const u8, comptime named_fields: []const StructField) void { const msg = // \\usage: {s} [options] [arg...] \\ @@ -561,7 +653,7 @@ fn printGeneratedHelp(writer: ?*Writer, prog: []const u8, comptime named_info: s \\ ; comptime var arguments_str: []const u8 = ""; - inline for (named_info.fields) |field| { + inline for (named_fields) |field| { switch (@typeInfo(field.type)) { .bool => { if (field.defaultValue()) |default| { @@ -635,121 +727,48 @@ inline fn quoteIfEmpty(comptime s: []const u8) []const u8 { var failing_writer: Writer = .failing; const silent_options = Options{ .writer = &failing_writer, .exit = false }; -test "usage errors" { +test "bool" { 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 = "", - }, - positional: []const []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 = "", - }, - positional: []const []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 = "", - }, - positional: []const []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 { + const Args = struct { named: struct { - name: bool = false, + b: bool, }, - positional: []const []const u8 = &.{}, - }, 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, - }, - positional: []const []const u8 = &.{}, - }, allocator, &[_][]const u8{}, options)); - try testing.expect(mem.indexOf(u8, aw.written(), "--name") != null); + 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" }, .{})); - // parse int error - aw.clearRetainingCapacity(); - try testing.expectError(error.Usage, parseSlice(struct { - named: struct { - name: i32, - }, - positional: []const []const u8 = &.{}, - }, 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 = &.{}, - }, - positional: []const []const u8 = &.{}, - }, allocator, &[_][]const u8{"--name=abc"}, options)); - try testing.expect(mem.indexOf(u8, aw.written(), "--name") != null); + 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)); +} - // parse float error - aw.clearRetainingCapacity(); - try testing.expectError(error.Usage, parseSlice(struct { - named: struct { - name: f32, - }, - positional: []const []const u8 = &.{}, - }, 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 = &.{}, - }, - positional: []const []const u8 = &.{}, - }, allocator, &[_][]const u8{"--name=abc"}, options)); - try testing.expect(mem.indexOf(u8, aw.written(), "--name") != null); +test "string" { + var arena: std.heap.ArenaAllocator = .init(testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); - // parse enum error - aw.clearRetainingCapacity(); - try testing.expectError(error.Usage, parseSlice(struct { + const Args = struct { named: struct { - name: enum { auto, never, always }, + a: []const u8, + b: [:0]const u8, }, - positional: []const []const u8 = &.{}, - }, 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); + }; + const args = try parseSlice(Args, allocator, &[_][:0]const u8{ + "--a", "a", + "--b", "b", + }, .{}); - // reject single-letter alias-looking arguments - aw.clearRetainingCapacity(); - try testing.expectError(error.Usage, parseSlice(struct { - named: struct { - z: bool = false, + try testing.expectEqualDeep(Args{ + .named = .{ + .a = "a", + .b = "b", }, - positional: []const []const u8 = &.{}, - }, allocator, &[_][]const u8{"-z"}, options)); - try testing.expect(mem.indexOf(u8, aw.written(), "-z") != null); + }, args); } test "ints and floats" { @@ -768,7 +787,6 @@ test "ints and floats" { inf_f32: f32, ninf_f64: f64, }, - positional: []const []const u8 = &.{}, }; const args = try parseSlice(Args, allocator, &[_][]const u8{ "--int_u32", "0xffffffff", @@ -792,14 +810,12 @@ test "ints and floats" { .inf_f32 = std.math.inf(f32), .ninf_f64 = -std.math.inf(f64), }, - .positional = &.{}, }, args); const Args2 = struct { named: struct { nan: f64, }, - positional: []const []const u8 = &.{}, }; const args2 = try parseSlice(Args2, allocator, &[_][]const u8{ "--nan", "nAN", @@ -808,80 +824,37 @@ test "ints and floats" { try testing.expect(std.math.isNan(args2.named.nan)); } -test "bool" { +test "array" { var arena: std.heap.ArenaAllocator = .init(testing.allocator); defer arena.deinit(); const allocator = arena.allocator(); const Args = struct { named: struct { - b: bool, + path: []const []const u8 = &.{}, + id: []const i32 = &.{}, }, - positional: []const []const u8 = &.{}, - }; - - 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, + positional: struct { + args: []const []const u8 = &.{}, }, - positional: []const []const u8 = &.{}, }; - const args = try parseSlice(Args, allocator, &[_][:0]const u8{ - "--a", "a", - "--b", "b", - }, .{}); try testing.expectEqualDeep(Args{ .named = .{ - .a = "a", - .b = "b", + .path = &[_][]const u8{ "a", "b", "a" }, + .id = &[_]i32{ 1, -12 }, }, - .positional = &.{}, - }, args); -} - -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 = .{ + .args = &[_][]const u8{ "x", "y" }, }, - positional: []const []const u8 = &.{}, - }; - const args = try parseSlice(Args, allocator, &[_][]const u8{ + }, try parseSlice(Args, allocator, &[_][]const u8{ "--path", "a", "--path", "b", "--path", "a", "--id", "1", "--id", "-12", - }, .{}); - - try testing.expectEqualDeep(Args{ - .named = .{ - .path = &[_][]const u8{ "a", "b", "a" }, - .id = &[_]i32{ 1, -12 }, - }, - .positional = &.{}, - }, args); + "x", "y", + }, .{})); } test "enum" { @@ -905,7 +878,6 @@ test "enum" { VTALRM = 26, }, }, - positional: []const []const u8 = &.{}, }; const args = try parseSlice(Args, allocator, &[_][]const u8{ "--color", "always", @@ -919,7 +891,6 @@ test "enum" { .guess = .@"the-only-option", .signal = .TERM, }, - .positional = &.{}, }, args); } @@ -942,24 +913,20 @@ test "defaults" { force: bool = false, cleanup: bool = true, }, - positional: []const []const u8 = &.{}, }; try testing.expectEqualDeep(Args{ .named = .{}, - .positional = &.{}, }, try parseSlice(Args, allocator, &[_][]const u8{}, .{})); try testing.expectEqualDeep(Args{ .named = .{ .color = .always, }, - .positional = &.{}, }, try parseSlice(Args, allocator, &[_][]const u8{ "--color", "always" }, .{})); try testing.expectEqualDeep(Args{ .named = .{ .file = &[_][]const u8{"file.txt"}, }, - .positional = &.{}, }, try parseSlice(Args, allocator, &[_][]const u8{ "--file", "file.txt" }, .{})); try testing.expectEqualDeep(Args{ @@ -967,10 +934,208 @@ test "defaults" { .force = true, .cleanup = false, }, - .positional = &.{}, }, 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(); @@ -985,7 +1150,6 @@ test "help" { int: i32, flag: bool, }, - positional: []const []const u8 = &.{}, }, 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, @@ -1002,7 +1166,6 @@ test "help" { named: struct { color: enum { never, auto, always } = .auto, }, - positional: []const []const u8 = &.{}, }, 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); @@ -1016,14 +1179,12 @@ test "help" { named: struct { name: []const u8, }, - positional: []const []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 = &.{}, }, - positional: []const []const u8 = &.{}, }, allocator, &[_][]const u8{"--help"}, options)); try testing.expect(!mem.eql(u8, scalar_help, aw.written())); @@ -1035,7 +1196,6 @@ test "help" { int: i32 = 3, f: f32 = 1.25, }, - positional: []const []const u8 = &.{}, }, 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); @@ -1047,21 +1207,18 @@ test "help" { named: struct { b: bool, }, - positional: []const []const u8 = &.{}, }, allocator, &[_][]const u8{"--help"}, options)); const bool_required_help = try aw.toOwnedSlice(); try testing.expectError(error.Help, parseSlice(struct { named: struct { b: bool = true, }, - positional: []const []const u8 = &.{}, }, allocator, &[_][]const u8{"--help"}, options)); const default_true_help = try aw.toOwnedSlice(); try testing.expectError(error.Help, parseSlice(struct { named: struct { b: bool = false, }, - positional: []const []const u8 = &.{}, }, allocator, &[_][]const u8{"--help"}, options)); const default_false_help = try aw.toOwnedSlice(); try testing.expect(!mem.eql(u8, bool_required_help, default_true_help)); @@ -1074,21 +1231,18 @@ test "help" { named: struct { color: enum { never, auto, always }, }, - positional: []const []const u8 = &.{}, }, 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, }, - positional: []const []const u8 = &.{}, }, 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, }, - positional: []const []const u8 = &.{}, }, allocator, &[_][]const u8{"--help"}, options)); const default_never_help = try aw.toOwnedSlice(); try testing.expect(!mem.eql(u8, enum_required_help, default_auto_help)); @@ -1097,16 +1251,11 @@ test "help" { } test "minimal" { - const Args = struct { - named: struct {}, - positional: []const []const u8 = &.{}, - }; + const Args = struct {}; var arena: std.heap.ArenaAllocator = .init(testing.allocator); defer arena.deinit(); - const args = try parseSlice(Args, arena.allocator(), &[_][]const u8{}, .{}); - - try testing.expectEqual(@as(usize, 0), args.positional.len); + _ = try parseSlice(Args, arena.allocator(), &[_][]const u8{}, .{}); } test "manual deinit" { @@ -1116,7 +1265,9 @@ test "manual deinit" { int_arr: []const i32 = &.{}, empty_arr: []const []const u8 = &.{}, }, - positional: []const []const u8 = &.{}, + positional: struct { + args: []const []const u8 = &.{}, + }, }; const args = try parseSlice(Args, testing.allocator, &[_][]const u8{ @@ -1130,14 +1281,16 @@ test "manual deinit" { .str_arr = &.{ "hello1", "hello2" }, .int_arr = &.{ 123456, 789012 }, }, - .positional = &.{ "positional-12345", "positi" }, + .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); + testing.allocator.free(args.positional.args); // Should be no memory leak errors now. } @@ -1146,7 +1299,9 @@ test "actually calling error" { named: struct { output: []const u8 = "", }, - positional: []const []const u8 = &.{}, + positional: struct { + args: []const []const u8 = &.{}, + }, }; var arena: std.heap.ArenaAllocator = .init(testing.allocator); @@ -1182,7 +1337,9 @@ test "custom help" { output: []const u8, force: bool = false, }, - positional: []const []const u8 = &.{}, + 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()); From 9df8a48667af45f67cd12c02d2e77f1dd67ea373 Mon Sep 17 00:00:00 2001 From: Josh Wolfe Date: Sat, 30 Aug 2025 06:08:21 -0400 Subject: [PATCH 07/13] nicer usage --- lib/std/cli.zig | 258 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 183 insertions(+), 75 deletions(-) diff --git a/lib/std/cli.zig b/lib/std/cli.zig index 5da13dbdfc73..618246d24314 100644 --- a/lib/std/cli.zig +++ b/lib/std/cli.zig @@ -293,7 +293,7 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: [] file_writer.interface.flush() catch {}; } } else { - printGeneratedHelp(writer, prog, named_fields); + printGeneratedHelp(named_fields, positional_fields, writer, prog); } if (exit_on_error) { std.process.exit(0); @@ -304,7 +304,7 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: [] 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(writer, "unrecognized argument: {s}", .{arg}, exit_on_error); + return usageError(named_fields, positional_fields, 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. @@ -314,14 +314,14 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: [] 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(writer, "unexpected positional argument: {s}", .{arg}, exit_on_error); + if (positional_field_index >= positional_fields.len) return usageError(named_fields, positional_fields, 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(C, arg, field.name, writer, exit_on_error)); + try @field(positional_array_lists, field.name).append(allocator, try parseValue(named_fields, positional_fields, C, arg, field.name, writer, prog, exit_on_error)); // Don't increment positional_field_index. } else { - @field(result.positional, field.name) = try parseValue(field.type, arg, field.name, writer, exit_on_error); + @field(result.positional, field.name) = try parseValue(named_fields, positional_fields, field.type, arg, field.name, writer, prog, exit_on_error); positional_field_index += 1; } break; @@ -349,25 +349,25 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: [] if (mem.eql(u8, field.name, arg_name)) { named_fields_seen[i] = true; if (field.type == bool) { - if (immediate_value != null) return usageError(writer, "cannot specify value for bool argument: {s}", .{arg}, exit_on_error); + if (immediate_value != null) return usageError(named_fields, positional_fields, 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(writer, "unrecognized argument: {s}", .{arg}, exit_on_error); + if (no_prefixed) return usageError(named_fields, positional_fields, 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(writer, "expected argument after --{s}", .{field.name}, exit_on_error); + const arg_value = immediate_value orelse iter.next() orelse return usageError(named_fields, positional_fields, 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(C, arg_value, field.name, writer, exit_on_error)); + try @field(named_array_lists, field.name).append(allocator, try parseValue(named_fields, positional_fields, C, arg_value, field.name, writer, prog, exit_on_error)); } else { - @field(result.named, field.name) = try parseValue(field.type, arg_value, field.name, writer, exit_on_error); + @field(result.named, field.name) = try parseValue(named_fields, positional_fields, field.type, arg_value, field.name, writer, prog, exit_on_error); } break; } } else { // Didn't match anything. - return usageError(writer, "unrecognized argument: {s}", .{arg}, exit_on_error); + return usageError(named_fields, positional_fields, writer, "unrecognized argument: {s}", .{arg}, prog, exit_on_error); } } @@ -384,9 +384,9 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: [] @field(result.named, field.name) = default; } else { if (field.type == bool) { - return usageError(writer, "missing required argument: --" ++ field.name ++ " or --no-" ++ field.name, .{}, exit_on_error); + return usageError(named_fields, positional_fields, writer, "missing required argument: --" ++ field.name ++ " or --no-" ++ field.name, .{}, prog, exit_on_error); } else { - return usageError(writer, "missing required argument: --" ++ field.name, .{}, exit_on_error); + return usageError(named_fields, positional_fields, writer, "missing required argument: --" ++ field.name, .{}, prog, exit_on_error); } } } @@ -403,7 +403,7 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: [] if (field.defaultValue()) |default| { @field(result.positional, field.name) = default; } else { - return usageError(writer, "missing required argument: " ++ field.name, .{}, exit_on_error); + return usageError(named_fields, positional_fields, writer, "missing required argument: " ++ field.name, .{}, prog, exit_on_error); } } } @@ -413,22 +413,22 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: [] } /// arg_value is []const u8 or [:0]const u8. -fn parseValue(comptime T: type, arg_value: anytype, comptime field_name: []const u8, writer: ?*Writer, exit_on_error: bool) !T { +fn parseValue(comptime named_fields: []const StructField, comptime positional_fields: []const StructField, 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(writer, "unable to parse --{s}={s}: {s}", .{ field_name, arg_value, @errorName(err) }, exit_on_error); + return usageError(named_fields, positional_fields, 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(writer, "unable to parse --{s}={s}: {s}", .{ field_name, arg_value, @errorName(err) }, exit_on_error); + return usageError(named_fields, positional_fields, 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(writer, "unrecognized value: --{s}={s}, expected one of: {s}", .{ field_name, arg_value, enumValuesExpr(T) }, exit_on_error); + return usageError(named_fields, positional_fields, writer, "unrecognized value: --{s}={s}, expected one of: {s}", .{ field_name, arg_value, enumValuesExpr(T) }, prog, exit_on_error); }; }, .pointer => |ptrInfo| { @@ -574,12 +574,20 @@ fn ArrayListsForFields(comptime fields: []const StructField) type { /// 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. +/// `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 msg: []const u8, args: anytype, options: Options) error{Usage} { - return usageError(options.writer, msg, args, options.exit orelse true); +pub fn @"error"(comptime Args: type, comptime msg: []const u8, msg_args: anytype, options: Options) error{Usage} { + const named_fields, const positional_fields = comptime checkArgsType(Args); + 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(named_fields, positional_fields, options.writer, msg, msg_args, prog orelse "", options.exit orelse true); } test @"error" { @@ -597,25 +605,8 @@ test @"error" { 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"("--output must not be absolute: {s}", .{args.named.output}, .{ .exit = false }); - } -} - -fn usageError(writer: ?*Writer, comptime msg: []const u8, args: anytype, exit_on_error: bool) error{Usage} { - const whole_msg = - "error: " ++ msg ++ "\n" ++ - \\try --help for full help info - \\ - ; - if (writer) |w| { - w.print(whole_msg, args) catch {}; - } else { - std.debug.print(whole_msg, args); + return std.cli.@"error"(Args, "--output must not be absolute: {s}", .{args.named.output}, .{ .exit = false }); } - if (exit_on_error) { - std.process.exit(1); - } - return error.Usage; } fn ArgIteratorSlice(comptime String: type) type { @@ -644,53 +635,125 @@ fn enumValuesExpr(comptime Enum: type) []const u8 { return values_str; } -fn printGeneratedHelp(writer: ?*Writer, prog: []const u8, comptime named_fields: []const StructField) void { - const msg = // - \\usage: {s} [options] [arg...] - \\ - \\arguments:{s} - \\ --help - \\ - ; - comptime var arguments_str: []const u8 = ""; +fn usageError(comptime named_fields: []const StructField, comptime positional_fields: []const StructField, writer: ?*Writer, comptime msg: []const u8, args: anytype, prog: []const u8, exit_on_error: bool) error{Usage} { + 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 named_fields: []const StructField, comptime positional_fields: []const StructField, writer: ?*Writer, prog: []const u8) void { + comptime var arguments_table: []const []const []const u8 = &.{}; + + comptime var arguments_str: []const u8 = ""; // TODO: delete + + if (positional_fields.len > 0) { + arguments_table = arguments_table ++ .{&[_][]const u8{"positional arguments:"}}; + } + //inline for (positional_fields) |field| {} + + arguments_table = arguments_table ++ .{&[_][]const u8{"named arguments:"}}; // The --help option is always there. inline for (named_fields) |field| { switch (@typeInfo(field.type)) { .bool => { if (field.defaultValue()) |default| { if (default) { - arguments_str = arguments_str ++ "\n --no-" ++ field.name ++ " default: --" ++ field.name; + arguments_table = arguments_table ++ .{&[_][]const u8{ " --no-" ++ field.name, "default: --" ++ field.name }}; } else { - arguments_str = arguments_str ++ "\n --" ++ field.name ++ " default: --no-" ++ field.name; + arguments_table = arguments_table ++ .{&[_][]const u8{ " --" ++ field.name, "default: --no-" ++ field.name }}; } } else { - arguments_str = arguments_str ++ "\n --" ++ field.name ++ " or --no-" ++ field.name ++ " required"; + arguments_table = arguments_table ++ .{&[_][]const u8{ " --[no-]" ++ field.name, "required" }}; } }, .int, .float => { - arguments_str = arguments_str ++ "\n --" ++ field.name ++ " " ++ @typeName(field.type); - if (field.defaultValue()) |default| { - arguments_str = arguments_str ++ " default: " ++ std.fmt.comptimePrint("{}", .{default}); - } else { - arguments_str = arguments_str ++ " required"; - } + arguments_table = arguments_table ++ .{&[_][]const u8{ + " --" ++ field.name ++ "=" ++ @typeName(field.type), + if (field.defaultValue()) |default| + "default: " ++ std.fmt.comptimePrint("{}", .{default}) + else + "required", + }}; }, .@"enum" => { - arguments_str = arguments_str ++ "\n --" ++ field.name ++ " " ++ comptime enumValuesExpr(field.type); - if (field.defaultValue()) |default| { - arguments_str = arguments_str ++ " default: " ++ quoteIfEmpty(@tagName(default)); - } else { - arguments_str = arguments_str ++ " required"; - } + arguments_table = arguments_table ++ .{&[_][]const u8{ + " --" ++ field.name ++ "=" ++ comptime enumValuesExpr(field.type), + if (field.defaultValue()) |default| + "default: " ++ @tagName(default) + else + "required", + }}; }, .pointer => |ptrInfo| { if (ptrInfo.size == .slice and ptrInfo.child == u8) { - // String. - arguments_str = arguments_str ++ "\n --" ++ field.name ++ " string"; - if (field.defaultValue()) |default| { - arguments_str = arguments_str ++ " default: " ++ quoteIfEmpty(default); - } else { - arguments_str = arguments_str ++ " required"; - } + // String + arguments_table = arguments_table ++ .{&[_][]const u8{ + " --" ++ field.name ++ "=string", + if (field.defaultValue()) |default| + "default: " ++ quoteIfEmpty(default) + else + "required", + }}; } else { // Array const type_name = switch (@typeInfo(ptrInfo.child)) { @@ -703,18 +766,43 @@ fn printGeneratedHelp(writer: ?*Writer, prog: []const u8, comptime named_fields: arguments_str = arguments_str ++ "\n " ++ // "--" ++ field.name ++ " " ++ type_name ++ " " ++ // "[--" ++ field.name ++ " " ++ type_name ++ " ...]"; + arguments_table = arguments_table ++ .{&[_][]const u8{ + " --" ++ field.name ++ "=" ++ type_name ++ " " ++ // + "[--" ++ field.name ++ "=" ++ type_name ++ " ...]", + }}; } }, - else => @compileError("Unsupported field type: " ++ @typeName(field.type)), + 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 = ""; + 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, arguments_str }) catch {}; + 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, arguments_str }) catch {}; + file_writer.interface.print(msg, .{prog}) catch {}; file_writer.interface.flush() catch {}; } } @@ -724,6 +812,26 @@ inline fn quoteIfEmpty(comptime s: []const u8) []const u8 { 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 }; @@ -1310,8 +1418,8 @@ test "actually calling error" { "--output=/absolute/path", "too", "many", "other", "args", }, .{}); - try testing.expectEqual(error.Usage, std.cli.@"error"("--output must not be absolute: {s}", .{args.named.output}, silent_options)); - try testing.expectEqual(error.Usage, std.cli.@"error"("expected exactly 1 positional arg", .{}, silent_options)); + 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" { From bc05dc4c7db6e32131af7127d49b10486805ad93 Mon Sep 17 00:00:00 2001 From: Josh Wolfe Date: Fri, 29 Aug 2025 20:32:46 -0400 Subject: [PATCH 08/13] typo --- lib/std/cli.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/std/cli.zig b/lib/std/cli.zig index 618246d24314..12bfe1200f64 100644 --- a/lib/std/cli.zig +++ b/lib/std/cli.zig @@ -160,7 +160,7 @@ test parse { /// First positional (non-named) argument: input: [:0]const u8 = "", /// Second positional argument is declared as optional: - reptitions: u32 = 1, + repititions: u32 = 1, /// Receives the rest of the positional arguments. @"the-rest": []const [:0]const u8 = &.{}, }, From 5278e4dcb8dd251142a3a0443b0391697ffc22b6 Mon Sep 17 00:00:00 2001 From: Josh Wolfe Date: Fri, 29 Aug 2025 20:38:16 -0400 Subject: [PATCH 09/13] fix tests --- lib/std/cli.zig | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/std/cli.zig b/lib/std/cli.zig index 12bfe1200f64..f8109f6b5d1e 100644 --- a/lib/std/cli.zig +++ b/lib/std/cli.zig @@ -1263,10 +1263,9 @@ test "help" { // 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(), "--str=string") != null); try testing.expect(mem.indexOf(u8, aw.written(), "--int") != null); - try testing.expect(mem.indexOf(u8, aw.written(), "--flag") != null); - try testing.expect(mem.indexOf(u8, aw.written(), "--no-flag") != null); + try testing.expect(mem.indexOf(u8, aw.written(), "--[no-]flag") != null); try testing.expect(mem.indexOf(u8, aw.written(), "--help") != null); aw.clearRetainingCapacity(); From 96f353fc22652cefcaf24374f2e463baee237cad Mon Sep 17 00:00:00 2001 From: Josh Wolfe Date: Sat, 30 Aug 2025 08:26:12 -0400 Subject: [PATCH 10/13] help for positional --- lib/std/cli.zig | 133 +++++++++++++++++++++++++++++++++--------------- 1 file changed, 91 insertions(+), 42 deletions(-) diff --git a/lib/std/cli.zig b/lib/std/cli.zig index f8109f6b5d1e..8c2849eac633 100644 --- a/lib/std/cli.zig +++ b/lib/std/cli.zig @@ -293,7 +293,7 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: [] file_writer.interface.flush() catch {}; } } else { - printGeneratedHelp(named_fields, positional_fields, writer, prog); + printGeneratedHelp(Args, writer, prog); } if (exit_on_error) { std.process.exit(0); @@ -304,7 +304,7 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: [] 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(named_fields, positional_fields, writer, "unrecognized argument: {s}", .{arg}, prog, exit_on_error); + 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. @@ -314,14 +314,14 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: [] 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(named_fields, positional_fields, writer, "unexpected positional argument: {s}", .{arg}, prog, exit_on_error); + 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(named_fields, positional_fields, C, arg, field.name, writer, prog, exit_on_error)); + 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(named_fields, positional_fields, field.type, arg, field.name, writer, prog, exit_on_error); + @field(result.positional, field.name) = try parseValue(Args, field.type, arg, field.name, writer, prog, exit_on_error); positional_field_index += 1; } break; @@ -349,25 +349,25 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: [] if (mem.eql(u8, field.name, arg_name)) { named_fields_seen[i] = true; if (field.type == bool) { - if (immediate_value != null) return usageError(named_fields, positional_fields, writer, "cannot specify value for bool argument: {s}", .{arg}, prog, exit_on_error); + 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(named_fields, positional_fields, writer, "unrecognized argument: {s}", .{arg}, prog, exit_on_error); + 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(named_fields, positional_fields, writer, "expected argument after --{s}", .{field.name}, prog, exit_on_error); + 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(named_fields, positional_fields, C, arg_value, field.name, writer, prog, exit_on_error)); + 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(named_fields, positional_fields, field.type, arg_value, field.name, writer, prog, exit_on_error); + @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(named_fields, positional_fields, writer, "unrecognized argument: {s}", .{arg}, prog, exit_on_error); + return usageError(Args, writer, "unrecognized argument: {s}", .{arg}, prog, exit_on_error); } } @@ -384,9 +384,9 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: [] @field(result.named, field.name) = default; } else { if (field.type == bool) { - return usageError(named_fields, positional_fields, writer, "missing required argument: --" ++ field.name ++ " or --no-" ++ field.name, .{}, prog, exit_on_error); + return usageError(Args, writer, "missing required argument: --" ++ field.name ++ " or --no-" ++ field.name, .{}, prog, exit_on_error); } else { - return usageError(named_fields, positional_fields, writer, "missing required argument: --" ++ field.name, .{}, prog, exit_on_error); + return usageError(Args, writer, "missing required argument: --" ++ field.name, .{}, prog, exit_on_error); } } } @@ -403,7 +403,7 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: [] if (field.defaultValue()) |default| { @field(result.positional, field.name) = default; } else { - return usageError(named_fields, positional_fields, writer, "missing required argument: " ++ field.name, .{}, prog, exit_on_error); + return usageError(Args, writer, "missing required argument: " ++ field.name, .{}, prog, exit_on_error); } } } @@ -413,22 +413,22 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: [] } /// arg_value is []const u8 or [:0]const u8. -fn parseValue(comptime named_fields: []const StructField, comptime positional_fields: []const StructField, comptime T: type, arg_value: anytype, comptime field_name: []const u8, writer: ?*Writer, prog: []const u8, exit_on_error: bool) !T { +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(named_fields, positional_fields, writer, "unable to parse --{s}={s}: {s}", .{ field_name, arg_value, @errorName(err) }, prog, exit_on_error); + 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(named_fields, positional_fields, writer, "unable to parse --{s}={s}: {s}", .{ field_name, arg_value, @errorName(err) }, prog, exit_on_error); + 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(named_fields, positional_fields, writer, "unrecognized value: --{s}={s}, expected one of: {s}", .{ field_name, arg_value, enumValuesExpr(T) }, prog, exit_on_error); + return usageError(Args, writer, "unrecognized value: --{s}={s}, expected one of: {s}", .{ field_name, arg_value, enumValuesExpr(T) }, prog, exit_on_error); }; }, .pointer => |ptrInfo| { @@ -451,8 +451,8 @@ fn checkArgsType(comptime Args: type) struct { []const StructField, []const Stru } else @compileError("unrecognized Args name: " ++ field.name); } - const named_fields = if (has_named) @typeInfo(@TypeOf(@as(Args, undefined).named)).@"struct".fields else &.{}; - const positional_fields = if (has_positional) @typeInfo(@TypeOf(@as(Args, undefined).positional)).@"struct".fields else &.{}; + 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| { @@ -508,15 +508,16 @@ fn validateField(field: StructField) void { // String. } else { // Array. - if (field.default_value_ptr == null) @compileError("Array arguments must have a default value: " ++ field.name); + 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) @compileError("Unsupported field type: " ++ @typeName(field.type)); - if (ptrInfo2.child == u8) { + if (ptrInfo2.size == .slice and ptrInfo2.child == u8) { // String. } else { @compileError("Unsupported field type: " ++ @typeName(field.type)); @@ -579,7 +580,6 @@ fn ArrayListsForFields(comptime fields: []const StructField) type { /// 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} { - const named_fields, const positional_fields = comptime checkArgsType(Args); var buf: [0x1000]u8 = undefined; const prog: ?[]const u8 = options.prog orelse blk: { var fba: std.heap.FixedBufferAllocator = .init(&buf); @@ -587,7 +587,7 @@ pub fn @"error"(comptime Args: type, comptime msg: []const u8, msg_args: anytype const argv0 = iter.next(); break :blk if (argv0) |arg| std.fs.path.basename(arg) else null; }; - return usageError(named_fields, positional_fields, options.writer, msg, msg_args, prog orelse "", options.exit orelse true); + return usageError(Args, options.writer, msg, msg_args, prog orelse "", options.exit orelse true); } test @"error" { @@ -635,7 +635,8 @@ fn enumValuesExpr(comptime Enum: type) []const u8 { return values_str; } -fn usageError(comptime named_fields: []const StructField, comptime positional_fields: []const StructField, writer: ?*Writer, comptime msg: []const u8, args: anytype, prog: []const u8, exit_on_error: bool) error{Usage} { +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" ++ @@ -702,17 +703,67 @@ fn usageLineFmt(comptime named_fields: []const StructField, comptime positional_ } return escapeFmt(usage_str); } -fn printGeneratedHelp(comptime named_fields: []const StructField, comptime positional_fields: []const StructField, writer: ?*Writer, prog: []const u8) void { - comptime var arguments_table: []const []const []const u8 = &.{}; +fn printGeneratedHelp(comptime Args: type, writer: ?*Writer, prog: []const u8) void { + const named_fields, const positional_fields = comptime checkArgsType(Args); - comptime var arguments_str: []const u8 = ""; // TODO: delete + comptime var arguments_table: []const []const []const u8 = &.{}; if (positional_fields.len > 0) { - arguments_table = arguments_table ++ .{&[_][]const u8{"positional arguments:"}}; + 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", + }}; + }, + .@"enum" => { + arguments_table = arguments_table ++ .{&[_][]const u8{ + " " ++ field.name, + comptime enumValuesExpr(field.type) ++ ". " ++ + if (field.defaultValue()) |default| + "default: " ++ @tagName(default) + else + "required", + }}; + }, + .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", + }}; + } 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", + }}; + } + }, + else => comptime unreachable, + } } - //inline for (positional_fields) |field| {} - arguments_table = arguments_table ++ .{&[_][]const u8{"named arguments:"}}; // The --help option is always there. + arguments_table = arguments_table ++ .{ &[_][]const u8{""}, &[_][]const u8{"named arguments:"} }; inline for (named_fields) |field| { switch (@typeInfo(field.type)) { .bool => { @@ -737,11 +788,12 @@ fn printGeneratedHelp(comptime named_fields: []const StructField, comptime posit }, .@"enum" => { arguments_table = arguments_table ++ .{&[_][]const u8{ - " --" ++ field.name ++ "=" ++ comptime enumValuesExpr(field.type), - if (field.defaultValue()) |default| - "default: " ++ @tagName(default) - else - "required", + " --" ++ field.name ++ "=enum", + comptime enumValuesExpr(field.type) ++ " " ++ + if (field.defaultValue()) |default| + "default: " ++ @tagName(default) + else + "required", }}; }, .pointer => |ptrInfo| { @@ -763,12 +815,9 @@ fn printGeneratedHelp(comptime named_fields: []const StructField, comptime posit .pointer => "string", // The array-of-pointer that doesn't cause compile errors elsewhere. else => comptime unreachable, }; - arguments_str = arguments_str ++ "\n " ++ // - "--" ++ field.name ++ " " ++ type_name ++ " " ++ // - "[--" ++ field.name ++ " " ++ type_name ++ " ...]"; arguments_table = arguments_table ++ .{&[_][]const u8{ - " --" ++ field.name ++ "=" ++ type_name ++ " " ++ // - "[--" ++ field.name ++ "=" ++ type_name ++ " ...]", + " --" ++ field.name ++ "=" ++ type_name, + "can be specified multiple times", }}; } }, From 473c1d6fa5bea6dcb03151a304af645910826be8 Mon Sep 17 00:00:00 2001 From: Josh Wolfe Date: Sat, 30 Aug 2025 11:57:23 -0400 Subject: [PATCH 11/13] support extra help decoration --- lib/std/cli.zig | 76 +++++++++++++++++++++++++++++++++++-------------- 1 file changed, 54 insertions(+), 22 deletions(-) diff --git a/lib/std/cli.zig b/lib/std/cli.zig index 8c2849eac633..d9c380c31a94 100644 --- a/lib/std/cli.zig +++ b/lib/std/cli.zig @@ -40,15 +40,37 @@ pub const Error = error{ /// `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 = &.{}, /// }, /// }; /// ``` -/// Either or both of `named` and `positional` may be omitted, which is effectively equivalent to them having no fields. +/// 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. /// @@ -160,7 +182,7 @@ test parse { /// First positional (non-named) argument: input: [:0]const u8 = "", /// Second positional argument is declared as optional: - repititions: u32 = 1, + repetitions: u32 = 1, /// Receives the rest of the positional arguments. @"the-rest": []const [:0]const u8 = &.{}, }, @@ -717,20 +739,20 @@ fn printGeneratedHelp(comptime Args: type, writer: ?*Writer, prog: []const u8) v arguments_table = arguments_table ++ .{&[_][]const u8{ " " ++ field.name, @typeName(field.type) ++ " " ++ - if (field.defaultValue()) |default| + (if (field.defaultValue()) |default| "default: " ++ std.fmt.comptimePrint("{}", .{default}) else - "required", + "required") ++ argHelp(Args, "positional", field.name), }}; }, .@"enum" => { arguments_table = arguments_table ++ .{&[_][]const u8{ " " ++ field.name, comptime enumValuesExpr(field.type) ++ ". " ++ - if (field.defaultValue()) |default| + (if (field.defaultValue()) |default| "default: " ++ @tagName(default) else - "required", + "required") ++ argHelp(Args, "positional", field.name), }}; }, .pointer => |ptrInfo| { @@ -739,10 +761,10 @@ fn printGeneratedHelp(comptime Args: type, writer: ?*Writer, prog: []const u8) v arguments_table = arguments_table ++ .{&[_][]const u8{ " " ++ field.name, "string. " ++ - if (field.defaultValue()) |default| + (if (field.defaultValue()) |default| "default: " ++ quoteIfEmpty(default) else - "required", + "required") ++ argHelp(Args, "positional", field.name), }}; } else { // Array @@ -755,7 +777,7 @@ fn printGeneratedHelp(comptime Args: type, writer: ?*Writer, prog: []const u8) v }; arguments_table = arguments_table ++ .{&[_][]const u8{ " " ++ field.name, - type_name ++ ". can be specified multiple times", + type_name ++ ". can be specified multiple times" ++ argHelp(Args, "positional", field.name), }}; } }, @@ -769,31 +791,31 @@ fn printGeneratedHelp(comptime Args: type, writer: ?*Writer, prog: []const u8) v .bool => { if (field.defaultValue()) |default| { if (default) { - arguments_table = arguments_table ++ .{&[_][]const u8{ " --no-" ++ field.name, "default: --" ++ field.name }}; + 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 }}; + 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" }}; + 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| + (if (field.defaultValue()) |default| "default: " ++ std.fmt.comptimePrint("{}", .{default}) else - "required", + "required") ++ argHelp(Args, "named", field.name), }}; }, .@"enum" => { arguments_table = arguments_table ++ .{&[_][]const u8{ " --" ++ field.name ++ "=enum", - comptime enumValuesExpr(field.type) ++ " " ++ - if (field.defaultValue()) |default| + comptime enumValuesExpr(field.type) ++ ". " ++ + (if (field.defaultValue()) |default| "default: " ++ @tagName(default) else - "required", + "required") ++ argHelp(Args, "named", field.name), }}; }, .pointer => |ptrInfo| { @@ -801,10 +823,10 @@ fn printGeneratedHelp(comptime Args: type, writer: ?*Writer, prog: []const u8) v // String arguments_table = arguments_table ++ .{&[_][]const u8{ " --" ++ field.name ++ "=string", - if (field.defaultValue()) |default| + (if (field.defaultValue()) |default| "default: " ++ quoteIfEmpty(default) else - "required", + "required") ++ argHelp(Args, "named", field.name), }}; } else { // Array @@ -817,7 +839,7 @@ fn printGeneratedHelp(comptime Args: type, writer: ?*Writer, prog: []const u8) v }; arguments_table = arguments_table ++ .{&[_][]const u8{ " --" ++ field.name ++ "=" ++ type_name, - "can be specified multiple times", + "can be specified multiple times" ++ argHelp(Args, "named", field.name), }}; } }, @@ -833,6 +855,9 @@ fn printGeneratedHelp(comptime Args: type, writer: ?*Writer, prog: []const u8) v } 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| { @@ -856,6 +881,13 @@ fn printGeneratedHelp(comptime Args: type, writer: ?*Writer, prog: []const u8) v } } +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; From 69c1dbc9ff9f5031f7262d6079b83285f5fc0c86 Mon Sep 17 00:00:00 2001 From: Josh Wolfe Date: Sat, 30 Aug 2025 22:15:37 -0400 Subject: [PATCH 12/13] update tools/ to use std.cli.parse --- tools/docgen.zig | 57 +++++-------------- tools/dump-cov.zig | 12 +++- tools/fetch_them_macos_headers.zig | 58 +++++-------------- tools/gen_macos_headers_c.zig | 26 ++------- tools/gen_outline_atomics.zig | 2 +- tools/gen_spirv_spec.zig | 38 +++++-------- tools/gen_stubs.zig | 8 ++- tools/generate_JSONTestSuite.zig | 6 +- tools/generate_c_size_and_align_checks.zig | 21 +++---- tools/generate_linux_syscalls.zig | 29 ++++------ tools/incr-check.zig | 65 ++++++++-------------- tools/migrate_langref.zig | 11 +++- tools/process_headers.zig | 65 +++++----------------- tools/update-linux-headers.zig | 45 ++++----------- tools/update_clang_options.zig | 40 +++---------- tools/update_cpu_features.zig | 62 ++++++--------------- tools/update_crc_catalog.zig | 23 ++------ tools/update_freebsd_libc.zig | 11 +++- tools/update_glibc.zig | 11 +++- tools/update_mingw.zig | 11 +++- tools/update_netbsd_libc.zig | 11 +++- 21 files changed, 214 insertions(+), 398 deletions(-) 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}); From 8de26d5382a0f34db62f6e7d3b45b73ef801588f Mon Sep 17 00:00:00 2001 From: Josh Wolfe Date: Sat, 30 Aug 2025 23:46:10 -0400 Subject: [PATCH 13/13] add tests for new help stuff --- lib/std/cli.zig | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/lib/std/cli.zig b/lib/std/cli.zig index d9c380c31a94..c3e3fd84d80a 100644 --- a/lib/std/cli.zig +++ b/lib/std/cli.zig @@ -1532,3 +1532,43 @@ test "custom help" { 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); +}