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;...@@ -7,6 +7,7 @@ const isAlphabetic = std.ascii.isAlphabetic;
7const Writer = std.Io.Writer;7const Writer = std.Io.Writer;
8const ArgIterator = std.process.ArgIterator;8const ArgIterator = std.process.ArgIterator;
9const ArenaAllocator = std.heap.ArenaAllocator;9const ArenaAllocator = std.heap.ArenaAllocator;
10const StructField = std.builtin.Type.StructField;
10const mem = std.mem;11const mem = std.mem;
11const Allocator = mem.Allocator;12const Allocator = mem.Allocator;
1213
...@@ -29,23 +30,25 @@ pub const Options = struct {...@@ -29,23 +30,25 @@ pub const Options = struct {
29pub const Error = error{30pub const Error = error{
30 /// Caused by unrecognized option names, values that cannot be parsed into the appropriate field type,31 /// Caused by unrecognized option names, values that cannot be parsed into the appropriate field type,
31 /// missing arguments for fields with no default value, and other similar parsing errors.32 /// missing arguments for fields with no default value, and other similar parsing errors.
33 /// See also `options.exit`, which can supersede this error.
32 Usage,34 Usage,
33 /// The --help argument was given.35 /// The --help argument was given (and `options.exit` resolved to `false`).
34 Help,36 Help,
35} || Allocator.Error;37} || Allocator.Error;
3638
37/// Parses CLI args from a `std.process.ArgIterator` according to the configuration in `Args`.39/// 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:
39/// ```41/// ```
40/// const Args = struct {42/// const Args = struct {
41/// named: struct {43/// named: struct {
42/// // ...44/// // ...
43/// },45/// },
44/// positional: []const []const u8 = &.{},46/// positional: struct {
47/// // ...
48/// },
45/// };49/// };
46/// ```50/// ```
47/// The `named` and `positional` fields are required, although `named` need not have any subfields.51/// Either or both of `named` and `positional` may be omitted, which is effectively equivalent to them having no fields.
48/// `positional` may instead have type `[]const [:0]const u8`.
49///52///
50/// The sequence of arg strings from the `ArgIterator` is parsed to determine named and positional arguments.53/// The sequence of arg strings from the `ArgIterator` is parsed to determine named and positional arguments.
51///54///
...@@ -62,7 +65,7 @@ pub const Error = error{...@@ -62,7 +65,7 @@ pub const Error = error{
62/// Forms (1), (2), and (3) must correspond to a field `Args.named.<name>`; see below for named argument handling.65/// Forms (1), (2), and (3) must correspond to a field `Args.named.<name>`; see below for named argument handling.
63/// Form (4) immediately prints the long help documentation and exits or returns `error.Help` depending on options.exit.66/// Form (4) immediately prints the long help documentation and exits or returns `error.Help` depending on options.exit.
64/// Form (6) signals that all following arg strings are positional.67/// 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.
66///69///
67/// Form (5) is always an error.70/// Form (5) is always an error.
68/// This API does not support single letter aliases like `-v` or `-lA` or named arguments prefixed by only a single hyphen like `-flag`.71/// 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{...@@ -88,6 +91,17 @@ pub const Error = error{
88/// Slice arguments `[]const C` (where `C` is not `u8`) must have a default value, usually `&.{}`.91/// Slice arguments `[]const C` (where `C` is not `u8`) must have a default value, usually `&.{}`.
89/// If a bool argument has no default value, then at least one of `--<name>` or `--no-<name>` must be given.92/// If a bool argument has no default value, then at least one of `--<name>` or `--no-<name>` must be given.
90///93///
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///
91/// It's possible to override the automatically-generated long help documentation by declaring a public constant named `help` in `Args`.105/// It's possible to override the automatically-generated long help documentation by declaring a public constant named `help` in `Args`.
92/// The value must coerce to `[]const u8`.106/// The value must coerce to `[]const u8`.
93///107///
...@@ -104,7 +118,6 @@ pub const Error = error{...@@ -104,7 +118,6 @@ pub const Error = error{
104/// named: struct {118/// named: struct {
105/// // [...]119/// // [...]
106/// },120/// },
107/// positional: []const []const u8 = &.{},
108/// };121/// };
109/// ```122/// ```
110///123///
...@@ -136,13 +149,21 @@ test parse {...@@ -136,13 +149,21 @@ test parse {
136 level: i8 = -1,149 level: i8 = -1,
137 /// Parsed as the name of the member `--color=never`.150 /// Parsed as the name of the member `--color=never`.
138 color: enum { auto, never, always } = .auto,151 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).
140 seed: u32 = 0,155 seed: u32 = 0,
141 @"cache-dir": []const u8 = "",156 @"cache-dir": []const u8 = "",
142 listen: []const u8 = "",157 listen: []const u8 = "",
143 },158 },
144 /// Receives the rest of the arguments.159 positional: struct {
145 positional: []const [:0]const u8 = &.{},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 },
146 };167 };
147168
148 var arena: ArenaAllocator = .init(testing.allocator);169 var arena: ArenaAllocator = .init(testing.allocator);
...@@ -157,8 +178,8 @@ test parse {...@@ -157,8 +178,8 @@ test parse {
157/// ```178/// ```
158/// pub fn next(self: *Self) ?String { ... }179/// pub fn next(self: *Self) ?String { ... }
159/// ```180/// ```
160/// Where `String` is `[]const u8` or `[:0]const u8`, or something else that coerces to `[]const u8`.181/// 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.182/// If `String` does not coerce to `[:0]const u8`, then `Args` cannot have any `[:0]const u8` in its fields.
162///183///
163/// The first string arg returned by the `iter` (`argv[0]`) is skipped by all the parsing logic.184/// The first string arg returned by the `iter` (`argv[0]`) is skipped by all the parsing logic.
164/// If `options.prog` is `null`, then the final path component of `argv[0]` is used by default.185/// If `options.prog` is `null`, then the final path component of `argv[0]` is used by default.
...@@ -170,7 +191,7 @@ test parse {...@@ -170,7 +191,7 @@ test parse {
170///191///
171/// An `ArenaAllocator` is recommended to cleanup the memory allocated from this function;192/// An `ArenaAllocator` is recommended to cleanup the memory allocated from this function;
172/// however, it's also possible to free all the memory by freeing every slice field `[]const C` (other than `u8`)193/// 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`.
174pub fn parseIter(comptime Args: type, arena: Allocator, iter: anytype, options: Options) Error!Args {195pub fn parseIter(comptime Args: type, arena: Allocator, iter: anytype, options: Options) Error!Args {
175 const argv0 = iter.next();196 const argv0 = iter.next();
176 const prog = options.prog orelse if (argv0) |arg| std.fs.path.basename(arg) else "<prog>";197 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:...@@ -179,7 +200,7 @@ pub fn parseIter(comptime Args: type, arena: Allocator, iter: anytype, options:
179200
180/// Like `parse`, but takes a slice of strings in place of using an `ArgIterator`.201/// Like `parse`, but takes a slice of strings in place of using an `ArgIterator`.
181/// `argv` must be either be a slice of `String` or a single-item pointer to an array of `String`,202/// `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`.
183/// If `String` does not coerce to `[:0]const u8`, then `Args` cannot have `[:0]const u8` fields.204/// If `String` does not coerce to `[:0]const u8`, then `Args` cannot have `[:0]const u8` fields.
184///205///
185/// Unlike `parse` and `parseIter`, this function does not skip the first item of `argv`.206/// 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:...@@ -192,7 +213,7 @@ pub fn parseIter(comptime Args: type, arena: Allocator, iter: anytype, options:
192///213///
193/// An `ArenaAllocator` is recommended to cleanup the memory allocated from this function;214/// An `ArenaAllocator` is recommended to cleanup the memory allocated from this function;
194/// however, it's also possible to free all the memory by freeing every slice field `[]const C` (other than `u8`)215/// 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`.
196pub fn parseSlice(comptime Args: type, arena: Allocator, argv: anytype, options: Options) Error!Args {217pub fn parseSlice(comptime Args: type, arena: Allocator, argv: anytype, options: Options) Error!Args {
197 const argvInfo = @typeInfo(@TypeOf(argv)).pointer;218 const argvInfo = @typeInfo(@TypeOf(argv)).pointer;
198 const String = if (argvInfo.size == .one)219 const String = if (argvInfo.size == .one)
...@@ -218,7 +239,9 @@ test parseSlice {...@@ -218,7 +239,9 @@ test parseSlice {
218 flag: bool = true,239 flag: bool = true,
219 @"enum-option": enum { auto, always, never } = .auto,240 @"enum-option": enum { auto, always, never } = .auto,
220 },241 },
221 positional: []const []const u8 = &.{},242 positional: struct {
243 args: []const []const u8 = &.{},
244 },
222 };245 };
223 const args = try parseSlice(Args, allocator, &[_][]const u8{246 const args = try parseSlice(Args, allocator, &[_][]const u8{
224 "--example_required", "a.txt",247 "--example_required", "a.txt",
...@@ -238,7 +261,7 @@ test parseSlice {...@@ -238,7 +261,7 @@ test parseSlice {
238 .flag = false,261 .flag = false,
239 .@"enum-option" = .always,262 .@"enum-option" = .always,
240 },263 },
241 .positional = &.{ "positional1", "positional2", "-12345678", "--positional4", "--positional=5" },264 .positional = .{ .args = &.{ "positional1", "positional2", "-12345678", "--positional4", "--positional=5" } },
242 }, args);265 }, args);
243}266}
244267
...@@ -246,42 +269,19 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: []...@@ -246,42 +269,19 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: []
246 // argv0 has already been consumed.269 // argv0 has already been consumed.
247270
248 // Do all comptime checks up front so that we can be sure any compile error the user sees is the one we wrote.271 // 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
251 var result: Args = undefined;277 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);281 var the_rest_is_positional = false;
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 }
282282
283 while (iter.next()) |arg| {283 while (iter.next()) |arg| {
284 if (mem.eql(u8, arg, "--help")) {284 if (!the_rest_is_positional and mem.eql(u8, arg, "--help")) {
285 if (@hasDecl(Args, "help")) {285 if (@hasDecl(Args, "help")) {
286 // Custom help.286 // Custom help.
287 if (writer) |w| {287 if (writer) |w| {
...@@ -293,7 +293,7 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: []...@@ -293,7 +293,7 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: []
293 file_writer.interface.flush() catch {};293 file_writer.interface.flush() catch {};
294 }294 }
295 } else {295 } else {
296 printGeneratedHelp(writer, prog, named_info);296 printGeneratedHelp(writer, prog, named_fields);
297 }297 }
298 if (exit_on_error) {298 if (exit_on_error) {
299 std.process.exit(0);299 std.process.exit(0);
...@@ -301,22 +301,32 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: []...@@ -301,22 +301,32 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: []
301 return error.Help;301 return error.Help;
302 }302 }
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])) {
305 // Always invalid.305 // Always invalid.
306 // Examples: -h, -flag, -I/path306 // Examples: -h, -flag, -I/path
307 return usageError(writer, "unrecognized argument: {s}", .{arg}, exit_on_error);307 return usageError(writer, "unrecognized argument: {s}", .{arg}, exit_on_error);
308 }308 }
309 if (mem.eql(u8, arg, "--")) {309 if (!the_rest_is_positional and mem.eql(u8, arg, "--")) {
310 // Stop recognizing named arguments. Everything else is positional.310 // Stop recognizing named arguments. Everything else is positional.
311 while (iter.next()) |arg2| {311 the_rest_is_positional = true;
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`.312 continue;
313 }
314 break;
315 }313 }
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] == '-')) {
317 // Positional.315 // Positional.
318 // Examples: "", "a", "-", "-1",316 // Examples: "", "a", "-", "-1", "other"
319 try positional.append(allocator, arg);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;
320 continue;330 continue;
321 }331 }
322332
...@@ -335,12 +345,12 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: []...@@ -335,12 +345,12 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: []
335 break :blk .{ arg["--".len..], null, false };345 break :blk .{ arg["--".len..], null, false };
336 };346 };
337347
338 inline for (named_info.fields, 0..) |field, i| {348 inline for (named_fields, 0..) |field, i| {
339 if (mem.eql(u8, field.name, arg_name)) {349 if (mem.eql(u8, field.name, arg_name)) {
350 named_fields_seen[i] = true;
340 if (field.type == bool) {351 if (field.type == bool) {
341 if (immediate_value != null) return usageError(writer, "cannot specify value for bool argument: {s}", .{arg}, exit_on_error);352 if (immediate_value != null) return usageError(writer, "cannot specify value for bool argument: {s}", .{arg}, exit_on_error);
342 @field(result.named, field.name) = !no_prefixed;353 @field(result.named, field.name) = !no_prefixed;
343 fields_seen[i] = true;
344 break;354 break;
345 }355 }
346 if (no_prefixed) return usageError(writer, "unrecognized argument: {s}", .{arg}, exit_on_error);356 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: []...@@ -348,56 +358,11 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: []
348 // All other argument types require a value.358 // All other argument types require a value.
349 const arg_value = immediate_value orelse iter.next() orelse return usageError(writer, "expected argument after --{s}", .{field.name}, exit_on_error);359 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)) {361 if (getArrayChild(field.type)) |C| {
352 .bool => unreachable, // Handled above.362 try @field(named_array_lists, field.name).append(allocator, try parseValue(C, arg_value, field.name, writer, exit_on_error));
353 .float => {363 } else {
354 @field(result.named, field.name) = std.fmt.parseFloat(field.type, arg_value) catch |err| {364 @field(result.named, field.name) = try parseValue(field.type, arg_value, field.name, writer, exit_on_error);
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,
399 }365 }
400 fields_seen[i] = true;
401 break;366 break;
402 }367 }
403 } else {368 } else {
...@@ -407,74 +372,202 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: []...@@ -407,74 +372,202 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: []
407 }372 }
408373
409 // Fill default values.374 // Fill default values.
410 inline for (named_info.fields, 0..) |field, i| {375 inline for (named_fields, 0..) |field, i| {
411 if (!fields_seen[i]) {376 if (getArrayChild(field.type)) |_| {
412 if (field.defaultValue()) |default| {377 // Array.
413 @field(result.named, field.name) = default;378 @field(result.named, field.name) = try @field(named_array_lists, field.name).toOwnedSlice(allocator);
414 } else {379 } else {
415 if (field.type == bool) {380 // Scalar.
416 return usageError(writer, "missing required argument: --" ++ field.name ++ " or --no-" ++ field.name, .{}, exit_on_error);381 if (!named_fields_seen[i]) {
382 // Unspecified.
383 if (field.defaultValue()) |default| {
384 @field(result.named, field.name) = default;
417 } else {385 } 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 }
419 }391 }
420 }392 }
421 }393 }
422 }394 }
423395 inline for (positional_fields, 0..) |field, i| {
424 // Finalize the array lists.396 if (getArrayChild(field.type)) |_| {
425 result.positional = try positional.toOwnedSlice(allocator);397 // Array.
426 inline for (@typeInfo(@TypeOf(array_lists)).@"struct".fields) |field| {398 @field(result.positional, field.name) = try @field(positional_array_lists, field.name).toOwnedSlice(allocator);
427 @field(result.named, field.name) = try @field(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 }
428 }410 }
429411
430 return result;412 return result;
431}413}
432414
433fn checkArgsType(comptime Args: type) void {415/// arg_value is []const u8 or [:0]const u8.
434 const args_fields = @typeInfo(Args).@"struct".fields;416fn parseValue(comptime T: type, arg_value: anytype, comptime field_name: []const u8, writer: ?*Writer, exit_on_error: bool) !T {
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");417 switch (@typeInfo(T)) {
436 if (args_fields[1].default_value_ptr == null) @compileError("Args.positional must have a default value");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| {443fn checkArgsType(comptime Args: type) struct { []const StructField, []const StructField } {
439 if (field.is_comptime) @compileError("comptime fields are not supported: " ++ field.name);444 var has_named = false;
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.");445 var has_positional = false;
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.");446 inline for (@typeInfo(Args).@"struct".fields) |field| {
442 if (comptime mem.indexOfScalar(u8, field.name, '=') != null) @compileError("Field name contains @\"=\": " ++ field.name);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)) {454 const named_fields = if (has_named) @typeInfo(@TypeOf(@as(Args, undefined).named)).@"struct".fields else &.{};
445 .bool => {},455 const positional_fields = if (has_positional) @typeInfo(@TypeOf(@as(Args, undefined).positional)).@"struct".fields else &.{};
446 .float => {},456
447 .int => {},457 // Named arguments are more lenient.
448 .@"enum" => {458 inline for (named_fields) |field| {
449 if (@typeInfo(field.type).@"enum".fields.len == 0) @compileError("Empty enums not allowed");459 validateField(field);
450 },460 }
451 .pointer => |ptrInfo| {461
452 if (ptrInfo.size != .slice) @compileError("Unsupported field type: " ++ @typeName(field.type));462 // Positional arguments have stricter rules.
453 if (ptrInfo.child == u8) {463 var everything_still_required = true;
454 // String.464 var everything_still_scalar = true;
455 } else {465 inline for (positional_fields) |field| {
456 // Array.466 if (field.type == bool) @compileError("Args.positional cannot have bool fields: " ++ field.name);
457 if (field.default_value_ptr == null) @compileError("Array arguments must have a default value: " ++ field.name);467 validateField(field);
458 switch (@typeInfo(ptrInfo.child)) {468 const is_scalar = getArrayChild(field.type) == null;
459 .bool => @compileError("Unsupported field type: " ++ @typeName(field.type)),469
460 .float => {},470 const is_required = field.default_value_ptr == null;
461 .int => {},471
462 .@"enum" => @compileError("Unsupported field type: " ++ @typeName(field.type)),472 // There can only be one array parameter, and it must be last.
463 .pointer => |ptrInfo2| {473 if (everything_still_scalar) {
464 if (ptrInfo2.size != .slice) @compileError("Unsupported field type: " ++ @typeName(field.type));474 if (!is_scalar) {
465 if (ptrInfo2.child == u8) {475 everything_still_scalar = false;
466 // String.476 }
467 } else {477 } else @compileError("a positional array argument must be last. found: " ++ field.name);
468 @compileError("Unsupported field type: " ++ @typeName(field.type));478
469 }479 // Required positional parameters must come first.
470 },480 if (everything_still_required) {
471 else => @compileError("Unsupported field type: " ++ @typeName(field.type)),481 if (!is_required) {
472 }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)),
473 }526 }
474 },527 }
475 else => @compileError("Unsupported field type: " ++ @typeName(field.type)),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 }
476 }568 }
477 }569 }
570 return @Type(.{ .@"struct" = .{ .layout = .auto, .fields = array_list_fields, .decls = &.{}, .is_tuple = false } });
478}571}
479572
480/// If you do your own validation after getting an `args` from `parse` or similar,573/// If you do your own validation after getting an `args` from `parse` or similar,
...@@ -494,7 +587,9 @@ test @"error" {...@@ -494,7 +587,9 @@ test @"error" {
494 named: struct {587 named: struct {
495 output: []const u8 = "",588 output: []const u8 = "",
496 },589 },
497 positional: []const []const u8 = &.{},590 positional: struct {
591 input: []const u8,
592 },
498 };593 };
499594
500 var arena: std.heap.ArenaAllocator = .init(testing.allocator);595 var arena: std.heap.ArenaAllocator = .init(testing.allocator);
...@@ -504,9 +599,6 @@ test @"error" {...@@ -504,9 +599,6 @@ test @"error" {
504 if (std.fs.path.isAbsolute(args.named.output)) {599 if (std.fs.path.isAbsolute(args.named.output)) {
505 return std.cli.@"error"("--output must not be absolute: {s}", .{args.named.output}, .{ .exit = false });600 return std.cli.@"error"("--output must not be absolute: {s}", .{args.named.output}, .{ .exit = false });
506 }601 }
507 if (args.positional.len > 1) {
508 return std.cli.@"error"("expected exactly 1 positional arg", .{}, .{ .exit = false });
509 }
510}602}
511603
512fn usageError(writer: ?*Writer, comptime msg: []const u8, args: anytype, exit_on_error: bool) error{Usage} {604fn 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 {...@@ -552,7 +644,7 @@ fn enumValuesExpr(comptime Enum: type) []const u8 {
552 return values_str;644 return values_str;
553}645}
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 {
556 const msg = //648 const msg = //
557 \\usage: {s} [options] [arg...]649 \\usage: {s} [options] [arg...]
558 \\650 \\
...@@ -561,7 +653,7 @@ fn printGeneratedHelp(writer: ?*Writer, prog: []const u8, comptime named_info: s...@@ -561,7 +653,7 @@ fn printGeneratedHelp(writer: ?*Writer, prog: []const u8, comptime named_info: s
561 \\653 \\
562 ;654 ;
563 comptime var arguments_str: []const u8 = "";655 comptime var arguments_str: []const u8 = "";
564 inline for (named_info.fields) |field| {656 inline for (named_fields) |field| {
565 switch (@typeInfo(field.type)) {657 switch (@typeInfo(field.type)) {
566 .bool => {658 .bool => {
567 if (field.defaultValue()) |default| {659 if (field.defaultValue()) |default| {
...@@ -635,121 +727,48 @@ inline fn quoteIfEmpty(comptime s: []const u8) []const u8 {...@@ -635,121 +727,48 @@ inline fn quoteIfEmpty(comptime s: []const u8) []const u8 {
635var failing_writer: Writer = .failing;727var failing_writer: Writer = .failing;
636const silent_options = Options{ .writer = &failing_writer, .exit = false };728const silent_options = Options{ .writer = &failing_writer, .exit = false };
637729
638test "usage errors" {730test "bool" {
639 var arena: std.heap.ArenaAllocator = .init(testing.allocator);731 var arena: std.heap.ArenaAllocator = .init(testing.allocator);
640 defer arena.deinit();732 defer arena.deinit();
641 const allocator = arena.allocator();733 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 argument735 const Args = struct {
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 {
678 named: struct {736 named: struct {
679 name: bool = false,737 b: bool,
680 },738 },
681 positional: []const []const u8 = &.{},739 };
682 }, allocator, &[_][]const u8{"--name=true"}, options));
683 try testing.expect(mem.indexOf(u8, aw.written(), "--name") != null);
684740
685 // missing required argument741 try testing.expectEqualDeep(Args{ .named = .{ .b = true } }, try parseSlice(Args, allocator, &[_][]const u8{"--b"}, .{}));
686 aw.clearRetainingCapacity();742 try testing.expectEqualDeep(Args{ .named = .{ .b = false } }, try parseSlice(Args, allocator, &[_][]const u8{"--no-b"}, .{}));
687 try testing.expectError(error.Usage, parseSlice(struct {743 try testing.expectEqualDeep(Args{ .named = .{ .b = true } }, try parseSlice(Args, allocator, &[_][]const u8{ "--no-b", "--b" }, .{}));
688 named: struct {744 try testing.expectEqualDeep(Args{ .named = .{ .b = false } }, try parseSlice(Args, allocator, &[_][]const u8{ "--b", "--no-b" }, .{}));
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);
694745
695 // parse int error746 try testing.expectError(error.Usage, parseSlice(Args, allocator, &[_][]const u8{"--b=true"}, silent_options));
696 aw.clearRetainingCapacity();747 try testing.expectError(error.Usage, parseSlice(Args, allocator, &[_][]const u8{"--b=false"}, silent_options));
697 try testing.expectError(error.Usage, parseSlice(struct {748}
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);
712749
713 // parse float error750test "string" {
714 aw.clearRetainingCapacity();751 var arena: std.heap.ArenaAllocator = .init(testing.allocator);
715 try testing.expectError(error.Usage, parseSlice(struct {752 defer arena.deinit();
716 named: struct {753 const allocator = arena.allocator();
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);
730754
731 // parse enum error755 const Args = struct {
732 aw.clearRetainingCapacity();
733 try testing.expectError(error.Usage, parseSlice(struct {
734 named: struct {756 named: struct {
735 name: enum { auto, never, always },757 a: []const u8,
758 b: [:0]const u8,
736 },759 },
737 positional: []const []const u8 = &.{},760 };
738 }, allocator, &[_][]const u8{"--name=abc"}, options));761 const args = try parseSlice(Args, allocator, &[_][:0]const u8{
739 try testing.expect(mem.indexOf(u8, aw.written(), "--name") != null);762 "--a", "a",
740 try testing.expect(mem.indexOf(u8, aw.written(), "abc") != null);763 "--b", "b",
741 // Error should suggest the set of options.764 }, .{});
742 try testing.expect(mem.indexOf(u8, aw.written(), "always") != null);
743765
744 // reject single-letter alias-looking arguments766 try testing.expectEqualDeep(Args{
745 aw.clearRetainingCapacity();767 .named = .{
746 try testing.expectError(error.Usage, parseSlice(struct {768 .a = "a",
747 named: struct {769 .b = "b",
748 z: bool = false,
749 },770 },
750 positional: []const []const u8 = &.{},771 }, args);
751 }, allocator, &[_][]const u8{"-z"}, options));
752 try testing.expect(mem.indexOf(u8, aw.written(), "-z") != null);
753}772}
754773
755test "ints and floats" {774test "ints and floats" {
...@@ -768,7 +787,6 @@ test "ints and floats" {...@@ -768,7 +787,6 @@ test "ints and floats" {
768 inf_f32: f32,787 inf_f32: f32,
769 ninf_f64: f64,788 ninf_f64: f64,
770 },789 },
771 positional: []const []const u8 = &.{},
772 };790 };
773 const args = try parseSlice(Args, allocator, &[_][]const u8{791 const args = try parseSlice(Args, allocator, &[_][]const u8{
774 "--int_u32", "0xffffffff",792 "--int_u32", "0xffffffff",
...@@ -792,14 +810,12 @@ test "ints and floats" {...@@ -792,14 +810,12 @@ test "ints and floats" {
792 .inf_f32 = std.math.inf(f32),810 .inf_f32 = std.math.inf(f32),
793 .ninf_f64 = -std.math.inf(f64),811 .ninf_f64 = -std.math.inf(f64),
794 },812 },
795 .positional = &.{},
796 }, args);813 }, args);
797814
798 const Args2 = struct {815 const Args2 = struct {
799 named: struct {816 named: struct {
800 nan: f64,817 nan: f64,
801 },818 },
802 positional: []const []const u8 = &.{},
803 };819 };
804 const args2 = try parseSlice(Args2, allocator, &[_][]const u8{820 const args2 = try parseSlice(Args2, allocator, &[_][]const u8{
805 "--nan", "nAN",821 "--nan", "nAN",
...@@ -808,80 +824,37 @@ test "ints and floats" {...@@ -808,80 +824,37 @@ test "ints and floats" {
808 try testing.expect(std.math.isNan(args2.named.nan));824 try testing.expect(std.math.isNan(args2.named.nan));
809}825}
810826
811test "bool" {827test "array" {
812 var arena: std.heap.ArenaAllocator = .init(testing.allocator);828 var arena: std.heap.ArenaAllocator = .init(testing.allocator);
813 defer arena.deinit();829 defer arena.deinit();
814 const allocator = arena.allocator();830 const allocator = arena.allocator();
815831
816 const Args = struct {832 const Args = struct {
817 named: struct {833 named: struct {
818 b: bool,834 path: []const []const u8 = &.{},
835 id: []const i32 = &.{},
819 },836 },
820 positional: []const []const u8 = &.{},837 positional: struct {
821 };838 args: []const []const u8 = &.{},
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,
841 },839 },
842 positional: []const []const u8 = &.{},
843 };840 };
844 const args = try parseSlice(Args, allocator, &[_][:0]const u8{
845 "--a", "a",
846 "--b", "b",
847 }, .{});
848841
849 try testing.expectEqualDeep(Args{842 try testing.expectEqualDeep(Args{
850 .named = .{843 .named = .{
851 .a = "a",844 .path = &[_][]const u8{ "a", "b", "a" },
852 .b = "b",845 .id = &[_]i32{ 1, -12 },
853 },846 },
854 .positional = &.{},847 .positional = .{
855 }, args);848 .args = &[_][]const u8{ "x", "y" },
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 = &.{},
867 },849 },
868 positional: []const []const u8 = &.{},850 }, try parseSlice(Args, allocator, &[_][]const u8{
869 };
870 const args = try parseSlice(Args, allocator, &[_][]const u8{
871 "--path", "a",851 "--path", "a",
872 "--path", "b",852 "--path", "b",
873 "--path", "a",853 "--path", "a",
874 "--id", "1",854 "--id", "1",
875 "--id", "-12",855 "--id", "-12",
876 }, .{});856 "x", "y",
877857 }, .{}));
878 try testing.expectEqualDeep(Args{
879 .named = .{
880 .path = &[_][]const u8{ "a", "b", "a" },
881 .id = &[_]i32{ 1, -12 },
882 },
883 .positional = &.{},
884 }, args);
885}858}
886859
887test "enum" {860test "enum" {
...@@ -905,7 +878,6 @@ test "enum" {...@@ -905,7 +878,6 @@ test "enum" {
905 VTALRM = 26,878 VTALRM = 26,
906 },879 },
907 },880 },
908 positional: []const []const u8 = &.{},
909 };881 };
910 const args = try parseSlice(Args, allocator, &[_][]const u8{882 const args = try parseSlice(Args, allocator, &[_][]const u8{
911 "--color", "always",883 "--color", "always",
...@@ -919,7 +891,6 @@ test "enum" {...@@ -919,7 +891,6 @@ test "enum" {
919 .guess = .@"the-only-option",891 .guess = .@"the-only-option",
920 .signal = .TERM,892 .signal = .TERM,
921 },893 },
922 .positional = &.{},
923 }, args);894 }, args);
924}895}
925896
...@@ -942,24 +913,20 @@ test "defaults" {...@@ -942,24 +913,20 @@ test "defaults" {
942 force: bool = false,913 force: bool = false,
943 cleanup: bool = true,914 cleanup: bool = true,
944 },915 },
945 positional: []const []const u8 = &.{},
946 };916 };
947917
948 try testing.expectEqualDeep(Args{918 try testing.expectEqualDeep(Args{
949 .named = .{},919 .named = .{},
950 .positional = &.{},
951 }, try parseSlice(Args, allocator, &[_][]const u8{}, .{}));920 }, try parseSlice(Args, allocator, &[_][]const u8{}, .{}));
952 try testing.expectEqualDeep(Args{921 try testing.expectEqualDeep(Args{
953 .named = .{922 .named = .{
954 .color = .always,923 .color = .always,
955 },924 },
956 .positional = &.{},
957 }, try parseSlice(Args, allocator, &[_][]const u8{ "--color", "always" }, .{}));925 }, try parseSlice(Args, allocator, &[_][]const u8{ "--color", "always" }, .{}));
958 try testing.expectEqualDeep(Args{926 try testing.expectEqualDeep(Args{
959 .named = .{927 .named = .{
960 .file = &[_][]const u8{"file.txt"},928 .file = &[_][]const u8{"file.txt"},
961 },929 },
962 .positional = &.{},
963 }, try parseSlice(Args, allocator, &[_][]const u8{ "--file", "file.txt" }, .{}));930 }, try parseSlice(Args, allocator, &[_][]const u8{ "--file", "file.txt" }, .{}));
964931
965 try testing.expectEqualDeep(Args{932 try testing.expectEqualDeep(Args{
...@@ -967,10 +934,208 @@ test "defaults" {...@@ -967,10 +934,208 @@ test "defaults" {
967 .force = true,934 .force = true,
968 .cleanup = false,935 .cleanup = false,
969 },936 },
970 .positional = &.{},
971 }, try parseSlice(Args, allocator, &[_][]const u8{ "--force", "--no-cleanup" }, .{}));937 }, try parseSlice(Args, allocator, &[_][]const u8{ "--force", "--no-cleanup" }, .{}));
972}938}
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
974test "help" {1139test "help" {
975 var arena: std.heap.ArenaAllocator = .init(testing.allocator);1140 var arena: std.heap.ArenaAllocator = .init(testing.allocator);
976 defer arena.deinit();1141 defer arena.deinit();
...@@ -985,7 +1150,6 @@ test "help" {...@@ -985,7 +1150,6 @@ test "help" {
985 int: i32,1150 int: i32,
986 flag: bool,1151 flag: bool,
987 },1152 },
988 positional: []const []const u8 = &.{},
989 }, allocator, &[_][]const u8{"--help"}, options));1153 }, allocator, &[_][]const u8{"--help"}, options));
990 // Because the help output is primarily for humans, don't get too strict in the unit test.1154 // Because the help output is primarily for humans, don't get too strict in the unit test.
991 // Only verify that we see the important stuff that should definitely be there somewhere,1155 // Only verify that we see the important stuff that should definitely be there somewhere,
...@@ -1002,7 +1166,6 @@ test "help" {...@@ -1002,7 +1166,6 @@ test "help" {
1002 named: struct {1166 named: struct {
1003 color: enum { never, auto, always } = .auto,1167 color: enum { never, auto, always } = .auto,
1004 },1168 },
1005 positional: []const []const u8 = &.{},
1006 }, allocator, &[_][]const u8{"--help"}, options));1169 }, allocator, &[_][]const u8{"--help"}, options));
1007 // All allowed values for an enum should be spelled out.1170 // All allowed values for an enum should be spelled out.
1008 try testing.expect(mem.indexOf(u8, aw.written(), "--color") != null);1171 try testing.expect(mem.indexOf(u8, aw.written(), "--color") != null);
...@@ -1016,14 +1179,12 @@ test "help" {...@@ -1016,14 +1179,12 @@ test "help" {
1016 named: struct {1179 named: struct {
1017 name: []const u8,1180 name: []const u8,
1018 },1181 },
1019 positional: []const []const u8 = &.{},
1020 }, allocator, &[_][]const u8{"--help"}, options));1182 }, allocator, &[_][]const u8{"--help"}, options));
1021 const scalar_help = try aw.toOwnedSlice();1183 const scalar_help = try aw.toOwnedSlice();
1022 try testing.expectError(error.Help, parseSlice(struct {1184 try testing.expectError(error.Help, parseSlice(struct {
1023 named: struct {1185 named: struct {
1024 name: []const []const u8 = &.{},1186 name: []const []const u8 = &.{},
1025 },1187 },
1026 positional: []const []const u8 = &.{},
1027 }, allocator, &[_][]const u8{"--help"}, options));1188 }, allocator, &[_][]const u8{"--help"}, options));
1028 try testing.expect(!mem.eql(u8, scalar_help, aw.written()));1189 try testing.expect(!mem.eql(u8, scalar_help, aw.written()));
10291190
...@@ -1035,7 +1196,6 @@ test "help" {...@@ -1035,7 +1196,6 @@ test "help" {
1035 int: i32 = 3,1196 int: i32 = 3,
1036 f: f32 = 1.25,1197 f: f32 = 1.25,
1037 },1198 },
1038 positional: []const []const u8 = &.{},
1039 }, allocator, &[_][]const u8{"--help"}, options));1199 }, allocator, &[_][]const u8{"--help"}, options));
1040 try testing.expect(mem.indexOf(u8, aw.written(), "hello") != null);1200 try testing.expect(mem.indexOf(u8, aw.written(), "hello") != null);
1041 try testing.expect(mem.indexOf(u8, aw.written(), "3") != null);1201 try testing.expect(mem.indexOf(u8, aw.written(), "3") != null);
...@@ -1047,21 +1207,18 @@ test "help" {...@@ -1047,21 +1207,18 @@ test "help" {
1047 named: struct {1207 named: struct {
1048 b: bool,1208 b: bool,
1049 },1209 },
1050 positional: []const []const u8 = &.{},
1051 }, allocator, &[_][]const u8{"--help"}, options));1210 }, allocator, &[_][]const u8{"--help"}, options));
1052 const bool_required_help = try aw.toOwnedSlice();1211 const bool_required_help = try aw.toOwnedSlice();
1053 try testing.expectError(error.Help, parseSlice(struct {1212 try testing.expectError(error.Help, parseSlice(struct {
1054 named: struct {1213 named: struct {
1055 b: bool = true,1214 b: bool = true,
1056 },1215 },
1057 positional: []const []const u8 = &.{},
1058 }, allocator, &[_][]const u8{"--help"}, options));1216 }, allocator, &[_][]const u8{"--help"}, options));
1059 const default_true_help = try aw.toOwnedSlice();1217 const default_true_help = try aw.toOwnedSlice();
1060 try testing.expectError(error.Help, parseSlice(struct {1218 try testing.expectError(error.Help, parseSlice(struct {
1061 named: struct {1219 named: struct {
1062 b: bool = false,1220 b: bool = false,
1063 },1221 },
1064 positional: []const []const u8 = &.{},
1065 }, allocator, &[_][]const u8{"--help"}, options));1222 }, allocator, &[_][]const u8{"--help"}, options));
1066 const default_false_help = try aw.toOwnedSlice();1223 const default_false_help = try aw.toOwnedSlice();
1067 try testing.expect(!mem.eql(u8, bool_required_help, default_true_help));1224 try testing.expect(!mem.eql(u8, bool_required_help, default_true_help));
...@@ -1074,21 +1231,18 @@ test "help" {...@@ -1074,21 +1231,18 @@ test "help" {
1074 named: struct {1231 named: struct {
1075 color: enum { never, auto, always },1232 color: enum { never, auto, always },
1076 },1233 },
1077 positional: []const []const u8 = &.{},
1078 }, allocator, &[_][]const u8{"--help"}, options));1234 }, allocator, &[_][]const u8{"--help"}, options));
1079 const enum_required_help = try aw.toOwnedSlice();1235 const enum_required_help = try aw.toOwnedSlice();
1080 try testing.expectError(error.Help, parseSlice(struct {1236 try testing.expectError(error.Help, parseSlice(struct {
1081 named: struct {1237 named: struct {
1082 color: enum { never, auto, always } = .auto,1238 color: enum { never, auto, always } = .auto,
1083 },1239 },
1084 positional: []const []const u8 = &.{},
1085 }, allocator, &[_][]const u8{"--help"}, options));1240 }, allocator, &[_][]const u8{"--help"}, options));
1086 const default_auto_help = try aw.toOwnedSlice();1241 const default_auto_help = try aw.toOwnedSlice();
1087 try testing.expectError(error.Help, parseSlice(struct {1242 try testing.expectError(error.Help, parseSlice(struct {
1088 named: struct {1243 named: struct {
1089 color: enum { never, auto, always } = .never,1244 color: enum { never, auto, always } = .never,
1090 },1245 },
1091 positional: []const []const u8 = &.{},
1092 }, allocator, &[_][]const u8{"--help"}, options));1246 }, allocator, &[_][]const u8{"--help"}, options));
1093 const default_never_help = try aw.toOwnedSlice();1247 const default_never_help = try aw.toOwnedSlice();
1094 try testing.expect(!mem.eql(u8, enum_required_help, default_auto_help));1248 try testing.expect(!mem.eql(u8, enum_required_help, default_auto_help));
...@@ -1097,16 +1251,11 @@ test "help" {...@@ -1097,16 +1251,11 @@ test "help" {
1097}1251}
10981252
1099test "minimal" {1253test "minimal" {
1100 const Args = struct {1254 const Args = struct {};
1101 named: struct {},
1102 positional: []const []const u8 = &.{},
1103 };
11041255
1105 var arena: std.heap.ArenaAllocator = .init(testing.allocator);1256 var arena: std.heap.ArenaAllocator = .init(testing.allocator);
1106 defer arena.deinit();1257 defer arena.deinit();
1107 const args = try parseSlice(Args, arena.allocator(), &[_][]const u8{}, .{});1258 _ = try parseSlice(Args, arena.allocator(), &[_][]const u8{}, .{});
1108
1109 try testing.expectEqual(@as(usize, 0), args.positional.len);
1110}1259}
11111260
1112test "manual deinit" {1261test "manual deinit" {
...@@ -1116,7 +1265,9 @@ test "manual deinit" {...@@ -1116,7 +1265,9 @@ test "manual deinit" {
1116 int_arr: []const i32 = &.{},1265 int_arr: []const i32 = &.{},
1117 empty_arr: []const []const u8 = &.{},1266 empty_arr: []const []const u8 = &.{},
1118 },1267 },
1119 positional: []const []const u8 = &.{},1268 positional: struct {
1269 args: []const []const u8 = &.{},
1270 },
1120 };1271 };
11211272
1122 const args = try parseSlice(Args, testing.allocator, &[_][]const u8{1273 const args = try parseSlice(Args, testing.allocator, &[_][]const u8{
...@@ -1130,14 +1281,16 @@ test "manual deinit" {...@@ -1130,14 +1281,16 @@ test "manual deinit" {
1130 .str_arr = &.{ "hello1", "hello2" },1281 .str_arr = &.{ "hello1", "hello2" },
1131 .int_arr = &.{ 123456, 789012 },1282 .int_arr = &.{ 123456, 789012 },
1132 },1283 },
1133 .positional = &.{ "positional-12345", "positi" },1284 .positional = .{
1285 .args = &.{ "positional-12345", "positi" },
1286 },
1134 }, args);1287 }, args);
11351288
1136 // Surgically cleanup memory.1289 // Surgically cleanup memory.
1137 testing.allocator.free(args.named.str_arr);1290 testing.allocator.free(args.named.str_arr);
1138 testing.allocator.free(args.named.int_arr);1291 testing.allocator.free(args.named.int_arr);
1139 testing.allocator.free(args.named.empty_arr);1292 testing.allocator.free(args.named.empty_arr);
1140 testing.allocator.free(args.positional);1293 testing.allocator.free(args.positional.args);
1141 // Should be no memory leak errors now.1294 // Should be no memory leak errors now.
1142}1295}
11431296
...@@ -1146,7 +1299,9 @@ test "actually calling error" {...@@ -1146,7 +1299,9 @@ test "actually calling error" {
1146 named: struct {1299 named: struct {
1147 output: []const u8 = "",1300 output: []const u8 = "",
1148 },1301 },
1149 positional: []const []const u8 = &.{},1302 positional: struct {
1303 args: []const []const u8 = &.{},
1304 },
1150 };1305 };
11511306
1152 var arena: std.heap.ArenaAllocator = .init(testing.allocator);1307 var arena: std.heap.ArenaAllocator = .init(testing.allocator);
...@@ -1182,7 +1337,9 @@ test "custom help" {...@@ -1182,7 +1337,9 @@ test "custom help" {
1182 output: []const u8,1337 output: []const u8,
1183 force: bool = false,1338 force: bool = false,
1184 },1339 },
1185 positional: []const []const u8 = &.{},1340 positional: struct {
1341 args: []const []const u8 = &.{},
1342 },
1186 };1343 };
1187 try testing.expectError(error.Help, parseSlice(Args, allocator, &[_][]const u8{"--help"}, options));1344 try testing.expectError(error.Help, parseSlice(Args, allocator, &[_][]const u8{"--help"}, options));
1188 try testing.expectEqualStrings(Args.help, aw.written());1345 try testing.expectEqualStrings(Args.help, aw.written());