| ... | ... | @@ -7,6 +7,7 @@ const isAlphabetic = std.ascii.isAlphabetic; |
| 7 | 7 | const Writer = std.Io.Writer; |
| 8 | 8 | const ArgIterator = std.process.ArgIterator; |
| 9 | 9 | const ArenaAllocator = std.heap.ArenaAllocator; |
| 10 | const StructField = std.builtin.Type.StructField; |
| 10 | 11 | const mem = std.mem; |
| 11 | 12 | const Allocator = mem.Allocator; |
| 12 | 13 | |
| ... | ... | @@ -29,23 +30,25 @@ pub const Options = struct { |
| 29 | 30 | pub const Error = error{ |
| 30 | 31 | /// Caused by unrecognized option names, values that cannot be parsed into the appropriate field type, |
| 31 | 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 | 34 | Usage, |
| 33 | | /// The --help argument was given. |
| 35 | /// The --help argument was given (and `options.exit` resolved to `false`). |
| 34 | 36 | Help, |
| 35 | 37 | } || Allocator.Error; |
| 36 | 38 | |
| 37 | 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 | 42 | /// const Args = struct { |
| 41 | 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. |
| 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. |
| 49 | 52 | /// |
| 50 | 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 | 65 | /// Forms (1), (2), and (3) must correspond to a field `Args.named.<name>`; see below for named argument handling. |
| 63 | 66 | /// Form (4) immediately prints the long help documentation and exits or returns `error.Help` depending on options.exit. |
| 64 | 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 | 70 | /// Form (5) is always an error. |
| 68 | 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 | 91 | /// Slice arguments `[]const C` (where `C` is not `u8`) must have a default value, usually `&.{}`. |
| 89 | 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 | 105 | /// It's possible to override the automatically-generated long help documentation by declaring a public constant named `help` in `Args`. |
| 92 | 106 | /// The value must coerce to `[]const u8`. |
| 93 | 107 | /// |
| ... | ... | @@ -104,7 +118,6 @@ pub const Error = error{ |
| 104 | 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 | 149 | level: i8 = -1, |
| 137 | 150 | /// Parsed as the name of the member `--color=never`. |
| 138 | 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 | 155 | seed: u32 = 0, |
| 141 | 156 | @"cache-dir": []const u8 = "", |
| 142 | 157 | listen: []const u8 = "", |
| 143 | 158 | }, |
| 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 | }, |
| 146 | 167 | }; |
| 147 | 168 | |
| 148 | 169 | var arena: ArenaAllocator = .init(testing.allocator); |
| ... | ... | @@ -157,8 +178,8 @@ test parse { |
| 157 | 178 | /// ``` |
| 158 | 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`. |
| 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. |
| 162 | 183 | /// |
| 163 | 184 | /// The first string arg returned by the `iter` (`argv[0]`) is skipped by all the parsing logic. |
| 164 | 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 | 191 | /// |
| 171 | 192 | /// An `ArenaAllocator` is recommended to cleanup the memory allocated from this function; |
| 172 | 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`. |
| 174 | 195 | pub fn parseIter(comptime Args: type, arena: Allocator, iter: anytype, options: Options) Error!Args { |
| 175 | 196 | const argv0 = iter.next(); |
| 176 | 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 | 200 | |
| 180 | 201 | /// Like `parse`, but takes a slice of strings in place of using an `ArgIterator`. |
| 181 | 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 | 204 | /// If `String` does not coerce to `[:0]const u8`, then `Args` cannot have `[:0]const u8` fields. |
| 184 | 205 | /// |
| 185 | 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 | 213 | /// |
| 193 | 214 | /// An `ArenaAllocator` is recommended to cleanup the memory allocated from this function; |
| 194 | 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`. |
| 196 | 217 | pub fn parseSlice(comptime Args: type, arena: Allocator, argv: anytype, options: Options) Error!Args { |
| 197 | 218 | const argvInfo = @typeInfo(@TypeOf(argv)).pointer; |
| 198 | 219 | const String = if (argvInfo.size == .one) |
| ... | ... | @@ -218,7 +239,9 @@ test parseSlice { |
| 218 | 239 | flag: bool = true, |
| 219 | 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 | 246 | const args = try parseSlice(Args, allocator, &[_][]const u8{ |
| 224 | 247 | "--example_required", "a.txt", |
| ... | ... | @@ -238,7 +261,7 @@ test parseSlice { |
| 238 | 261 | .flag = false, |
| 239 | 262 | .@"enum-option" = .always, |
| 240 | 263 | }, |
| 241 | | .positional = &.{ "positional1", "positional2", "-12345678", "--positional4", "--positional=5" }, |
| 264 | .positional = .{ .args = &.{ "positional1", "positional2", "-12345678", "--positional4", "--positional=5" } }, |
| 242 | 265 | }, args); |
| 243 | 266 | } |
| 244 | 267 | |
| ... | ... | @@ -246,42 +269,19 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: [] |
| 246 | 269 | // argv0 has already been consumed. |
| 247 | 270 | |
| 248 | 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); |
| 250 | 276 | |
| 251 | 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; |
| 253 | 280 | |
| 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; |
| 282 | 282 | |
| 283 | 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 | 285 | if (@hasDecl(Args, "help")) { |
| 286 | 286 | // Custom help. |
| 287 | 287 | if (writer) |w| { |
| ... | ... | @@ -293,7 +293,7 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: [] |
| 293 | 293 | file_writer.interface.flush() catch {}; |
| 294 | 294 | } |
| 295 | 295 | } else { |
| 296 | | printGeneratedHelp(writer, prog, named_info); |
| 296 | printGeneratedHelp(writer, prog, named_fields); |
| 297 | 297 | } |
| 298 | 298 | if (exit_on_error) { |
| 299 | 299 | std.process.exit(0); |
| ... | ... | @@ -301,22 +301,32 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: [] |
| 301 | 301 | return error.Help; |
| 302 | 302 | } |
| 303 | 303 | |
| 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 | 305 | // Always invalid. |
| 306 | 306 | // Examples: -h, -flag, -I/path |
| 307 | 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 | 310 | // 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; |
| 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 | 315 | // 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; |
| 320 | 330 | continue; |
| 321 | 331 | } |
| 322 | 332 | |
| ... | ... | @@ -335,12 +345,12 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: [] |
| 335 | 345 | break :blk .{ arg["--".len..], null, false }; |
| 336 | 346 | }; |
| 337 | 347 | |
| 338 | | inline for (named_info.fields, 0..) |field, i| { |
| 348 | inline for (named_fields, 0..) |field, i| { |
| 339 | 349 | if (mem.eql(u8, field.name, arg_name)) { |
| 350 | named_fields_seen[i] = true; |
| 340 | 351 | if (field.type == bool) { |
| 341 | 352 | if (immediate_value != null) return usageError(writer, "cannot specify value for bool argument: {s}", .{arg}, exit_on_error); |
| 342 | 353 | @field(result.named, field.name) = !no_prefixed; |
| 343 | | fields_seen[i] = true; |
| 344 | 354 | break; |
| 345 | 355 | } |
| 346 | 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 | 358 | // All other argument types require a value. |
| 349 | 359 | const arg_value = immediate_value orelse iter.next() orelse return usageError(writer, "expected argument after --{s}", .{field.name}, exit_on_error); |
| 350 | 360 | |
| 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); |
| 399 | 365 | } |
| 400 | | fields_seen[i] = true; |
| 401 | 366 | break; |
| 402 | 367 | } |
| 403 | 368 | } else { |
| ... | ... | @@ -407,74 +372,202 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: [] |
| 407 | 372 | } |
| 408 | 373 | |
| 409 | 374 | // 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; |
| 417 | 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 | } |
| 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 | } |
| 428 | 410 | } |
| 429 | 411 | |
| 430 | 412 | return result; |
| 431 | 413 | } |
| 432 | 414 | |
| 433 | | fn 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. |
| 416 | fn 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 | } |
| 437 | 442 | |
| 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); |
| 443 | fn 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 | } |
| 443 | 453 | |
| 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 | |
| 492 | fn 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 | | }, |
| 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. |
| 534 | fn 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 | |
| 542 | fn 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 | } |
| 549 | fn 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 | } |
| 479 | 572 | |
| 480 | 573 | /// If you do your own validation after getting an `args` from `parse` or similar, |
| ... | ... | @@ -494,7 +587,9 @@ test @"error" { |
| 494 | 587 | named: struct { |
| 495 | 588 | output: []const u8 = "", |
| 496 | 589 | }, |
| 497 | | positional: []const []const u8 = &.{}, |
| 590 | positional: struct { |
| 591 | input: []const u8, |
| 592 | }, |
| 498 | 593 | }; |
| 499 | 594 | |
| 500 | 595 | var arena: std.heap.ArenaAllocator = .init(testing.allocator); |
| ... | ... | @@ -504,9 +599,6 @@ test @"error" { |
| 504 | 599 | if (std.fs.path.isAbsolute(args.named.output)) { |
| 505 | 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 | } |
| 511 | 603 | |
| 512 | 604 | fn usageError(writer: ?*Writer, comptime msg: []const u8, args: anytype, exit_on_error: bool) error{Usage} { |
| ... | ... | @@ -552,7 +644,7 @@ fn enumValuesExpr(comptime Enum: type) []const u8 { |
| 552 | 644 | return values_str; |
| 553 | 645 | } |
| 554 | 646 | |
| 555 | | fn printGeneratedHelp(writer: ?*Writer, prog: []const u8, comptime named_info: std.builtin.Type.Struct) void { |
| 647 | fn printGeneratedHelp(writer: ?*Writer, prog: []const u8, comptime named_fields: []const StructField) void { |
| 556 | 648 | const msg = // |
| 557 | 649 | \\usage: {s} [options] [arg...] |
| 558 | 650 | \\ |
| ... | ... | @@ -561,7 +653,7 @@ fn printGeneratedHelp(writer: ?*Writer, prog: []const u8, comptime named_info: s |
| 561 | 653 | \\ |
| 562 | 654 | ; |
| 563 | 655 | comptime var arguments_str: []const u8 = ""; |
| 564 | | inline for (named_info.fields) |field| { |
| 656 | inline for (named_fields) |field| { |
| 565 | 657 | switch (@typeInfo(field.type)) { |
| 566 | 658 | .bool => { |
| 567 | 659 | if (field.defaultValue()) |default| { |
| ... | ... | @@ -635,121 +727,48 @@ inline fn quoteIfEmpty(comptime s: []const u8) []const u8 { |
| 635 | 727 | var failing_writer: Writer = .failing; |
| 636 | 728 | const silent_options = Options{ .writer = &failing_writer, .exit = false }; |
| 637 | 729 | |
| 638 | | test "usage errors" { |
| 730 | test "bool" { |
| 639 | 731 | var arena: std.heap.ArenaAllocator = .init(testing.allocator); |
| 640 | 732 | defer arena.deinit(); |
| 641 | 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); |
| 654 | 734 | |
| 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 { |
| 678 | 736 | named: struct { |
| 679 | | name: bool = false, |
| 737 | b: bool, |
| 680 | 738 | }, |
| 681 | | positional: []const []const u8 = &.{}, |
| 682 | | }, allocator, &[_][]const u8{"--name=true"}, options)); |
| 683 | | try testing.expect(mem.indexOf(u8, aw.written(), "--name") != null); |
| 739 | }; |
| 684 | 740 | |
| 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" }, .{})); |
| 694 | 745 | |
| 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 | } |
| 712 | 749 | |
| 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); |
| 750 | test "string" { |
| 751 | var arena: std.heap.ArenaAllocator = .init(testing.allocator); |
| 752 | defer arena.deinit(); |
| 753 | const allocator = arena.allocator(); |
| 730 | 754 | |
| 731 | | // parse enum error |
| 732 | | aw.clearRetainingCapacity(); |
| 733 | | try testing.expectError(error.Usage, parseSlice(struct { |
| 755 | const Args = struct { |
| 734 | 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 = &.{}, |
| 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 | }, .{}); |
| 743 | 765 | |
| 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", |
| 749 | 770 | }, |
| 750 | | positional: []const []const u8 = &.{}, |
| 751 | | }, allocator, &[_][]const u8{"-z"}, options)); |
| 752 | | try testing.expect(mem.indexOf(u8, aw.written(), "-z") != null); |
| 771 | }, args); |
| 753 | 772 | } |
| 754 | 773 | |
| 755 | 774 | test "ints and floats" { |
| ... | ... | @@ -768,7 +787,6 @@ test "ints and floats" { |
| 768 | 787 | inf_f32: f32, |
| 769 | 788 | ninf_f64: f64, |
| 770 | 789 | }, |
| 771 | | positional: []const []const u8 = &.{}, |
| 772 | 790 | }; |
| 773 | 791 | const args = try parseSlice(Args, allocator, &[_][]const u8{ |
| 774 | 792 | "--int_u32", "0xffffffff", |
| ... | ... | @@ -792,14 +810,12 @@ test "ints and floats" { |
| 792 | 810 | .inf_f32 = std.math.inf(f32), |
| 793 | 811 | .ninf_f64 = -std.math.inf(f64), |
| 794 | 812 | }, |
| 795 | | .positional = &.{}, |
| 796 | 813 | }, args); |
| 797 | 814 | |
| 798 | 815 | const Args2 = struct { |
| 799 | 816 | named: struct { |
| 800 | 817 | nan: f64, |
| 801 | 818 | }, |
| 802 | | positional: []const []const u8 = &.{}, |
| 803 | 819 | }; |
| 804 | 820 | const args2 = try parseSlice(Args2, allocator, &[_][]const u8{ |
| 805 | 821 | "--nan", "nAN", |
| ... | ... | @@ -808,80 +824,37 @@ test "ints and floats" { |
| 808 | 824 | try testing.expect(std.math.isNan(args2.named.nan)); |
| 809 | 825 | } |
| 810 | 826 | |
| 811 | | test "bool" { |
| 827 | test "array" { |
| 812 | 828 | var arena: std.heap.ArenaAllocator = .init(testing.allocator); |
| 813 | 829 | defer arena.deinit(); |
| 814 | 830 | const allocator = arena.allocator(); |
| 815 | 831 | |
| 816 | 832 | const Args = struct { |
| 817 | 833 | named: struct { |
| 818 | | b: bool, |
| 834 | path: []const []const u8 = &.{}, |
| 835 | id: []const i32 = &.{}, |
| 819 | 836 | }, |
| 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 | | |
| 832 | | test "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 = &.{}, |
| 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 | | }, .{}); |
| 848 | 841 | |
| 849 | 842 | try testing.expectEqualDeep(Args{ |
| 850 | 843 | .named = .{ |
| 851 | | .a = "a", |
| 852 | | .b = "b", |
| 844 | .path = &[_][]const u8{ "a", "b", "a" }, |
| 845 | .id = &[_]i32{ 1, -12 }, |
| 853 | 846 | }, |
| 854 | | .positional = &.{}, |
| 855 | | }, args); |
| 856 | | } |
| 857 | | |
| 858 | | test "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" }, |
| 867 | 849 | }, |
| 868 | | positional: []const []const u8 = &.{}, |
| 869 | | }; |
| 870 | | const args = try parseSlice(Args, allocator, &[_][]const u8{ |
| 850 | }, try parseSlice(Args, allocator, &[_][]const u8{ |
| 871 | 851 | "--path", "a", |
| 872 | 852 | "--path", "b", |
| 873 | 853 | "--path", "a", |
| 874 | 854 | "--id", "1", |
| 875 | 855 | "--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 | }, .{})); |
| 885 | 858 | } |
| 886 | 859 | |
| 887 | 860 | test "enum" { |
| ... | ... | @@ -905,7 +878,6 @@ test "enum" { |
| 905 | 878 | VTALRM = 26, |
| 906 | 879 | }, |
| 907 | 880 | }, |
| 908 | | positional: []const []const u8 = &.{}, |
| 909 | 881 | }; |
| 910 | 882 | const args = try parseSlice(Args, allocator, &[_][]const u8{ |
| 911 | 883 | "--color", "always", |
| ... | ... | @@ -919,7 +891,6 @@ test "enum" { |
| 919 | 891 | .guess = .@"the-only-option", |
| 920 | 892 | .signal = .TERM, |
| 921 | 893 | }, |
| 922 | | .positional = &.{}, |
| 923 | 894 | }, args); |
| 924 | 895 | } |
| 925 | 896 | |
| ... | ... | @@ -942,24 +913,20 @@ test "defaults" { |
| 942 | 913 | force: bool = false, |
| 943 | 914 | cleanup: bool = true, |
| 944 | 915 | }, |
| 945 | | positional: []const []const u8 = &.{}, |
| 946 | 916 | }; |
| 947 | 917 | |
| 948 | 918 | try testing.expectEqualDeep(Args{ |
| 949 | 919 | .named = .{}, |
| 950 | | .positional = &.{}, |
| 951 | 920 | }, try parseSlice(Args, allocator, &[_][]const u8{}, .{})); |
| 952 | 921 | try testing.expectEqualDeep(Args{ |
| 953 | 922 | .named = .{ |
| 954 | 923 | .color = .always, |
| 955 | 924 | }, |
| 956 | | .positional = &.{}, |
| 957 | 925 | }, try parseSlice(Args, allocator, &[_][]const u8{ "--color", "always" }, .{})); |
| 958 | 926 | try testing.expectEqualDeep(Args{ |
| 959 | 927 | .named = .{ |
| 960 | 928 | .file = &[_][]const u8{"file.txt"}, |
| 961 | 929 | }, |
| 962 | | .positional = &.{}, |
| 963 | 930 | }, try parseSlice(Args, allocator, &[_][]const u8{ "--file", "file.txt" }, .{})); |
| 964 | 931 | |
| 965 | 932 | try testing.expectEqualDeep(Args{ |
| ... | ... | @@ -967,10 +934,208 @@ test "defaults" { |
| 967 | 934 | .force = true, |
| 968 | 935 | .cleanup = false, |
| 969 | 936 | }, |
| 970 | | .positional = &.{}, |
| 971 | 937 | }, try parseSlice(Args, allocator, &[_][]const u8{ "--force", "--no-cleanup" }, .{})); |
| 972 | 938 | } |
| 973 | 939 | |
| 940 | test "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 | |
| 1004 | test "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 | |
| 974 | 1139 | test "help" { |
| 975 | 1140 | var arena: std.heap.ArenaAllocator = .init(testing.allocator); |
| 976 | 1141 | defer arena.deinit(); |
| ... | ... | @@ -985,7 +1150,6 @@ test "help" { |
| 985 | 1150 | int: i32, |
| 986 | 1151 | flag: bool, |
| 987 | 1152 | }, |
| 988 | | positional: []const []const u8 = &.{}, |
| 989 | 1153 | }, allocator, &[_][]const u8{"--help"}, options)); |
| 990 | 1154 | // Because the help output is primarily for humans, don't get too strict in the unit test. |
| 991 | 1155 | // Only verify that we see the important stuff that should definitely be there somewhere, |
| ... | ... | @@ -1002,7 +1166,6 @@ test "help" { |
| 1002 | 1166 | named: struct { |
| 1003 | 1167 | color: enum { never, auto, always } = .auto, |
| 1004 | 1168 | }, |
| 1005 | | positional: []const []const u8 = &.{}, |
| 1006 | 1169 | }, allocator, &[_][]const u8{"--help"}, options)); |
| 1007 | 1170 | // All allowed values for an enum should be spelled out. |
| 1008 | 1171 | try testing.expect(mem.indexOf(u8, aw.written(), "--color") != null); |
| ... | ... | @@ -1016,14 +1179,12 @@ test "help" { |
| 1016 | 1179 | named: struct { |
| 1017 | 1180 | name: []const u8, |
| 1018 | 1181 | }, |
| 1019 | | positional: []const []const u8 = &.{}, |
| 1020 | 1182 | }, allocator, &[_][]const u8{"--help"}, options)); |
| 1021 | 1183 | const scalar_help = try aw.toOwnedSlice(); |
| 1022 | 1184 | try testing.expectError(error.Help, parseSlice(struct { |
| 1023 | 1185 | named: struct { |
| 1024 | 1186 | name: []const []const u8 = &.{}, |
| 1025 | 1187 | }, |
| 1026 | | positional: []const []const u8 = &.{}, |
| 1027 | 1188 | }, allocator, &[_][]const u8{"--help"}, options)); |
| 1028 | 1189 | try testing.expect(!mem.eql(u8, scalar_help, aw.written())); |
| 1029 | 1190 | |
| ... | ... | @@ -1035,7 +1196,6 @@ test "help" { |
| 1035 | 1196 | int: i32 = 3, |
| 1036 | 1197 | f: f32 = 1.25, |
| 1037 | 1198 | }, |
| 1038 | | positional: []const []const u8 = &.{}, |
| 1039 | 1199 | }, allocator, &[_][]const u8{"--help"}, options)); |
| 1040 | 1200 | try testing.expect(mem.indexOf(u8, aw.written(), "hello") != null); |
| 1041 | 1201 | try testing.expect(mem.indexOf(u8, aw.written(), "3") != null); |
| ... | ... | @@ -1047,21 +1207,18 @@ test "help" { |
| 1047 | 1207 | named: struct { |
| 1048 | 1208 | b: bool, |
| 1049 | 1209 | }, |
| 1050 | | positional: []const []const u8 = &.{}, |
| 1051 | 1210 | }, allocator, &[_][]const u8{"--help"}, options)); |
| 1052 | 1211 | const bool_required_help = try aw.toOwnedSlice(); |
| 1053 | 1212 | try testing.expectError(error.Help, parseSlice(struct { |
| 1054 | 1213 | named: struct { |
| 1055 | 1214 | b: bool = true, |
| 1056 | 1215 | }, |
| 1057 | | positional: []const []const u8 = &.{}, |
| 1058 | 1216 | }, allocator, &[_][]const u8{"--help"}, options)); |
| 1059 | 1217 | const default_true_help = try aw.toOwnedSlice(); |
| 1060 | 1218 | try testing.expectError(error.Help, parseSlice(struct { |
| 1061 | 1219 | named: struct { |
| 1062 | 1220 | b: bool = false, |
| 1063 | 1221 | }, |
| 1064 | | positional: []const []const u8 = &.{}, |
| 1065 | 1222 | }, allocator, &[_][]const u8{"--help"}, options)); |
| 1066 | 1223 | const default_false_help = try aw.toOwnedSlice(); |
| 1067 | 1224 | try testing.expect(!mem.eql(u8, bool_required_help, default_true_help)); |
| ... | ... | @@ -1074,21 +1231,18 @@ test "help" { |
| 1074 | 1231 | named: struct { |
| 1075 | 1232 | color: enum { never, auto, always }, |
| 1076 | 1233 | }, |
| 1077 | | positional: []const []const u8 = &.{}, |
| 1078 | 1234 | }, allocator, &[_][]const u8{"--help"}, options)); |
| 1079 | 1235 | const enum_required_help = try aw.toOwnedSlice(); |
| 1080 | 1236 | try testing.expectError(error.Help, parseSlice(struct { |
| 1081 | 1237 | named: struct { |
| 1082 | 1238 | color: enum { never, auto, always } = .auto, |
| 1083 | 1239 | }, |
| 1084 | | positional: []const []const u8 = &.{}, |
| 1085 | 1240 | }, allocator, &[_][]const u8{"--help"}, options)); |
| 1086 | 1241 | const default_auto_help = try aw.toOwnedSlice(); |
| 1087 | 1242 | try testing.expectError(error.Help, parseSlice(struct { |
| 1088 | 1243 | named: struct { |
| 1089 | 1244 | color: enum { never, auto, always } = .never, |
| 1090 | 1245 | }, |
| 1091 | | positional: []const []const u8 = &.{}, |
| 1092 | 1246 | }, allocator, &[_][]const u8{"--help"}, options)); |
| 1093 | 1247 | const default_never_help = try aw.toOwnedSlice(); |
| 1094 | 1248 | try testing.expect(!mem.eql(u8, enum_required_help, default_auto_help)); |
| ... | ... | @@ -1097,16 +1251,11 @@ test "help" { |
| 1097 | 1251 | } |
| 1098 | 1252 | |
| 1099 | 1253 | test "minimal" { |
| 1100 | | const Args = struct { |
| 1101 | | named: struct {}, |
| 1102 | | positional: []const []const u8 = &.{}, |
| 1103 | | }; |
| 1254 | const Args = struct {}; |
| 1104 | 1255 | |
| 1105 | 1256 | var arena: std.heap.ArenaAllocator = .init(testing.allocator); |
| 1106 | 1257 | 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{}, .{}); |
| 1110 | 1259 | } |
| 1111 | 1260 | |
| 1112 | 1261 | test "manual deinit" { |
| ... | ... | @@ -1116,7 +1265,9 @@ test "manual deinit" { |
| 1116 | 1265 | int_arr: []const i32 = &.{}, |
| 1117 | 1266 | empty_arr: []const []const u8 = &.{}, |
| 1118 | 1267 | }, |
| 1119 | | positional: []const []const u8 = &.{}, |
| 1268 | positional: struct { |
| 1269 | args: []const []const u8 = &.{}, |
| 1270 | }, |
| 1120 | 1271 | }; |
| 1121 | 1272 | |
| 1122 | 1273 | const args = try parseSlice(Args, testing.allocator, &[_][]const u8{ |
| ... | ... | @@ -1130,14 +1281,16 @@ test "manual deinit" { |
| 1130 | 1281 | .str_arr = &.{ "hello1", "hello2" }, |
| 1131 | 1282 | .int_arr = &.{ 123456, 789012 }, |
| 1132 | 1283 | }, |
| 1133 | | .positional = &.{ "positional-12345", "positi" }, |
| 1284 | .positional = .{ |
| 1285 | .args = &.{ "positional-12345", "positi" }, |
| 1286 | }, |
| 1134 | 1287 | }, args); |
| 1135 | 1288 | |
| 1136 | 1289 | // Surgically cleanup memory. |
| 1137 | 1290 | testing.allocator.free(args.named.str_arr); |
| 1138 | 1291 | testing.allocator.free(args.named.int_arr); |
| 1139 | 1292 | testing.allocator.free(args.named.empty_arr); |
| 1140 | | testing.allocator.free(args.positional); |
| 1293 | testing.allocator.free(args.positional.args); |
| 1141 | 1294 | // Should be no memory leak errors now. |
| 1142 | 1295 | } |
| 1143 | 1296 | |
| ... | ... | @@ -1146,7 +1299,9 @@ test "actually calling error" { |
| 1146 | 1299 | named: struct { |
| 1147 | 1300 | output: []const u8 = "", |
| 1148 | 1301 | }, |
| 1149 | | positional: []const []const u8 = &.{}, |
| 1302 | positional: struct { |
| 1303 | args: []const []const u8 = &.{}, |
| 1304 | }, |
| 1150 | 1305 | }; |
| 1151 | 1306 | |
| 1152 | 1307 | var arena: std.heap.ArenaAllocator = .init(testing.allocator); |
| ... | ... | @@ -1182,7 +1337,9 @@ test "custom help" { |
| 1182 | 1337 | output: []const u8, |
| 1183 | 1338 | force: bool = false, |
| 1184 | 1339 | }, |
| 1185 | | positional: []const []const u8 = &.{}, |
| 1340 | positional: struct { |
| 1341 | args: []const []const u8 = &.{}, |
| 1342 | }, |
| 1186 | 1343 | }; |
| 1187 | 1344 | try testing.expectError(error.Help, parseSlice(Args, allocator, &[_][]const u8{"--help"}, options)); |
| 1188 | 1345 | try testing.expectEqualStrings(Args.help, aw.written()); |