| ... | @@ -7,6 +7,7 @@ const isAlphabetic = std.ascii.isAlphabetic; | ... | @@ -7,6 +7,7 @@ const isAlphabetic = std.ascii.isAlphabetic; |
| 7 | const Writer = std.Io.Writer; | 7 | const Writer = std.Io.Writer; |
| 8 | const ArgIterator = std.process.ArgIterator; | 8 | const ArgIterator = std.process.ArgIterator; |
| 9 | const ArenaAllocator = std.heap.ArenaAllocator; | 9 | const ArenaAllocator = std.heap.ArenaAllocator; |
| | 10 | const StructField = std.builtin.Type.StructField; |
| 10 | const mem = std.mem; | 11 | const mem = std.mem; |
| 11 | const Allocator = mem.Allocator; | 12 | const Allocator = mem.Allocator; |
| 12 | | 13 | |
| ... | @@ -29,23 +30,25 @@ pub const Options = struct { | ... | @@ -29,23 +30,25 @@ pub const Options = struct { |
| 29 | pub const Error = error{ | 30 | pub 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; |
| 36 | | 38 | |
| 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 | }; |
| 147 | | 168 | |
| 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`. |
| 174 | pub fn parseIter(comptime Args: type, arena: Allocator, iter: anytype, options: Options) Error!Args { | 195 | pub 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: |
| 179 | | 200 | |
| 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`. |
| 196 | pub fn parseSlice(comptime Args: type, arena: Allocator, argv: anytype, options: Options) Error!Args { | 217 | pub 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 | } |
| 244 | | 267 | |
| ... | @@ -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. |
| 247 | | 270 | |
| 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); |
| 250 | | 276 | |
| 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; |
| 253 | | 280 | |
| 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 | } | | |
| 282 | | 282 | |
| 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 | } |
| 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 | // Always invalid. | 305 | // Always invalid. |
| 306 | // Examples: -h, -flag, -I/path | 306 | // 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 | } |
| 322 | | 332 | |
| ... | @@ -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 | }; |
| 337 | | 347 | |
| 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); |
| 350 | | 360 | |
| 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 | } |
| 408 | | 373 | |
| 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 | } |
| 423 | | 395 | 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 | } |
| 429 | | 411 | |
| 430 | return result; | 412 | return result; |
| 431 | } | 413 | } |
| 432 | | 414 | |
| 433 | fn checkArgsType(comptime Args: type) void { | 415 | /// arg_value is []const u8 or [:0]const u8. |
| 434 | const args_fields = @typeInfo(Args).@"struct".fields; | 416 | fn 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 | } |
| 437 | | 442 | |
| 438 | inline for (@typeInfo(args_fields[0].type).@"struct".fields) |field| { | 443 | fn 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 | } |
| 443 | | 453 | |
| 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 | |
| | 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 | }, | 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. |
| | 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 | /// 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 | }; |
| 499 | | 594 | |
| 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 | } |
| 511 | | 603 | |
| 512 | fn usageError(writer: ?*Writer, comptime msg: []const u8, args: anytype, exit_on_error: bool) error{Usage} { | 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,7 +644,7 @@ fn enumValuesExpr(comptime Enum: type) []const u8 { |
| 552 | return values_str; | 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 | 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 { |
| 635 | var failing_writer: Writer = .failing; | 727 | var failing_writer: Writer = .failing; |
| 636 | const silent_options = Options{ .writer = &failing_writer, .exit = false }; | 728 | const silent_options = Options{ .writer = &failing_writer, .exit = false }; |
| 637 | | 729 | |
| 638 | test "usage errors" { | 730 | test "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); | | |
| 654 | | 734 | |
| 655 | // expected argument | 735 | 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); | | |
| 684 | | 740 | |
| 685 | // missing required argument | 741 | 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); | | |
| 694 | | 745 | |
| 695 | // parse int error | 746 | 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); | | |
| 712 | | 749 | |
| 713 | // parse float error | 750 | test "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); | | |
| 730 | | 754 | |
| 731 | // parse enum error | 755 | 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); | | |
| 743 | | 765 | |
| 744 | // reject single-letter alias-looking arguments | 766 | 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 | } |
| 754 | | 773 | |
| 755 | test "ints and floats" { | 774 | test "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); |
| 797 | | 814 | |
| 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 | } |
| 810 | | 826 | |
| 811 | test "bool" { | 827 | test "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(); |
| 815 | | 831 | |
| 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 | | | |
| 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, | | |
| 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 | 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 | | | |
| 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 = &.{}, | | |
| 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", |
| 877 | | 857 | }, .{})); |
| 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 | } |
| 886 | | 859 | |
| 887 | test "enum" { | 860 | test "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 | } |
| 925 | | 896 | |
| ... | @@ -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 | }; |
| 947 | | 917 | |
| 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" }, .{})); |
| 964 | | 931 | |
| 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 | } |
| 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 | test "help" { | 1139 | test "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())); |
| 1029 | | 1190 | |
| ... | @@ -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 | } |
| 1098 | | 1252 | |
| 1099 | test "minimal" { | 1253 | test "minimal" { |
| 1100 | const Args = struct { | 1254 | const Args = struct {}; |
| 1101 | named: struct {}, | | |
| 1102 | positional: []const []const u8 = &.{}, | | |
| 1103 | }; | | |
| 1104 | | 1255 | |
| 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 | } |
| 1111 | | 1260 | |
| 1112 | test "manual deinit" { | 1261 | test "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 | }; |
| 1121 | | 1272 | |
| 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); |
| 1135 | | 1288 | |
| 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 | } |
| 1143 | | 1296 | |
| ... | @@ -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 | }; |
| 1151 | | 1306 | |
| 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()); |