authorgravatar for thejoshwolfe@gmail.comJosh Wolfe <thejoshwolfe@gmail.com> 2025-08-24 21:59:49-04:00
committergravatar for thejoshwolfe@gmail.comJosh Wolfe <thejoshwolfe@gmail.com> 2025-08-30 22:36:30-04:00
logf2753f5910b320b53c26fa6e0e145bc934119cb8
treedf488513967470dee41192be7720c296ad4c4609
parentc6f3e4bd5aacef63fa98bf025de13c29192b84c7

change positional to a struct


1 files changed, 514 insertions(+), 357 deletions(-)

lib/std/cli.zig+514-357
......@@ -7,6 +7,7 @@ const isAlphabetic = std.ascii.isAlphabetic;
77const Writer = std.Io.Writer;
88const ArgIterator = std.process.ArgIterator;
99const ArenaAllocator = std.heap.ArenaAllocator;
10const StructField = std.builtin.Type.StructField;
1011const mem = std.mem;
1112const Allocator = mem.Allocator;
1213
......@@ -29,23 +30,25 @@ pub const Options = struct {
2930pub const Error = error{
3031 /// Caused by unrecognized option names, values that cannot be parsed into the appropriate field type,
3132 /// missing arguments for fields with no default value, and other similar parsing errors.
33 /// See also `options.exit`, which can supersede this error.
3234 Usage,
33 /// The --help argument was given.
35 /// The --help argument was given (and `options.exit` resolved to `false`).
3436 Help,
3537} || Allocator.Error;
3638
3739/// Parses CLI args from a `std.process.ArgIterator` according to the configuration in `Args`.
38/// Args is a struct that you define looking like this:
40/// `Args` is a struct that you define looking like this:
3941/// ```
4042/// const Args = struct {
4143/// named: struct {
4244/// // ...
4345/// },
44/// positional: []const []const u8 = &.{},
46/// positional: struct {
47/// // ...
48/// },
4549/// };
4650/// ```
47/// The `named` and `positional` fields are required, although `named` need not have any subfields.
48/// `positional` may instead have type `[]const [:0]const u8`.
51/// Either or both of `named` and `positional` may be omitted, which is effectively equivalent to them having no fields.
4952///
5053/// The sequence of arg strings from the `ArgIterator` is parsed to determine named and positional arguments.
5154///
......@@ -62,7 +65,7 @@ pub const Error = error{
6265/// Forms (1), (2), and (3) must correspond to a field `Args.named.<name>`; see below for named argument handling.
6366/// Form (4) immediately prints the long help documentation and exits or returns `error.Help` depending on options.exit.
6467/// Form (6) signals that all following arg strings are positional.
65/// Form (7) and all arg strings following form (6) are appended into the `positional` array in order.
68/// Form (7) and all arg strings following form (6) are considered positional arguments, discussed below.
6669///
6770/// Form (5) is always an error.
6871/// 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{
8891/// Slice arguments `[]const C` (where `C` is not `u8`) must have a default value, usually `&.{}`.
8992/// If a bool argument has no default value, then at least one of `--<name>` or `--no-<name>` must be given.
9093///
94/// Each positional arg string corresponds to a field in `Args.positional` in declaration order.
95/// Each field in `Args.positional` may have a default value, making the corresponding argument optional.
96/// Fields for required positional arguments must precede fields for optional arguments.
97/// For each field, let `T` be its type.
98/// Similar to `Args.named` described above, `T` may be any of the following:
99/// any integer, any float, any `enum` with at least 1 member, or any string that `[:0]const u8` can coerce into.
100/// Only the last declared field of `Args.positional` may alternatively have type `[]const C` where `C` is one of:
101/// any integer, any float, any `enum` with at least 1 member, or any string that `[:0]const u8` can coerce into.
102/// Similar to `Args.named`, a positional field declared with such a `[]const C` must have a default value, usually `&.{}`.
103/// Such a `[]const C` field corresponds to all positional arguments after the positional arguments for the other fields.
104///
91105/// It's possible to override the automatically-generated long help documentation by declaring a public constant named `help` in `Args`.
92106/// The value must coerce to `[]const u8`.
93107///
......@@ -104,7 +118,6 @@ pub const Error = error{
104118/// named: struct {
105119/// // [...]
106120/// },
107/// positional: []const []const u8 = &.{},
108121/// };
109122/// ```
110123///
......@@ -136,13 +149,21 @@ test parse {
136149 level: i8 = -1,
137150 /// Parsed as the name of the member `--color=never`.
138151 color: enum { auto, never, always } = .auto,
139 /// --seed=0x<something> is actually passed in by the `zig test` system (as of 0.14.1), which we receive here.
152
153 // The below parameters are actually passed into the `zig test` process,
154 // so we have to receive them here (as of zig 0.15.1).
140155 seed: u32 = 0,
141156 @"cache-dir": []const u8 = "",
142157 listen: []const u8 = "",
143158 },
144 /// Receives the rest of the arguments.
145 positional: []const [:0]const u8 = &.{},
159 positional: struct {
160 /// First positional (non-named) argument:
161 input: [:0]const u8 = "",
162 /// Second positional argument is declared as optional:
163 reptitions: u32 = 1,
164 /// Receives the rest of the positional arguments.
165 @"the-rest": []const [:0]const u8 = &.{},
166 },
146167 };
147168
148169 var arena: ArenaAllocator = .init(testing.allocator);
......@@ -157,8 +178,8 @@ test parse {
157178/// ```
158179/// pub fn next(self: *Self) ?String { ... }
159180/// ```
160/// Where `String` is `[]const u8` or `[:0]const u8`, or something else that coerces to `[]const u8`.
161/// If `String` does not coerce to `[:0]const u8`, then `Args` cannot have `[:0]const u8` fields.
181/// Where `String` is `[]const u8` or `[:0]const u8` or something else that coerces to `[]const u8`.
182/// If `String` does not coerce to `[:0]const u8`, then `Args` cannot have any `[:0]const u8` in its fields.
162183///
163184/// The first string arg returned by the `iter` (`argv[0]`) is skipped by all the parsing logic.
164185/// If `options.prog` is `null`, then the final path component of `argv[0]` is used by default.
......@@ -170,7 +191,7 @@ test parse {
170191///
171192/// An `ArenaAllocator` is recommended to cleanup the memory allocated from this function;
172193/// however, it's also possible to free all the memory by freeing every slice field `[]const C` (other than `u8`)
173/// in the returned `args.named` as well as freeing `args.positional`.
194/// in the returned `args.named` and `args.positional`.
174195pub fn parseIter(comptime Args: type, arena: Allocator, iter: anytype, options: Options) Error!Args {
175196 const argv0 = iter.next();
176197 const prog = options.prog orelse if (argv0) |arg| std.fs.path.basename(arg) else "<prog>";
......@@ -179,7 +200,7 @@ pub fn parseIter(comptime Args: type, arena: Allocator, iter: anytype, options:
179200
180201/// Like `parse`, but takes a slice of strings in place of using an `ArgIterator`.
181202/// `argv` must be either be a slice of `String` or a single-item pointer to an array of `String`,
182/// where `String` is `[]const u8` or `[:0]const u8` or something that coerces to `[]const u8`.
203/// where `String` is `[]const u8` or `[:0]const u8` or something else that coerces to `[]const u8`.
183204/// If `String` does not coerce to `[:0]const u8`, then `Args` cannot have `[:0]const u8` fields.
184205///
185206/// 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:
192213///
193214/// An `ArenaAllocator` is recommended to cleanup the memory allocated from this function;
194215/// however, it's also possible to free all the memory by freeing every slice field `[]const C` (other than `u8`)
195/// in the returned `args.named` as well as freeing `args.positional`.
216/// in the returned `args.named` and `args.positional`.
196217pub fn parseSlice(comptime Args: type, arena: Allocator, argv: anytype, options: Options) Error!Args {
197218 const argvInfo = @typeInfo(@TypeOf(argv)).pointer;
198219 const String = if (argvInfo.size == .one)
......@@ -218,7 +239,9 @@ test parseSlice {
218239 flag: bool = true,
219240 @"enum-option": enum { auto, always, never } = .auto,
220241 },
221 positional: []const []const u8 = &.{},
242 positional: struct {
243 args: []const []const u8 = &.{},
244 },
222245 };
223246 const args = try parseSlice(Args, allocator, &[_][]const u8{
224247 "--example_required", "a.txt",
......@@ -238,7 +261,7 @@ test parseSlice {
238261 .flag = false,
239262 .@"enum-option" = .always,
240263 },
241 .positional = &.{ "positional1", "positional2", "-12345678", "--positional4", "--positional=5" },
264 .positional = .{ .args = &.{ "positional1", "positional2", "-12345678", "--positional4", "--positional=5" } },
242265 }, args);
243266}
244267
......@@ -246,42 +269,19 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: []
246269 // argv0 has already been consumed.
247270
248271 // Do all comptime checks up front so that we can be sure any compile error the user sees is the one we wrote.
249 comptime checkArgsType(Args);
272 const named_fields, const positional_fields = comptime checkArgsType(Args);
273
274 var named_array_lists = arrayListsForFields(named_fields);
275 var positional_array_lists = arrayListsForFields(positional_fields);
250276
251277 var result: Args = undefined;
252 var positional: ArrayList(@typeInfo(@TypeOf(result.positional)).pointer.child) = .{};
278 var named_fields_seen = [_]bool{false} ** named_fields.len;
279 var positional_field_index: usize = 0;
253280
254 const ArgsNamed = @TypeOf(result.named);
255 const named_info = @typeInfo(ArgsNamed).@"struct";
256
257 // Declare and initialize an ArrayList(C) for every []const C field (other than u8).
258 var fields_seen = [_]bool{false} ** named_info.fields.len;
259 comptime var array_list_fields: []const std.builtin.Type.StructField = &.{};
260 inline for (named_info.fields) |field| {
261 const info = @typeInfo(field.type);
262 if (info == .pointer) {
263 comptime assert(info.pointer.size == .slice);
264 if (info.pointer.child == u8) {
265 // String. skip.
266 } else {
267 // Array of scalar.
268 array_list_fields = array_list_fields ++ @as([]const std.builtin.Type.StructField, &.{.{
269 .name = field.name,
270 .type = ArrayList(info.pointer.child),
271 .default_value_ptr = null,
272 .is_comptime = false,
273 .alignment = @alignOf(ArrayList(info.pointer.child)),
274 }});
275 }
276 }
277 }
278 var array_lists: @Type(.{ .@"struct" = .{ .layout = .auto, .fields = array_list_fields, .decls = &.{}, .is_tuple = false } }) = undefined;
279 inline for (@typeInfo(@TypeOf(array_lists)).@"struct".fields) |field| {
280 @field(array_lists, field.name) = .{};
281 }
281 var the_rest_is_positional = false;
282282
283283 while (iter.next()) |arg| {
284 if (mem.eql(u8, arg, "--help")) {
284 if (!the_rest_is_positional and mem.eql(u8, arg, "--help")) {
285285 if (@hasDecl(Args, "help")) {
286286 // Custom help.
287287 if (writer) |w| {
......@@ -293,7 +293,7 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: []
293293 file_writer.interface.flush() catch {};
294294 }
295295 } else {
296 printGeneratedHelp(writer, prog, named_info);
296 printGeneratedHelp(writer, prog, named_fields);
297297 }
298298 if (exit_on_error) {
299299 std.process.exit(0);
......@@ -301,22 +301,32 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: []
301301 return error.Help;
302302 }
303303
304 if (arg.len >= 2 and arg[0] == '-' and isAlphabetic(arg[1])) {
304 if (!the_rest_is_positional and arg.len >= 2 and arg[0] == '-' and isAlphabetic(arg[1])) {
305305 // Always invalid.
306306 // Examples: -h, -flag, -I/path
307307 return usageError(writer, "unrecognized argument: {s}", .{arg}, exit_on_error);
308308 }
309 if (mem.eql(u8, arg, "--")) {
309 if (!the_rest_is_positional and mem.eql(u8, arg, "--")) {
310310 // Stop recognizing named arguments. Everything else is positional.
311 while (iter.next()) |arg2| {
312 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`.
313 }
314 break;
311 the_rest_is_positional = true;
312 continue;
315313 }
316 if (!(arg.len >= 3 and arg[0] == '-' and arg[1] == '-')) {
314 if (the_rest_is_positional or !(arg.len >= 3 and arg[0] == '-' and arg[1] == '-')) {
317315 // Positional.
318 // Examples: "", "a", "-", "-1",
319 try positional.append(allocator, arg);
316 // Examples: "", "a", "-", "-1", "other"
317 if (positional_field_index >= positional_fields.len) return usageError(writer, "unexpected positional argument: {s}", .{arg}, exit_on_error);
318 inline for (positional_fields, 0..) |field, i| {
319 if (positional_field_index == i) {
320 if (getArrayChild(field.type)) |C| {
321 try @field(positional_array_lists, field.name).append(allocator, try parseValue(C, arg, field.name, writer, exit_on_error));
322 // Don't increment positional_field_index.
323 } else {
324 @field(result.positional, field.name) = try parseValue(field.type, arg, field.name, writer, exit_on_error);
325 positional_field_index += 1;
326 }
327 break;
328 }
329 } else unreachable;
320330 continue;
321331 }
322332
......@@ -335,12 +345,12 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: []
335345 break :blk .{ arg["--".len..], null, false };
336346 };
337347
338 inline for (named_info.fields, 0..) |field, i| {
348 inline for (named_fields, 0..) |field, i| {
339349 if (mem.eql(u8, field.name, arg_name)) {
350 named_fields_seen[i] = true;
340351 if (field.type == bool) {
341352 if (immediate_value != null) return usageError(writer, "cannot specify value for bool argument: {s}", .{arg}, exit_on_error);
342353 @field(result.named, field.name) = !no_prefixed;
343 fields_seen[i] = true;
344354 break;
345355 }
346356 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: []
348358 // All other argument types require a value.
349359 const arg_value = immediate_value orelse iter.next() orelse return usageError(writer, "expected argument after --{s}", .{field.name}, exit_on_error);
350360
351 switch (@typeInfo(field.type)) {
352 .bool => unreachable, // Handled above.
353 .float => {
354 @field(result.named, field.name) = std.fmt.parseFloat(field.type, arg_value) catch |err| {
355 return usageError(writer, "unable to parse --{s}={s}: {s}", .{ field.name, arg_value, @errorName(err) }, exit_on_error);
356 };
357 },
358 .int => {
359 @field(result.named, field.name) = std.fmt.parseInt(field.type, arg_value, 0) catch |err| {
360 return usageError(writer, "unable to parse --{s}={s}: {s}", .{ field.name, arg_value, @errorName(err) }, exit_on_error);
361 };
362 },
363 .@"enum" => {
364 @field(result.named, field.name) = std.meta.stringToEnum(field.type, arg_value) orelse {
365 return usageError(writer, "unrecognized value: --{s}={s}, expected one of: {s}", .{ field.name, arg_value, enumValuesExpr(field.type) }, exit_on_error);
366 };
367 },
368 .pointer => |ptrInfo| {
369 comptime assert(ptrInfo.size == .slice);
370 if (ptrInfo.child == u8) {
371 @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`.
372 } else {
373 const array_list = &@field(array_lists, field.name);
374 switch (@typeInfo(ptrInfo.child)) {
375 .bool => comptime unreachable, // Nicer compile error emitted in checkArgsType().
376 .float => {
377 try array_list.append(allocator, std.fmt.parseFloat(ptrInfo.child, arg_value) catch |err| {
378 return usageError(writer, "unable to parse --{s}={s}: {s}", .{ field.name, arg_value, @errorName(err) }, exit_on_error);
379 });
380 },
381 .int => {
382 try array_list.append(allocator, std.fmt.parseInt(ptrInfo.child, arg_value, 0) catch |err| {
383 return usageError(writer, "unable to parse --{s}={s}: {s}", .{ field.name, arg_value, @errorName(err) }, exit_on_error);
384 });
385 },
386 .@"enum" => comptime unreachable,
387 .pointer => |ptrInfo2| {
388 comptime assert(ptrInfo2.size == .slice);
389 if (ptrInfo2.child == u8) {
390 // String.
391 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`.
392 } else comptime unreachable;
393 },
394 else => comptime unreachable,
395 }
396 }
397 },
398 else => comptime unreachable,
361 if (getArrayChild(field.type)) |C| {
362 try @field(named_array_lists, field.name).append(allocator, try parseValue(C, arg_value, field.name, writer, exit_on_error));
363 } else {
364 @field(result.named, field.name) = try parseValue(field.type, arg_value, field.name, writer, exit_on_error);
399365 }
400 fields_seen[i] = true;
401366 break;
402367 }
403368 } else {
......@@ -407,74 +372,202 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: []
407372 }
408373
409374 // Fill default values.
410 inline for (named_info.fields, 0..) |field, i| {
411 if (!fields_seen[i]) {
412 if (field.defaultValue()) |default| {
413 @field(result.named, field.name) = default;
414 } else {
415 if (field.type == bool) {
416 return usageError(writer, "missing required argument: --" ++ field.name ++ " or --no-" ++ field.name, .{}, exit_on_error);
375 inline for (named_fields, 0..) |field, i| {
376 if (getArrayChild(field.type)) |_| {
377 // Array.
378 @field(result.named, field.name) = try @field(named_array_lists, field.name).toOwnedSlice(allocator);
379 } else {
380 // Scalar.
381 if (!named_fields_seen[i]) {
382 // Unspecified.
383 if (field.defaultValue()) |default| {
384 @field(result.named, field.name) = default;
417385 } else {
418 return usageError(writer, "missing required argument: --" ++ field.name, .{}, exit_on_error);
386 if (field.type == bool) {
387 return usageError(writer, "missing required argument: --" ++ field.name ++ " or --no-" ++ field.name, .{}, exit_on_error);
388 } else {
389 return usageError(writer, "missing required argument: --" ++ field.name, .{}, exit_on_error);
390 }
419391 }
420392 }
421393 }
422394 }
423
424 // Finalize the array lists.
425 result.positional = try positional.toOwnedSlice(allocator);
426 inline for (@typeInfo(@TypeOf(array_lists)).@"struct".fields) |field| {
427 @field(result.named, field.name) = try @field(array_lists, field.name).toOwnedSlice(allocator);
395 inline for (positional_fields, 0..) |field, i| {
396 if (getArrayChild(field.type)) |_| {
397 // Array.
398 @field(result.positional, field.name) = try @field(positional_array_lists, field.name).toOwnedSlice(allocator);
399 } else {
400 // Scalar.
401 if (positional_field_index <= i) {
402 // Unspecified.
403 if (field.defaultValue()) |default| {
404 @field(result.positional, field.name) = default;
405 } else {
406 return usageError(writer, "missing required argument: " ++ field.name, .{}, exit_on_error);
407 }
408 }
409 }
428410 }
429411
430412 return result;
431413}
432414
433fn checkArgsType(comptime Args: type) void {
434 const args_fields = @typeInfo(Args).@"struct".fields;
435 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");
436 if (args_fields[1].default_value_ptr == null) @compileError("Args.positional must have a default value");
415/// arg_value is []const u8 or [:0]const u8.
416fn parseValue(comptime T: type, arg_value: anytype, comptime field_name: []const u8, writer: ?*Writer, exit_on_error: bool) !T {
417 switch (@typeInfo(T)) {
418 .bool => comptime unreachable, // Handled elsewhere.
419 .float => {
420 return std.fmt.parseFloat(T, arg_value) catch |err| {
421 return usageError(writer, "unable to parse --{s}={s}: {s}", .{ field_name, arg_value, @errorName(err) }, exit_on_error);
422 };
423 },
424 .int => {
425 return std.fmt.parseInt(T, arg_value, 0) catch |err| {
426 return usageError(writer, "unable to parse --{s}={s}: {s}", .{ field_name, arg_value, @errorName(err) }, exit_on_error);
427 };
428 },
429 .@"enum" => {
430 return std.meta.stringToEnum(T, arg_value) orelse {
431 return usageError(writer, "unrecognized value: --{s}={s}, expected one of: {s}", .{ field_name, arg_value, enumValuesExpr(T) }, exit_on_error);
432 };
433 },
434 .pointer => |ptrInfo| {
435 comptime assert(ptrInfo.size == .slice);
436 comptime assert(ptrInfo.child == u8);
437 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`.
438 },
439 else => comptime unreachable,
440 }
441}
437442
438 inline for (@typeInfo(args_fields[0].type).@"struct".fields) |field| {
439 if (field.is_comptime) @compileError("comptime fields are not supported: " ++ field.name);
440 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.");
441 if (comptime mem.startsWith(u8, field.name, "no-")) @compileError("Field name starts with @\"no-\": " ++ field.name ++ ". Note: use a bool type field, and --<name> and --no-<name> will turn it on and off.");
442 if (comptime mem.indexOfScalar(u8, field.name, '=') != null) @compileError("Field name contains @\"=\": " ++ field.name);
443fn checkArgsType(comptime Args: type) struct { []const StructField, []const StructField } {
444 var has_named = false;
445 var has_positional = false;
446 inline for (@typeInfo(Args).@"struct".fields) |field| {
447 if (mem.eql(u8, field.name, "named")) {
448 has_named = true;
449 } else if (mem.eql(u8, field.name, "positional")) {
450 has_positional = true;
451 } else @compileError("unrecognized Args name: " ++ field.name);
452 }
443453
444 switch (@typeInfo(field.type)) {
445 .bool => {},
446 .float => {},
447 .int => {},
448 .@"enum" => {
449 if (@typeInfo(field.type).@"enum".fields.len == 0) @compileError("Empty enums not allowed");
450 },
451 .pointer => |ptrInfo| {
452 if (ptrInfo.size != .slice) @compileError("Unsupported field type: " ++ @typeName(field.type));
453 if (ptrInfo.child == u8) {
454 // String.
455 } else {
456 // Array.
457 if (field.default_value_ptr == null) @compileError("Array arguments must have a default value: " ++ field.name);
458 switch (@typeInfo(ptrInfo.child)) {
459 .bool => @compileError("Unsupported field type: " ++ @typeName(field.type)),
460 .float => {},
461 .int => {},
462 .@"enum" => @compileError("Unsupported field type: " ++ @typeName(field.type)),
463 .pointer => |ptrInfo2| {
464 if (ptrInfo2.size != .slice) @compileError("Unsupported field type: " ++ @typeName(field.type));
465 if (ptrInfo2.child == u8) {
466 // String.
467 } else {
468 @compileError("Unsupported field type: " ++ @typeName(field.type));
469 }
470 },
471 else => @compileError("Unsupported field type: " ++ @typeName(field.type)),
472 }
454 const named_fields = if (has_named) @typeInfo(@TypeOf(@as(Args, undefined).named)).@"struct".fields else &.{};
455 const positional_fields = if (has_positional) @typeInfo(@TypeOf(@as(Args, undefined).positional)).@"struct".fields else &.{};
456
457 // Named arguments are more lenient.
458 inline for (named_fields) |field| {
459 validateField(field);
460 }
461
462 // Positional arguments have stricter rules.
463 var everything_still_required = true;
464 var everything_still_scalar = true;
465 inline for (positional_fields) |field| {
466 if (field.type == bool) @compileError("Args.positional cannot have bool fields: " ++ field.name);
467 validateField(field);
468 const is_scalar = getArrayChild(field.type) == null;
469
470 const is_required = field.default_value_ptr == null;
471
472 // There can only be one array parameter, and it must be last.
473 if (everything_still_scalar) {
474 if (!is_scalar) {
475 everything_still_scalar = false;
476 }
477 } else @compileError("a positional array argument must be last. found: " ++ field.name);
478
479 // Required positional parameters must come first.
480 if (everything_still_required) {
481 if (!is_required) {
482 everything_still_required = false;
483 }
484 } else {
485 if (is_required) @compileError("cannot have a required positional argument after an optional one: " ++ field.name);
486 }
487 }
488
489 return .{ named_fields, positional_fields };
490}
491
492fn validateField(field: StructField) void {
493 if (field.is_comptime) @compileError("comptime fields are not supported: " ++ field.name);
494 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.");
495 if (comptime mem.startsWith(u8, field.name, "no-")) @compileError("Field name starts with @\"no-\": " ++ field.name ++ ". Note: use a bool type field, and --<name> and --no-<name> will turn it on and off.");
496 if (comptime mem.indexOfScalar(u8, field.name, '=') != null) @compileError("Field name contains @\"=\": " ++ field.name);
497
498 switch (@typeInfo(field.type)) {
499 .bool => {},
500 .float => {},
501 .int => {},
502 .@"enum" => {
503 if (@typeInfo(field.type).@"enum".fields.len == 0) @compileError("Empty enums not allowed");
504 },
505 .pointer => |ptrInfo| {
506 if (ptrInfo.size != .slice) @compileError("Unsupported field type: " ++ @typeName(field.type));
507 if (ptrInfo.child == u8) {
508 // String.
509 } else {
510 // Array.
511 if (field.default_value_ptr == null) @compileError("Array arguments must have a default value: " ++ field.name);
512 switch (@typeInfo(ptrInfo.child)) {
513 .bool => @compileError("Unsupported field type: " ++ @typeName(field.type)),
514 .float => {},
515 .int => {},
516 .@"enum" => @compileError("Unsupported field type: " ++ @typeName(field.type)),
517 .pointer => |ptrInfo2| {
518 if (ptrInfo2.size != .slice) @compileError("Unsupported field type: " ++ @typeName(field.type));
519 if (ptrInfo2.child == u8) {
520 // String.
521 } else {
522 @compileError("Unsupported field type: " ++ @typeName(field.type));
523 }
524 },
525 else => @compileError("Unsupported field type: " ++ @typeName(field.type)),
473526 }
474 },
475 else => @compileError("Unsupported field type: " ++ @typeName(field.type)),
527 }
528 },
529 else => @compileError("Unsupported field type: " ++ @typeName(field.type)),
530 }
531}
532
533/// returns null if T is a scalar type.
534fn getArrayChild(comptime T: type) ?type {
535 // This logic assumes the type has already passed validation.
536 return switch (@typeInfo(T)) {
537 .pointer => |ptrInfo| if (ptrInfo.child == u8) null else ptrInfo.child,
538 else => null,
539 };
540}
541
542fn arrayListsForFields(comptime fields: []const StructField) ArrayListsForFields(fields) {
543 var array_lists: ArrayListsForFields(fields) = undefined;
544 inline for (@typeInfo(@TypeOf(array_lists)).@"struct".fields) |field| {
545 @field(array_lists, field.name) = .{};
546 }
547 return array_lists;
548}
549fn ArrayListsForFields(comptime fields: []const StructField) type {
550 // Declare and initialize an ArrayList(C) for every []const C field (other than u8).
551 comptime var array_list_fields: []const StructField = &.{};
552 inline for (fields) |field| {
553 const info = @typeInfo(field.type);
554 if (info == .pointer) {
555 comptime assert(info.pointer.size == .slice);
556 if (info.pointer.child == u8) {
557 // String. skip.
558 } else {
559 // Array of scalar.
560 array_list_fields = array_list_fields ++ @as([]const StructField, &.{.{
561 .name = field.name,
562 .type = ArrayList(info.pointer.child),
563 .default_value_ptr = null,
564 .is_comptime = false,
565 .alignment = @alignOf(ArrayList(info.pointer.child)),
566 }});
567 }
476568 }
477569 }
570 return @Type(.{ .@"struct" = .{ .layout = .auto, .fields = array_list_fields, .decls = &.{}, .is_tuple = false } });
478571}
479572
480573/// If you do your own validation after getting an `args` from `parse` or similar,
......@@ -494,7 +587,9 @@ test @"error" {
494587 named: struct {
495588 output: []const u8 = "",
496589 },
497 positional: []const []const u8 = &.{},
590 positional: struct {
591 input: []const u8,
592 },
498593 };
499594
500595 var arena: std.heap.ArenaAllocator = .init(testing.allocator);
......@@ -504,9 +599,6 @@ test @"error" {
504599 if (std.fs.path.isAbsolute(args.named.output)) {
505600 return std.cli.@"error"("--output must not be absolute: {s}", .{args.named.output}, .{ .exit = false });
506601 }
507 if (args.positional.len > 1) {
508 return std.cli.@"error"("expected exactly 1 positional arg", .{}, .{ .exit = false });
509 }
510602}
511603
512604fn 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 {
552644 return values_str;
553645}
554646
555fn printGeneratedHelp(writer: ?*Writer, prog: []const u8, comptime named_info: std.builtin.Type.Struct) void {
647fn printGeneratedHelp(writer: ?*Writer, prog: []const u8, comptime named_fields: []const StructField) void {
556648 const msg = //
557649 \\usage: {s} [options] [arg...]
558650 \\
......@@ -561,7 +653,7 @@ fn printGeneratedHelp(writer: ?*Writer, prog: []const u8, comptime named_info: s
561653 \\
562654 ;
563655 comptime var arguments_str: []const u8 = "";
564 inline for (named_info.fields) |field| {
656 inline for (named_fields) |field| {
565657 switch (@typeInfo(field.type)) {
566658 .bool => {
567659 if (field.defaultValue()) |default| {
......@@ -635,121 +727,48 @@ inline fn quoteIfEmpty(comptime s: []const u8) []const u8 {
635727var failing_writer: Writer = .failing;
636728const silent_options = Options{ .writer = &failing_writer, .exit = false };
637729
638test "usage errors" {
730test "bool" {
639731 var arena: std.heap.ArenaAllocator = .init(testing.allocator);
640732 defer arena.deinit();
641733 const allocator = arena.allocator();
642 var aw: Writer.Allocating = .init(allocator);
643 const options = Options{ .prog = "test-prog", .writer = &aw.writer };
644
645 // unrecognized argument
646 aw.clearRetainingCapacity();
647 try testing.expectError(error.Usage, parseSlice(struct {
648 named: struct {
649 name: []const u8 = "",
650 },
651 positional: []const []const u8 = &.{},
652 }, allocator, &[_][]const u8{"--bogus"}, options));
653 try testing.expect(mem.indexOf(u8, aw.written(), "--bogus") != null);
654734
655 // expected argument
656 aw.clearRetainingCapacity();
657 try testing.expectError(error.Usage, parseSlice(struct {
658 named: struct {
659 name: []const u8 = "",
660 },
661 positional: []const []const u8 = &.{},
662 }, allocator, &[_][]const u8{"--name"}, options));
663 try testing.expect(mem.indexOf(u8, aw.written(), "--name") != null);
664
665 // --no-<name> for non-bool.
666 aw.clearRetainingCapacity();
667 try testing.expectError(error.Usage, parseSlice(struct {
668 named: struct {
669 name: []const u8 = "",
670 },
671 positional: []const []const u8 = &.{},
672 }, allocator, &[_][]const u8{"--no-name"}, options));
673 try testing.expect(mem.indexOf(u8, aw.written(), "--no-name") != null);
674
675 // --name=false for bool
676 aw.clearRetainingCapacity();
677 try testing.expectError(error.Usage, parseSlice(struct {
735 const Args = struct {
678736 named: struct {
679 name: bool = false,
737 b: bool,
680738 },
681 positional: []const []const u8 = &.{},
682 }, allocator, &[_][]const u8{"--name=true"}, options));
683 try testing.expect(mem.indexOf(u8, aw.written(), "--name") != null);
739 };
684740
685 // missing required argument
686 aw.clearRetainingCapacity();
687 try testing.expectError(error.Usage, parseSlice(struct {
688 named: struct {
689 name: []const u8,
690 },
691 positional: []const []const u8 = &.{},
692 }, allocator, &[_][]const u8{}, options));
693 try testing.expect(mem.indexOf(u8, aw.written(), "--name") != null);
741 try testing.expectEqualDeep(Args{ .named = .{ .b = true } }, try parseSlice(Args, allocator, &[_][]const u8{"--b"}, .{}));
742 try testing.expectEqualDeep(Args{ .named = .{ .b = false } }, try parseSlice(Args, allocator, &[_][]const u8{"--no-b"}, .{}));
743 try testing.expectEqualDeep(Args{ .named = .{ .b = true } }, try parseSlice(Args, allocator, &[_][]const u8{ "--no-b", "--b" }, .{}));
744 try testing.expectEqualDeep(Args{ .named = .{ .b = false } }, try parseSlice(Args, allocator, &[_][]const u8{ "--b", "--no-b" }, .{}));
694745
695 // parse int error
696 aw.clearRetainingCapacity();
697 try testing.expectError(error.Usage, parseSlice(struct {
698 named: struct {
699 name: i32,
700 },
701 positional: []const []const u8 = &.{},
702 }, allocator, &[_][]const u8{"--name=abc"}, options));
703 try testing.expect(mem.indexOf(u8, aw.written(), "--name") != null);
704 aw.clearRetainingCapacity();
705 try testing.expectError(error.Usage, parseSlice(struct {
706 named: struct {
707 name: []const i32 = &.{},
708 },
709 positional: []const []const u8 = &.{},
710 }, allocator, &[_][]const u8{"--name=abc"}, options));
711 try testing.expect(mem.indexOf(u8, aw.written(), "--name") != null);
746 try testing.expectError(error.Usage, parseSlice(Args, allocator, &[_][]const u8{"--b=true"}, silent_options));
747 try testing.expectError(error.Usage, parseSlice(Args, allocator, &[_][]const u8{"--b=false"}, silent_options));
748}
712749
713 // parse float error
714 aw.clearRetainingCapacity();
715 try testing.expectError(error.Usage, parseSlice(struct {
716 named: struct {
717 name: f32,
718 },
719 positional: []const []const u8 = &.{},
720 }, allocator, &[_][]const u8{"--name=abc"}, options));
721 try testing.expect(mem.indexOf(u8, aw.written(), "--name") != null);
722 aw.clearRetainingCapacity();
723 try testing.expectError(error.Usage, parseSlice(struct {
724 named: struct {
725 name: []const f32 = &.{},
726 },
727 positional: []const []const u8 = &.{},
728 }, allocator, &[_][]const u8{"--name=abc"}, options));
729 try testing.expect(mem.indexOf(u8, aw.written(), "--name") != null);
750test "string" {
751 var arena: std.heap.ArenaAllocator = .init(testing.allocator);
752 defer arena.deinit();
753 const allocator = arena.allocator();
730754
731 // parse enum error
732 aw.clearRetainingCapacity();
733 try testing.expectError(error.Usage, parseSlice(struct {
755 const Args = struct {
734756 named: struct {
735 name: enum { auto, never, always },
757 a: []const u8,
758 b: [:0]const u8,
736759 },
737 positional: []const []const u8 = &.{},
738 }, allocator, &[_][]const u8{"--name=abc"}, options));
739 try testing.expect(mem.indexOf(u8, aw.written(), "--name") != null);
740 try testing.expect(mem.indexOf(u8, aw.written(), "abc") != null);
741 // Error should suggest the set of options.
742 try testing.expect(mem.indexOf(u8, aw.written(), "always") != null);
760 };
761 const args = try parseSlice(Args, allocator, &[_][:0]const u8{
762 "--a", "a",
763 "--b", "b",
764 }, .{});
743765
744 // reject single-letter alias-looking arguments
745 aw.clearRetainingCapacity();
746 try testing.expectError(error.Usage, parseSlice(struct {
747 named: struct {
748 z: bool = false,
766 try testing.expectEqualDeep(Args{
767 .named = .{
768 .a = "a",
769 .b = "b",
749770 },
750 positional: []const []const u8 = &.{},
751 }, allocator, &[_][]const u8{"-z"}, options));
752 try testing.expect(mem.indexOf(u8, aw.written(), "-z") != null);
771 }, args);
753772}
754773
755774test "ints and floats" {
......@@ -768,7 +787,6 @@ test "ints and floats" {
768787 inf_f32: f32,
769788 ninf_f64: f64,
770789 },
771 positional: []const []const u8 = &.{},
772790 };
773791 const args = try parseSlice(Args, allocator, &[_][]const u8{
774792 "--int_u32", "0xffffffff",
......@@ -792,14 +810,12 @@ test "ints and floats" {
792810 .inf_f32 = std.math.inf(f32),
793811 .ninf_f64 = -std.math.inf(f64),
794812 },
795 .positional = &.{},
796813 }, args);
797814
798815 const Args2 = struct {
799816 named: struct {
800817 nan: f64,
801818 },
802 positional: []const []const u8 = &.{},
803819 };
804820 const args2 = try parseSlice(Args2, allocator, &[_][]const u8{
805821 "--nan", "nAN",
......@@ -808,80 +824,37 @@ test "ints and floats" {
808824 try testing.expect(std.math.isNan(args2.named.nan));
809825}
810826
811test "bool" {
827test "array" {
812828 var arena: std.heap.ArenaAllocator = .init(testing.allocator);
813829 defer arena.deinit();
814830 const allocator = arena.allocator();
815831
816832 const Args = struct {
817833 named: struct {
818 b: bool,
834 path: []const []const u8 = &.{},
835 id: []const i32 = &.{},
819836 },
820 positional: []const []const u8 = &.{},
821 };
822
823 try testing.expectEqualDeep(Args{ .named = .{ .b = true } }, try parseSlice(Args, allocator, &[_][]const u8{"--b"}, .{}));
824 try testing.expectEqualDeep(Args{ .named = .{ .b = false } }, try parseSlice(Args, allocator, &[_][]const u8{"--no-b"}, .{}));
825 try testing.expectEqualDeep(Args{ .named = .{ .b = true } }, try parseSlice(Args, allocator, &[_][]const u8{ "--no-b", "--b" }, .{}));
826 try testing.expectEqualDeep(Args{ .named = .{ .b = false } }, try parseSlice(Args, allocator, &[_][]const u8{ "--b", "--no-b" }, .{}));
827
828 try testing.expectError(error.Usage, parseSlice(Args, allocator, &[_][]const u8{"--b=true"}, silent_options));
829 try testing.expectError(error.Usage, parseSlice(Args, allocator, &[_][]const u8{"--b=false"}, silent_options));
830}
831
832test "string" {
833 var arena: std.heap.ArenaAllocator = .init(testing.allocator);
834 defer arena.deinit();
835 const allocator = arena.allocator();
836
837 const Args = struct {
838 named: struct {
839 a: []const u8,
840 b: [:0]const u8,
837 positional: struct {
838 args: []const []const u8 = &.{},
841839 },
842 positional: []const []const u8 = &.{},
843840 };
844 const args = try parseSlice(Args, allocator, &[_][:0]const u8{
845 "--a", "a",
846 "--b", "b",
847 }, .{});
848841
849842 try testing.expectEqualDeep(Args{
850843 .named = .{
851 .a = "a",
852 .b = "b",
844 .path = &[_][]const u8{ "a", "b", "a" },
845 .id = &[_]i32{ 1, -12 },
853846 },
854 .positional = &.{},
855 }, args);
856}
857
858test "array" {
859 var arena: std.heap.ArenaAllocator = .init(testing.allocator);
860 defer arena.deinit();
861 const allocator = arena.allocator();
862
863 const Args = struct {
864 named: struct {
865 path: []const []const u8 = &.{},
866 id: []const i32 = &.{},
847 .positional = .{
848 .args = &[_][]const u8{ "x", "y" },
867849 },
868 positional: []const []const u8 = &.{},
869 };
870 const args = try parseSlice(Args, allocator, &[_][]const u8{
850 }, try parseSlice(Args, allocator, &[_][]const u8{
871851 "--path", "a",
872852 "--path", "b",
873853 "--path", "a",
874854 "--id", "1",
875855 "--id", "-12",
876 }, .{});
877
878 try testing.expectEqualDeep(Args{
879 .named = .{
880 .path = &[_][]const u8{ "a", "b", "a" },
881 .id = &[_]i32{ 1, -12 },
882 },
883 .positional = &.{},
884 }, args);
856 "x", "y",
857 }, .{}));
885858}
886859
887860test "enum" {
......@@ -905,7 +878,6 @@ test "enum" {
905878 VTALRM = 26,
906879 },
907880 },
908 positional: []const []const u8 = &.{},
909881 };
910882 const args = try parseSlice(Args, allocator, &[_][]const u8{
911883 "--color", "always",
......@@ -919,7 +891,6 @@ test "enum" {
919891 .guess = .@"the-only-option",
920892 .signal = .TERM,
921893 },
922 .positional = &.{},
923894 }, args);
924895}
925896
......@@ -942,24 +913,20 @@ test "defaults" {
942913 force: bool = false,
943914 cleanup: bool = true,
944915 },
945 positional: []const []const u8 = &.{},
946916 };
947917
948918 try testing.expectEqualDeep(Args{
949919 .named = .{},
950 .positional = &.{},
951920 }, try parseSlice(Args, allocator, &[_][]const u8{}, .{}));
952921 try testing.expectEqualDeep(Args{
953922 .named = .{
954923 .color = .always,
955924 },
956 .positional = &.{},
957925 }, try parseSlice(Args, allocator, &[_][]const u8{ "--color", "always" }, .{}));
958926 try testing.expectEqualDeep(Args{
959927 .named = .{
960928 .file = &[_][]const u8{"file.txt"},
961929 },
962 .positional = &.{},
963930 }, try parseSlice(Args, allocator, &[_][]const u8{ "--file", "file.txt" }, .{}));
964931
965932 try testing.expectEqualDeep(Args{
......@@ -967,10 +934,208 @@ test "defaults" {
967934 .force = true,
968935 .cleanup = false,
969936 },
970 .positional = &.{},
971937 }, try parseSlice(Args, allocator, &[_][]const u8{ "--force", "--no-cleanup" }, .{}));
972938}
973939
940test "positional" {
941 var arena: std.heap.ArenaAllocator = .init(testing.allocator);
942 defer arena.deinit();
943 const allocator = arena.allocator();
944
945 // defaults
946 {
947 const Args = struct {
948 positional: struct {
949 level: i8 = -1,
950 ratio: f32 = 0.5,
951 path: []const u8 = "-",
952 color: enum {
953 always,
954 never,
955 auto,
956 } = .auto,
957 file: []const []const u8 = &.{},
958 },
959 };
960
961 try testing.expectEqualDeep(Args{
962 .positional = .{},
963 }, try parseSlice(Args, allocator, &[_][]const u8{}, .{}));
964 try testing.expectEqualDeep(Args{
965 .positional = .{
966 .level = 1,
967 .ratio = 2,
968 .path = "a.txt",
969 .color = .always,
970 .file = &[_][]const u8{ "file1", "file2" },
971 },
972 }, try parseSlice(Args, allocator, &[_][]const u8{ "1", "2", "a.txt", "always", "file1", "file2" }, .{}));
973 }
974
975 // required
976 {
977 const Args = struct {
978 positional: struct {
979 level: i8,
980 ratio: f32,
981 path: []const u8,
982 color: enum {
983 always,
984 never,
985 auto,
986 },
987 file: []const []const u8 = &.{},
988 },
989 };
990
991 try testing.expectError(error.Usage, parseSlice(Args, allocator, &[_][]const u8{}, silent_options));
992 try testing.expectError(error.Usage, parseSlice(Args, allocator, &[_][]const u8{ "1", "2", "a.txt" }, silent_options));
993 try testing.expectEqualDeep(Args{
994 .positional = .{
995 .level = 1,
996 .ratio = 2,
997 .path = "a.txt",
998 .color = .always,
999 },
1000 }, try parseSlice(Args, allocator, &[_][]const u8{ "1", "2", "a.txt", "always" }, .{}));
1001 }
1002}
1003
1004test "usage errors" {
1005 var arena: std.heap.ArenaAllocator = .init(testing.allocator);
1006 defer arena.deinit();
1007 const allocator = arena.allocator();
1008 var aw: Writer.Allocating = .init(allocator);
1009 const options = Options{ .prog = "test-prog", .writer = &aw.writer };
1010
1011 // unrecognized argument
1012 aw.clearRetainingCapacity();
1013 try testing.expectError(error.Usage, parseSlice(struct {
1014 named: struct {
1015 name: []const u8 = "",
1016 },
1017 }, allocator, &[_][]const u8{"--bogus"}, options));
1018 try testing.expect(mem.indexOf(u8, aw.written(), "--bogus") != null);
1019
1020 // expected argument
1021 aw.clearRetainingCapacity();
1022 try testing.expectError(error.Usage, parseSlice(struct {
1023 named: struct {
1024 name: []const u8 = "",
1025 },
1026 }, allocator, &[_][]const u8{"--name"}, options));
1027 try testing.expect(mem.indexOf(u8, aw.written(), "--name") != null);
1028
1029 // --no-<name> for non-bool.
1030 aw.clearRetainingCapacity();
1031 try testing.expectError(error.Usage, parseSlice(struct {
1032 named: struct {
1033 name: []const u8 = "",
1034 },
1035 }, allocator, &[_][]const u8{"--no-name"}, options));
1036 try testing.expect(mem.indexOf(u8, aw.written(), "--no-name") != null);
1037
1038 // --name=false for bool
1039 aw.clearRetainingCapacity();
1040 try testing.expectError(error.Usage, parseSlice(struct {
1041 named: struct {
1042 name: bool = false,
1043 },
1044 }, allocator, &[_][]const u8{"--name=true"}, options));
1045 try testing.expect(mem.indexOf(u8, aw.written(), "--name") != null);
1046
1047 // missing required argument
1048 aw.clearRetainingCapacity();
1049 try testing.expectError(error.Usage, parseSlice(struct {
1050 named: struct {
1051 name: []const u8,
1052 },
1053 }, allocator, &[_][]const u8{}, options));
1054 try testing.expect(mem.indexOf(u8, aw.written(), "--name") != null);
1055
1056 // parse int error
1057 aw.clearRetainingCapacity();
1058 try testing.expectError(error.Usage, parseSlice(struct {
1059 named: struct {
1060 name: i32,
1061 },
1062 }, allocator, &[_][]const u8{"--name=abc"}, options));
1063 try testing.expect(mem.indexOf(u8, aw.written(), "--name") != null);
1064 aw.clearRetainingCapacity();
1065 try testing.expectError(error.Usage, parseSlice(struct {
1066 named: struct {
1067 name: []const i32 = &.{},
1068 },
1069 }, allocator, &[_][]const u8{"--name=abc"}, options));
1070 try testing.expect(mem.indexOf(u8, aw.written(), "--name") != null);
1071
1072 // parse float error
1073 aw.clearRetainingCapacity();
1074 try testing.expectError(error.Usage, parseSlice(struct {
1075 named: struct {
1076 name: f32,
1077 },
1078 }, allocator, &[_][]const u8{"--name=abc"}, options));
1079 try testing.expect(mem.indexOf(u8, aw.written(), "--name") != null);
1080 aw.clearRetainingCapacity();
1081 try testing.expectError(error.Usage, parseSlice(struct {
1082 named: struct {
1083 name: []const f32 = &.{},
1084 },
1085 }, allocator, &[_][]const u8{"--name=abc"}, options));
1086 try testing.expect(mem.indexOf(u8, aw.written(), "--name") != null);
1087
1088 // parse enum error
1089 aw.clearRetainingCapacity();
1090 try testing.expectError(error.Usage, parseSlice(struct {
1091 named: struct {
1092 name: enum { auto, never, always },
1093 },
1094 }, allocator, &[_][]const u8{"--name=abc"}, options));
1095 try testing.expect(mem.indexOf(u8, aw.written(), "--name") != null);
1096 try testing.expect(mem.indexOf(u8, aw.written(), "abc") != null);
1097 // Error should suggest the set of options.
1098 try testing.expect(mem.indexOf(u8, aw.written(), "always") != null);
1099
1100 // reject single-letter alias-looking arguments
1101 aw.clearRetainingCapacity();
1102 try testing.expectError(error.Usage, parseSlice(struct {
1103 named: struct {
1104 z: bool = false,
1105 },
1106 positional: struct {
1107 args: []const []const u8 = &.{},
1108 },
1109 }, allocator, &[_][]const u8{"-z"}, options));
1110 try testing.expect(mem.indexOf(u8, aw.written(), "-z") != null);
1111
1112 // expected required positional argument
1113 aw.clearRetainingCapacity();
1114 try testing.expectError(error.Usage, parseSlice(struct {
1115 positional: struct {
1116 input_file: []const u8,
1117 },
1118 }, allocator, &[_][]const u8{}, options));
1119 try testing.expect(mem.indexOf(u8, aw.written(), "input_file") != null);
1120 aw.clearRetainingCapacity();
1121 try testing.expectError(error.Usage, parseSlice(struct {
1122 positional: struct {
1123 input_file: []const u8,
1124 output_file: []const u8 = "",
1125 },
1126 }, allocator, &[_][]const u8{}, options));
1127 try testing.expect(mem.indexOf(u8, aw.written(), "input_file") != null);
1128 aw.clearRetainingCapacity();
1129 try testing.expectError(error.Usage, parseSlice(struct {
1130 positional: struct {
1131 input_file: []const u8,
1132 output_file: []const u8,
1133 other: []const u8 = "",
1134 },
1135 }, allocator, &[_][]const u8{"input.txt"}, options));
1136 try testing.expect(mem.indexOf(u8, aw.written(), "output_file") != null);
1137}
1138
9741139test "help" {
9751140 var arena: std.heap.ArenaAllocator = .init(testing.allocator);
9761141 defer arena.deinit();
......@@ -985,7 +1150,6 @@ test "help" {
9851150 int: i32,
9861151 flag: bool,
9871152 },
988 positional: []const []const u8 = &.{},
9891153 }, allocator, &[_][]const u8{"--help"}, options));
9901154 // Because the help output is primarily for humans, don't get too strict in the unit test.
9911155 // Only verify that we see the important stuff that should definitely be there somewhere,
......@@ -1002,7 +1166,6 @@ test "help" {
10021166 named: struct {
10031167 color: enum { never, auto, always } = .auto,
10041168 },
1005 positional: []const []const u8 = &.{},
10061169 }, allocator, &[_][]const u8{"--help"}, options));
10071170 // All allowed values for an enum should be spelled out.
10081171 try testing.expect(mem.indexOf(u8, aw.written(), "--color") != null);
......@@ -1016,14 +1179,12 @@ test "help" {
10161179 named: struct {
10171180 name: []const u8,
10181181 },
1019 positional: []const []const u8 = &.{},
10201182 }, allocator, &[_][]const u8{"--help"}, options));
10211183 const scalar_help = try aw.toOwnedSlice();
10221184 try testing.expectError(error.Help, parseSlice(struct {
10231185 named: struct {
10241186 name: []const []const u8 = &.{},
10251187 },
1026 positional: []const []const u8 = &.{},
10271188 }, allocator, &[_][]const u8{"--help"}, options));
10281189 try testing.expect(!mem.eql(u8, scalar_help, aw.written()));
10291190
......@@ -1035,7 +1196,6 @@ test "help" {
10351196 int: i32 = 3,
10361197 f: f32 = 1.25,
10371198 },
1038 positional: []const []const u8 = &.{},
10391199 }, allocator, &[_][]const u8{"--help"}, options));
10401200 try testing.expect(mem.indexOf(u8, aw.written(), "hello") != null);
10411201 try testing.expect(mem.indexOf(u8, aw.written(), "3") != null);
......@@ -1047,21 +1207,18 @@ test "help" {
10471207 named: struct {
10481208 b: bool,
10491209 },
1050 positional: []const []const u8 = &.{},
10511210 }, allocator, &[_][]const u8{"--help"}, options));
10521211 const bool_required_help = try aw.toOwnedSlice();
10531212 try testing.expectError(error.Help, parseSlice(struct {
10541213 named: struct {
10551214 b: bool = true,
10561215 },
1057 positional: []const []const u8 = &.{},
10581216 }, allocator, &[_][]const u8{"--help"}, options));
10591217 const default_true_help = try aw.toOwnedSlice();
10601218 try testing.expectError(error.Help, parseSlice(struct {
10611219 named: struct {
10621220 b: bool = false,
10631221 },
1064 positional: []const []const u8 = &.{},
10651222 }, allocator, &[_][]const u8{"--help"}, options));
10661223 const default_false_help = try aw.toOwnedSlice();
10671224 try testing.expect(!mem.eql(u8, bool_required_help, default_true_help));
......@@ -1074,21 +1231,18 @@ test "help" {
10741231 named: struct {
10751232 color: enum { never, auto, always },
10761233 },
1077 positional: []const []const u8 = &.{},
10781234 }, allocator, &[_][]const u8{"--help"}, options));
10791235 const enum_required_help = try aw.toOwnedSlice();
10801236 try testing.expectError(error.Help, parseSlice(struct {
10811237 named: struct {
10821238 color: enum { never, auto, always } = .auto,
10831239 },
1084 positional: []const []const u8 = &.{},
10851240 }, allocator, &[_][]const u8{"--help"}, options));
10861241 const default_auto_help = try aw.toOwnedSlice();
10871242 try testing.expectError(error.Help, parseSlice(struct {
10881243 named: struct {
10891244 color: enum { never, auto, always } = .never,
10901245 },
1091 positional: []const []const u8 = &.{},
10921246 }, allocator, &[_][]const u8{"--help"}, options));
10931247 const default_never_help = try aw.toOwnedSlice();
10941248 try testing.expect(!mem.eql(u8, enum_required_help, default_auto_help));
......@@ -1097,16 +1251,11 @@ test "help" {
10971251}
10981252
10991253test "minimal" {
1100 const Args = struct {
1101 named: struct {},
1102 positional: []const []const u8 = &.{},
1103 };
1254 const Args = struct {};
11041255
11051256 var arena: std.heap.ArenaAllocator = .init(testing.allocator);
11061257 defer arena.deinit();
1107 const args = try parseSlice(Args, arena.allocator(), &[_][]const u8{}, .{});
1108
1109 try testing.expectEqual(@as(usize, 0), args.positional.len);
1258 _ = try parseSlice(Args, arena.allocator(), &[_][]const u8{}, .{});
11101259}
11111260
11121261test "manual deinit" {
......@@ -1116,7 +1265,9 @@ test "manual deinit" {
11161265 int_arr: []const i32 = &.{},
11171266 empty_arr: []const []const u8 = &.{},
11181267 },
1119 positional: []const []const u8 = &.{},
1268 positional: struct {
1269 args: []const []const u8 = &.{},
1270 },
11201271 };
11211272
11221273 const args = try parseSlice(Args, testing.allocator, &[_][]const u8{
......@@ -1130,14 +1281,16 @@ test "manual deinit" {
11301281 .str_arr = &.{ "hello1", "hello2" },
11311282 .int_arr = &.{ 123456, 789012 },
11321283 },
1133 .positional = &.{ "positional-12345", "positi" },
1284 .positional = .{
1285 .args = &.{ "positional-12345", "positi" },
1286 },
11341287 }, args);
11351288
11361289 // Surgically cleanup memory.
11371290 testing.allocator.free(args.named.str_arr);
11381291 testing.allocator.free(args.named.int_arr);
11391292 testing.allocator.free(args.named.empty_arr);
1140 testing.allocator.free(args.positional);
1293 testing.allocator.free(args.positional.args);
11411294 // Should be no memory leak errors now.
11421295}
11431296
......@@ -1146,7 +1299,9 @@ test "actually calling error" {
11461299 named: struct {
11471300 output: []const u8 = "",
11481301 },
1149 positional: []const []const u8 = &.{},
1302 positional: struct {
1303 args: []const []const u8 = &.{},
1304 },
11501305 };
11511306
11521307 var arena: std.heap.ArenaAllocator = .init(testing.allocator);
......@@ -1182,7 +1337,9 @@ test "custom help" {
11821337 output: []const u8,
11831338 force: bool = false,
11841339 },
1185 positional: []const []const u8 = &.{},
1340 positional: struct {
1341 args: []const []const u8 = &.{},
1342 },
11861343 };
11871344 try testing.expectError(error.Help, parseSlice(Args, allocator, &[_][]const u8{"--help"}, options));
11881345 try testing.expectEqualStrings(Args.help, aw.written());