├── .gitignore ├── LICENSE ├── README.md ├── build.zig ├── build.zig.zon └── src └── root.zig /.gitignore: -------------------------------------------------------------------------------- 1 | .zig-cache 2 | zig-out 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Structopt 2 | 3 | This is the argument parser I use in my engine and various supporting tools. 4 | 5 | # Zig Version 6 | 7 | [`main`](https://github.com/Games-by-Mason/structopt/tree/main) loosely tracks Zig master. For support for Zig 0.14.0, use [v1.0.0](https://github.com/Games-by-Mason/structopt/releases/tag/v1.0.0). 8 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub fn build(b: *std.Build) void { 4 | const target = b.standardTargetOptions(.{}); 5 | const optimize = b.standardOptimizeOption(.{}); 6 | _ = b.addModule("structopt", .{ 7 | .root_source_file = b.path("src/root.zig"), 8 | .target = target, 9 | .optimize = optimize, 10 | }); 11 | 12 | const unit_tests = b.addTest(.{ 13 | .root_source_file = b.path("src/root.zig"), 14 | .target = target, 15 | .optimize = optimize, 16 | }); 17 | const run_unit_tests = b.addRunArtifact(unit_tests); 18 | const test_step = b.step("test", "Run unit tests"); 19 | test_step.dependOn(&run_unit_tests.step); 20 | } 21 | -------------------------------------------------------------------------------- /build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = .structopt, 3 | .fingerprint = 0x2636533c024a0b3a, 4 | .version = "1.0.0", 5 | .minimum_zig_version = "0.15.0-dev.388+05e217607", 6 | .dependencies = .{}, 7 | .paths = .{ 8 | "build.zig", 9 | "build.zig.zon", 10 | "src", 11 | "LICENSE", 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /src/root.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Allocator = std.mem.Allocator; 3 | const ArgIterator = std.process.ArgIterator; 4 | const FieldEnum = std.meta.FieldEnum; 5 | const log = std.log.scoped(.structopt); 6 | const expect = std.testing.expect; 7 | const expectEqual = std.testing.expectEqual; 8 | const expectEqualStrings = std.testing.expectEqualStrings; 9 | const expectEqualSlices = std.testing.expectEqualSlices; 10 | const expectError = std.testing.expectError; 11 | 12 | pub const Error = error{ OutOfMemory, Parser, Help }; 13 | 14 | pub const Command = struct { 15 | const Self = @This(); 16 | 17 | name: [:0]const u8, 18 | description: ?[]const u8 = null, 19 | named_args: []const NamedArg = &.{}, 20 | positional_args: []const PositionalArg = &.{}, 21 | subcommands: []const Command = &.{}, 22 | 23 | pub fn Result(comptime self: Command) type { 24 | return struct { 25 | program_name: []const u8, 26 | named: @FieldType(self.Subcommand(), "named"), 27 | positional: @FieldType(self.Subcommand(), "positional"), 28 | subcommand: @FieldType(self.Subcommand(), "subcommand"), 29 | 30 | fn fromSubcommand(program_name: []const u8, subcommand: self.Subcommand()) @This() { 31 | return .{ 32 | .program_name = program_name, 33 | .named = subcommand.named, 34 | .positional = subcommand.positional, 35 | .subcommand = subcommand.subcommand, 36 | }; 37 | } 38 | }; 39 | } 40 | 41 | /// The result of parsing a command 42 | pub fn Subcommand(comptime self: Command) type { 43 | var named_fields: [self.named_args.len]std.builtin.Type.StructField = undefined; 44 | for (self.named_args, 0..) |arg, i| { 45 | if (arg.type == ?bool) { 46 | @compileError(arg.long ++ ": booleans cannot be nullable"); 47 | } 48 | if (@typeInfo(arg.type) == .optional and arg.accum) { 49 | @compileError(arg.long ++ ": accum arguments cannot be nullable"); 50 | } 51 | const T = if (arg.accum) std.ArrayList(arg.type) else arg.type; 52 | named_fields[i] = .{ 53 | .name = arg.long, 54 | .type = T, 55 | .default_value_ptr = arg.default, 56 | .is_comptime = false, 57 | .alignment = @alignOf(T), 58 | }; 59 | } 60 | const NamedResults = @Type(.{ .@"struct" = .{ 61 | .layout = .auto, 62 | .fields = &named_fields, 63 | .decls = &.{}, 64 | .is_tuple = false, 65 | } }); 66 | 67 | var positional_fields: [self.positional_args.len]std.builtin.Type.StructField = undefined; 68 | for (self.positional_args, 0..) |arg, i| { 69 | if (@typeInfo(arg.type) == .optional) { 70 | @compileError(arg.meta ++ ": positional arguments cannot be nullable"); 71 | } 72 | if (arg.type == bool) { 73 | @compileError(arg.meta ++ ": positional arguments cannot be booleans"); 74 | } 75 | positional_fields[i] = .{ 76 | .name = arg.meta, 77 | .type = arg.type, 78 | .default_value_ptr = null, 79 | .is_comptime = false, 80 | .alignment = @alignOf(arg.type), 81 | }; 82 | } 83 | const PositionalResults = @Type(.{ .@"struct" = .{ 84 | .layout = .auto, 85 | .fields = &positional_fields, 86 | .decls = &.{}, 87 | .is_tuple = false, 88 | } }); 89 | 90 | var command_tags: [self.subcommands.len]std.builtin.Type.EnumField = undefined; 91 | for (self.subcommands, 0..) |command, i| { 92 | command_tags[i] = .{ 93 | .name = command.name, 94 | .value = i, 95 | }; 96 | } 97 | const SubcommandTag = @Type(.{ .@"enum" = .{ 98 | .tag_type = u16, 99 | .fields = &command_tags, 100 | .decls = &.{}, 101 | .is_exhaustive = true, 102 | } }); 103 | 104 | const Subcommands = if (self.subcommands.len > 0) b: { 105 | var command_fields: [self.subcommands.len]std.builtin.Type.UnionField = undefined; 106 | for (self.subcommands, 0..) |command, i| { 107 | const Current = Subcommand(command); 108 | command_fields[i] = .{ 109 | .name = command.name, 110 | .type = Current, 111 | .alignment = @alignOf(Current), 112 | }; 113 | } 114 | break :b @Type(.{ .@"union" = .{ 115 | .layout = .auto, 116 | .tag_type = SubcommandTag, 117 | .fields = &command_fields, 118 | .decls = &.{}, 119 | } }); 120 | } else void; 121 | 122 | return struct { 123 | named: NamedResults, 124 | positional: PositionalResults, 125 | subcommand: ?Subcommands, 126 | }; 127 | } 128 | 129 | /// Frees the parsed result. Only necessary if lists are used. 130 | pub fn parseFree(comptime self: @This(), result: self.Result()) void { 131 | inline for (self.named_args) |named_arg| { 132 | if (named_arg.accum) { 133 | @field(result.named, named_arg.long).deinit(); 134 | } 135 | } 136 | } 137 | 138 | /// Show the help menu 139 | pub fn usageBrief(self: @This()) void { 140 | self.usageImpl(true); 141 | } 142 | 143 | pub fn usage(self: @This()) void { 144 | self.usageImpl(false); 145 | } 146 | 147 | fn usageImpl(self: @This(), comptime brief: bool) void { 148 | log.info("{}", .{self.fmtUsage(brief)}); 149 | } 150 | 151 | /// Parse the command line arguments for this process, exit on help or failure. Panics on OOM. 152 | pub fn parseOrExit( 153 | self: @This(), 154 | gpa: Allocator, 155 | iter: *std.process.ArgIterator, 156 | ) self.Result() { 157 | return self.parse(gpa, iter) catch |err| switch (err) { 158 | error.Help => std.process.exit(0), 159 | error.Parser => std.process.exit(2), 160 | error.OutOfMemory => @panic("OOM"), 161 | }; 162 | } 163 | 164 | /// Parse the command line arguments for this process, return the defaults on help or failure. 165 | /// Requires every argument have a default. 166 | pub fn parseOrDefaults(self: @This(), iter: *std.process.ArgIterator) self.Result() { 167 | return self.parse(iter) catch |err| switch (err) { 168 | error.Help, error.Parser => .{}, 169 | }; 170 | } 171 | 172 | /// Parse the command line arguments for this process. 173 | pub fn parse(self: @This(), gpa: Allocator) Error!self.Result() { 174 | var iter = try std.process.argsWithAllocator(gpa); 175 | defer iter.deinit(); 176 | return self.parseFromIter(gpa, &iter); 177 | } 178 | 179 | /// Parse commands from the given slice. 180 | pub fn parseFromSlice( 181 | self: @This(), 182 | gpa: Allocator, 183 | args: []const [:0]const u8, 184 | ) Error!self.Result() { 185 | const Iter = struct { 186 | slice: []const [:0]const u8, 187 | 188 | fn init(slice: []const [:0]const u8) @This() { 189 | return .{ .slice = slice }; 190 | } 191 | 192 | fn next(iter: *@This()) ?[:0]const u8 { 193 | if (iter.slice.len == 0) return null; 194 | const result = iter.slice[0]; 195 | iter.slice = iter.slice[1..]; 196 | return result; 197 | } 198 | 199 | fn skip(iter: *@This()) bool { 200 | if (iter.slice.len == 0) return false; 201 | iter.slice = iter.slice[1..]; 202 | return true; 203 | } 204 | }; 205 | 206 | var iter = Iter.init(args); 207 | return self.parseFromIter(gpa, &iter); 208 | } 209 | 210 | /// Parses the arguments from an iterator, usually `std.process.ArgIterator`. 211 | pub fn parseFromIter(self: @This(), gpa: Allocator, iter: anytype) Error!self.Result() { 212 | const program_name = iter.*.next() orelse { 213 | log.err("expected program name", .{}); 214 | return error.Parser; 215 | }; 216 | const command = try self.parseCommand(gpa, iter); 217 | return .fromSubcommand(program_name, command); 218 | } 219 | 220 | fn parseCommand(self: @This(), gpa: Allocator, iter: anytype) Error!self.Subcommand() { 221 | // Validate types 222 | comptime validateLongName(self.name); 223 | inline for (self.named_args) |arg| { 224 | comptime validateLongName(arg.long); 225 | if (arg.short) |short| comptime validateShortName(short); 226 | } 227 | 228 | // Initialize result with all defaults set 229 | const ParsedCommand = self.Subcommand(); 230 | var result: ParsedCommand = undefined; 231 | result.subcommand = null; 232 | inline for (self.named_args) |arg| { 233 | if (arg.default) |default| { 234 | @field(result.named, arg.long) = @as( 235 | *const arg.type, 236 | @alignCast(@ptrCast(default)), 237 | ).*; 238 | } 239 | } 240 | 241 | // Parse the arguments 242 | const NamedArgEnum = FieldEnum(@FieldType(ParsedCommand, "named")); 243 | var named_args: std.EnumSet(NamedArgEnum) = .{}; 244 | inline for (self.named_args) |named_arg| { 245 | if (named_arg.default != null or named_arg.accum) { 246 | named_args.insert(std.meta.stringToEnum(NamedArgEnum, named_arg.long).?); 247 | } 248 | if (named_arg.accum) { 249 | @field(result.named, named_arg.long) = .init(gpa); 250 | } 251 | } 252 | 253 | // Parse the named arguments 254 | var peeked: ?[:0]const u8 = while (iter.*.next()) |arg_str| { 255 | // Stop parsing if we find a positional argument 256 | if (arg_str[0] != '-') { 257 | break arg_str; 258 | } 259 | 260 | // Check for help 261 | try self.checkHelp(arg_str); 262 | 263 | // Parse the argument name 264 | const arg_name = if (arg_str.len > 2 and arg_str[1] == '-') 265 | arg_str[2..] 266 | else 267 | arg_str[1..]; 268 | const negated = std.mem.startsWith(u8, arg_name, "no-"); 269 | const lookup = if (negated) arg_name[3..] else arg_name; 270 | 271 | // Look for this argument in the list of fields 272 | const field_enum = b: { 273 | if (lookup.len == 1) { 274 | break :b getShortArgs(self).get(lookup); 275 | } else { 276 | break :b std.meta.stringToEnum(NamedArgEnum, lookup); 277 | } 278 | } orelse { 279 | log.err("unexpected argument \"{s}\"", .{arg_name}); 280 | self.usageBrief(); 281 | return error.Parser; 282 | }; 283 | if (std.meta.fields(NamedArgEnum).len == 0) unreachable; 284 | 285 | // Mark this argument as found 286 | named_args.insert(field_enum); 287 | 288 | // Parse the argument value 289 | switch (field_enum) { 290 | inline else => |field_enum_inline| { 291 | const field = @typeInfo(@FieldType(ParsedCommand, "named")).@"struct" 292 | .fields[@intFromEnum(field_enum_inline)]; 293 | if (negated) { 294 | if (field.type == bool) { 295 | @field(result.named, field.name) = false; 296 | } else if (@typeInfo(field.type) == .optional) { 297 | @field(result.named, field.name) = null; 298 | } else if (comptime self.argIsAccum(@intFromEnum(field_enum_inline))) { 299 | @field(result.named, field.name).clearRetainingCapacity(); 300 | } else { 301 | log.err("unexpected argument \"{s}\"", .{arg_name}); 302 | self.usageBrief(); 303 | return error.Parser; 304 | } 305 | } else { 306 | var peeked: ?[:0]const u8 = null; 307 | if (comptime self.argIsAccum(@intFromEnum(field_enum_inline))) { 308 | const Items = @TypeOf(@field(result.named, field.name).items); 309 | const Item = @typeInfo(Items).pointer.child; 310 | try @field(result.named, field.name).append(try self.parseValue( 311 | Item, 312 | field.name, 313 | iter, 314 | &peeked, 315 | )); 316 | } else { 317 | @field(result.named, field.name) = try self.parseValue( 318 | field.type, 319 | field.name, 320 | iter, 321 | &peeked, 322 | ); 323 | } 324 | } 325 | }, 326 | } 327 | } else null; 328 | 329 | // Parse positional arguments 330 | inline for (self.positional_args) |field| { 331 | const value = try self.parseValue( 332 | field.type, 333 | field.meta, 334 | iter, 335 | &peeked, 336 | ); 337 | @field(result.positional, field.meta) = value; 338 | } 339 | 340 | // Make sure all non optional args were found 341 | for (std.meta.tags(NamedArgEnum)) |arg| { 342 | if (!named_args.contains(arg)) { 343 | log.err("missing required argument \"{s}\"", .{@tagName(arg)}); 344 | self.usageBrief(); 345 | return error.Parser; 346 | } 347 | } 348 | 349 | // Parse the command, if any 350 | if (peeked orelse iter.*.next()) |next| b: { 351 | // Check for help 352 | try self.checkHelp(next); 353 | 354 | // Check if it matches a command 355 | if (self.subcommands.len > 0) { 356 | const Commands = @typeInfo(@FieldType(self.Subcommand(), "subcommand")) 357 | .optional.child; 358 | if (std.meta.stringToEnum(FieldEnum(Commands), next)) |command_enum| { 359 | switch (command_enum) { 360 | inline else => |cmd| { 361 | const parsed_command = try self.subcommands[@intFromEnum(cmd)] 362 | .parseCommand(gpa, iter); 363 | const Current = @typeInfo(@FieldType(self.Subcommand(), "subcommand")) 364 | .optional.child; 365 | result.subcommand = @unionInit(Current, @tagName(cmd), parsed_command); 366 | break :b; 367 | }, 368 | } 369 | } 370 | } 371 | 372 | // Emit an error 373 | log.err("unexpected command \"{s}\"", .{next}); 374 | self.usageBrief(); 375 | return error.Parser; 376 | } 377 | 378 | return result; 379 | } 380 | 381 | fn argIsAccum(self: @This(), arg: usize) bool { 382 | if (arg >= self.named_args.len) return false; 383 | return self.named_args[arg].accum; 384 | } 385 | 386 | fn checkHelp(self: @This(), arg_str: []const u8) error{Help}!void { 387 | if (std.mem.eql(u8, arg_str, "-h") or std.mem.eql(u8, arg_str, "--help")) { 388 | self.usage(); 389 | return error.Help; 390 | } 391 | } 392 | 393 | fn getShortArgs( 394 | comptime self: @This(), 395 | ) std.StaticStringMap(FieldEnum(@FieldType(self.Subcommand(), "named"))) { 396 | const ArgEnum = FieldEnum(@FieldType(self.Subcommand(), "named")); 397 | const max_len = self.named_args.len; 398 | comptime var short_args: [max_len]struct { []const u8, ArgEnum } = undefined; 399 | comptime var len = 0; 400 | inline for (self.named_args) |arg| { 401 | if (arg.short) |short| { 402 | short_args[len] = .{ &.{short}, @field(ArgEnum, arg.long) }; 403 | len += 1; 404 | } 405 | } 406 | return std.StaticStringMap(ArgEnum).initComptime(short_args[0..len]); 407 | } 408 | 409 | fn parseValue( 410 | self: @This(), 411 | Type: type, 412 | comptime arg_str: []const u8, 413 | iter: anytype, 414 | peeked: *?[:0]const u8, 415 | ) Error!Type { 416 | // If we're optional, get the inner type 417 | const Inner = switch (@typeInfo(Type)) { 418 | .optional => |optional| optional.child, 419 | else => Type, 420 | }; 421 | 422 | // Parse the type 423 | switch (@typeInfo(Inner)) { 424 | .bool => return true, 425 | .@"enum" => { 426 | const value_str = try self.parseValueStr(arg_str, iter, peeked); 427 | return std.meta.stringToEnum(Inner, value_str) orelse { 428 | log.err("{s}: unexpected value \"{s}\"", .{ 429 | arg_str, 430 | value_str, 431 | }); 432 | self.usageBrief(); 433 | return error.Parser; 434 | }; 435 | }, 436 | .int => { 437 | const value_str = try self.parseValueStr(arg_str, iter, peeked); 438 | return std.fmt.parseInt(Inner, value_str, 0) catch { 439 | log.err("{s}: expected {}, found \"{s}\"", .{ 440 | arg_str, 441 | Inner, 442 | value_str, 443 | }); 444 | self.usageBrief(); 445 | return error.Parser; 446 | }; 447 | }, 448 | .float => { 449 | const value_str = try self.parseValueStr(arg_str, iter, peeked); 450 | return std.fmt.parseFloat(Inner, value_str) catch { 451 | log.err("{s}: expected {}, found \"{s}\"", .{ 452 | arg_str, 453 | Inner, 454 | value_str, 455 | }); 456 | self.usageBrief(); 457 | return error.Parser; 458 | }; 459 | }, 460 | .pointer => |pointer| { 461 | if (pointer.child != u8 or !pointer.is_const) { 462 | unsupportedArgumentType(arg_str, Type); 463 | } 464 | return try self.parseValueStr(arg_str, iter, peeked); 465 | }, 466 | else => unsupportedArgumentType(arg_str, Type), 467 | } 468 | } 469 | 470 | fn parseValueStr( 471 | self: @This(), 472 | arg_str: []const u8, 473 | iter: anytype, 474 | peeked: *?[:0]const u8, 475 | ) Error![:0]const u8 { 476 | const value_str = peeked.* orelse iter.*.next() orelse { 477 | log.err("{s}: expected a value", .{arg_str}); 478 | self.usageBrief(); 479 | return error.Parser; 480 | }; 481 | 482 | if (value_str[0] == '-') { 483 | // Check for help 484 | try self.checkHelp(value_str); 485 | 486 | // Emit an error 487 | log.err("{s}: expected a value", .{arg_str}); 488 | self.usageBrief(); 489 | return error.Parser; 490 | } 491 | 492 | peeked.* = null; 493 | 494 | return value_str; 495 | } 496 | 497 | fn formatUsage( 498 | self: FormatUsageData, 499 | comptime fmt: []const u8, 500 | options: std.fmt.FormatOptions, 501 | writer: anytype, 502 | ) !void { 503 | _ = fmt; 504 | _ = options; 505 | 506 | // At what column to start help 507 | const col = 50; 508 | 509 | // Brief 510 | try writer.print("usage: {s}\n", .{self.options.name}); 511 | if (self.brief) { 512 | try writer.print("--help for more info", .{}); 513 | return; 514 | } 515 | 516 | // Help message 517 | if (self.options.description) |description| { 518 | try writer.print("\n{s}\n", .{description}); 519 | } 520 | 521 | // Named args 522 | if (self.options.named_args.len > 0) { 523 | try writer.writeAll("\noptions:\n"); 524 | inline for (self.options.named_args) |arg| { 525 | try writeArg( 526 | col, 527 | writer, 528 | false, 529 | arg.long, 530 | arg.short, 531 | arg.type, 532 | arg.description, 533 | arg.accum, 534 | arg.default, 535 | ); 536 | } 537 | } 538 | 539 | // Positional args 540 | if (self.options.positional_args.len > 0) { 541 | try writer.writeAll("\npositional arguments:\n"); 542 | inline for (self.options.positional_args) |arg| { 543 | try writeArg( 544 | col, 545 | writer, 546 | true, 547 | arg.meta, 548 | null, 549 | arg.type, 550 | arg.description, 551 | false, 552 | null, 553 | ); 554 | } 555 | } 556 | 557 | // Subcommands: 558 | if (self.options.subcommands.len > 0) { 559 | try writer.writeAll("\nsubcommands:\n"); 560 | inline for (self.options.subcommands) |subcommand| { 561 | const sub_fmt = " {s}"; 562 | const sub_args = .{subcommand.name}; 563 | try writer.print(sub_fmt, sub_args); 564 | if (subcommand.description) |description| { 565 | const count = std.fmt.count(sub_fmt, sub_args); 566 | if (std.math.sub(usize, col, count) catch null) |padding| { 567 | try writer.writeByteNTimes(' ', padding); 568 | } 569 | try writer.print("{s}\n", .{description}); 570 | } 571 | } 572 | } 573 | } 574 | 575 | fn writeArg( 576 | col: usize, 577 | writer: anytype, 578 | positional: bool, 579 | comptime long: []const u8, 580 | short: ?u8, 581 | T: type, 582 | description: ?[]const u8, 583 | accum: bool, 584 | default: ?*const anyopaque, 585 | ) !void { 586 | // Get the inner type if optional 587 | const Inner: type = switch (@typeInfo(T)) { 588 | .optional => |optional| optional.child, 589 | else => T, 590 | }; 591 | 592 | // Write the argument, and measure how many characters it took 593 | var count = if (short) |s| b: { 594 | const name_fmt = " -{c}, --{s}{}"; 595 | const name_args = .{ s, long, fmtType(T) }; 596 | try writer.print(name_fmt, name_args); 597 | break :b std.fmt.count(name_fmt, name_args); 598 | } else b: { 599 | const prefix = if (positional) "" else "--"; 600 | const name_fmt = " {s}{s}{}"; 601 | const name_args = .{ prefix, long, fmtType(T) }; 602 | try writer.print(name_fmt, name_args); 603 | break :b std.fmt.count(name_fmt, name_args); 604 | }; 605 | 606 | if (default) |untyped| { 607 | const typed: *const T = @alignCast(@ptrCast(untyped)); 608 | const default_fmt = if (Inner == []const u8 or Inner == [:0]const u8 or @typeInfo(Inner) == .@"enum") b: { 609 | break :b " (={?s})"; 610 | } else if (@typeInfo(Inner) == .float) b: { 611 | break :b " (={?d})"; 612 | } else b: { 613 | break :b " (={?})"; 614 | }; 615 | const default_args = if (@typeInfo(Inner) == .@"enum") b: { 616 | if (Inner != T) { 617 | if (typed.*) |some| { 618 | break :b .{@tagName(some)}; 619 | } else { 620 | break :b .{"null"}; 621 | } 622 | } else { 623 | break :b .{@tagName(typed.*)}; 624 | } 625 | } else b: { 626 | break :b .{typed.*}; 627 | }; 628 | count += std.fmt.count(default_fmt, default_args); 629 | try writer.print(default_fmt, default_args); 630 | } 631 | 632 | if (accum) { 633 | const str = " (accum)"; 634 | count += str.len; 635 | try writer.print(str, .{}); 636 | } 637 | 638 | // Write the help message offset by the correct number of characters 639 | if (description) |desc| { 640 | if (std.math.sub(usize, col, count) catch null) |padding| { 641 | try writer.writeByteNTimes(' ', padding); 642 | } 643 | try writer.writeAll(desc); 644 | } 645 | try writer.writeByte('\n'); 646 | 647 | // If we're an enum, list all the options 648 | if (@typeInfo(Inner) == .@"enum") { 649 | for (std.meta.fieldNames(Inner)) |name| { 650 | try writer.writeByteNTimes(' ', if (positional) 4 else 6); 651 | try writer.print("{s}\n", .{name}); 652 | } 653 | } 654 | 655 | // If we're optional, a boolean, or a list, display the "no-" variant 656 | if (Inner != T or Inner == bool or accum) { 657 | try writer.print(" --no-{s}\n", .{long}); 658 | } 659 | } 660 | 661 | fn formatType( 662 | T: type, 663 | comptime fmt: []const u8, 664 | options: std.fmt.FormatOptions, 665 | writer: anytype, 666 | ) !void { 667 | _ = fmt; 668 | _ = options; 669 | const Inner = switch (@typeInfo(T)) { 670 | .optional => |optional| optional.child, 671 | else => T, 672 | }; 673 | const optional = if (Inner != T) "?" else ""; 674 | switch (@typeInfo(Inner)) { 675 | .bool => {}, 676 | .int, .float => try writer.print(" <{s}{s}>", .{ optional, @typeName(Inner) }), 677 | .@"enum" => if (optional.len > 0) try writer.print(" {s}", .{optional}), 678 | .pointer => try writer.print(" <{s}string>", .{optional}), 679 | else => unreachable, 680 | } 681 | } 682 | 683 | const FormatUsageData = struct { 684 | options: Self, 685 | brief: bool, 686 | }; 687 | 688 | fn fmtUsage(self: @This(), brief: bool) std.fmt.Formatter(formatUsage) { 689 | return .{ .data = .{ .options = self, .brief = brief } }; 690 | } 691 | 692 | fn fmtType(T: type) std.fmt.Formatter(formatType) { 693 | return .{ .data = T }; 694 | } 695 | }; 696 | 697 | pub const PositionalArg = struct { 698 | meta: [:0]const u8, 699 | description: ?[:0]const u8, 700 | type: type, 701 | 702 | pub const Options = struct { 703 | meta: [:0]const u8, 704 | description: ?[:0]const u8 = null, 705 | }; 706 | 707 | pub fn init(Type: type, options: Options) @This() { 708 | // The user could do this directly, we're just maintaining syntactic similarity with named 709 | // args for copy paste convenience. 710 | return .{ 711 | .meta = options.meta, 712 | .description = options.description, 713 | .type = Type, 714 | }; 715 | } 716 | }; 717 | 718 | pub const NamedArg = struct { 719 | long: [:0]const u8, 720 | short: ?u8 = null, 721 | description: ?[:0]const u8, 722 | type: type, 723 | default: ?*const anyopaque, 724 | accum: bool = false, 725 | 726 | pub fn Options(Type: type) type { 727 | return struct { 728 | long: [:0]const u8, 729 | short: ?u8 = null, 730 | description: ?[:0]const u8 = null, 731 | required: bool = false, 732 | default: union(enum) { 733 | required: void, 734 | value: Type, 735 | } = .required, 736 | accum: bool = false, 737 | }; 738 | } 739 | 740 | pub const AccumOptions = struct { 741 | long: [:0]const u8, 742 | short: ?u8 = null, 743 | description: ?[:0]const u8 = null, 744 | }; 745 | 746 | pub fn init(Type: type, options: Options(Type)) @This() { 747 | // The user could do this directly, but it is tricky to set default correctly--not only is 748 | // the address of and pointer cast necessary giving up type safety, null is a valid default 749 | // value, which is syntactically tricky to get right! This wrapper makes it easy. 750 | return .{ 751 | .long = options.long, 752 | .short = options.short, 753 | .description = options.description, 754 | .type = Type, 755 | .default = switch (options.default) { 756 | .required => null, 757 | .value => |v| @ptrCast(&v), 758 | }, 759 | .accum = false, 760 | }; 761 | } 762 | 763 | pub fn initAccum(Type: type, options: AccumOptions) @This() { 764 | // For now, we don't support the required or default options on accumulation arguments. This 765 | // can be added later if there's a use for them. 766 | return .{ 767 | .long = options.long, 768 | .short = options.short, 769 | .description = options.description, 770 | .type = Type, 771 | .accum = true, 772 | .default = null, 773 | }; 774 | } 775 | }; 776 | 777 | fn validateLongName(comptime name: []const u8) void { 778 | if (name.len < 2) { 779 | // This prevents collisions with short names, and makes parsing a little simpler 780 | @compileError("long names must be at least two characters"); 781 | } 782 | if (name[0] == '-') { 783 | @compileError("argument may not start with '-': " ++ name); 784 | } 785 | if (std.mem.startsWith(u8, name, "no-")) { 786 | @compileError("argument may not start with \"no-\": " ++ name); 787 | } 788 | if (std.mem.eql(u8, name, "help")) { 789 | @compileError("use of reserved argument name: " ++ name); 790 | } 791 | inline for (name) |c| { 792 | switch (c) { 793 | 'a'...'z', 'A'...'Z', '0'...'9', '-', '_' => {}, 794 | else => @compileError( 795 | "unsupported character '" ++ @as([1]u8, .{c}) ++ "' in argument: " ++ name, 796 | ), 797 | } 798 | } 799 | } 800 | 801 | fn validateShortName(comptime c: u8) void { 802 | switch (c) { 803 | 'a'...'z', 'A'...'Z', '0'...'9' => {}, 804 | else => @compileError( 805 | "unsupported character '" ++ @as([1]u8, .{c}) ++ "' as short name", 806 | ), 807 | } 808 | } 809 | 810 | fn unsupportedArgumentType(comptime arg_str: []const u8, ty: type) noreturn { 811 | @compileError(arg_str ++ ": unsupported argument type " ++ @typeName(ty)); 812 | } 813 | 814 | test "all types nullable required" { 815 | const Enum = enum { foo, bar }; 816 | const options: Command = .{ 817 | .name = "command-name", 818 | .named_args = &.{ 819 | NamedArg.init(bool, .{ 820 | .long = "bool", 821 | }), 822 | NamedArg.init(?u32, .{ 823 | .long = "u32", 824 | }), 825 | NamedArg.init(?Enum, .{ 826 | .long = "enum", 827 | }), 828 | NamedArg.init(?[]const u8, .{ 829 | .long = "string", 830 | }), 831 | NamedArg.init(?f32, .{ 832 | .long = "f32", 833 | }), 834 | }, 835 | .positional_args = &.{ 836 | PositionalArg.init(u8, .{ 837 | .meta = "U8", 838 | }), 839 | PositionalArg.init([]const u8, .{ 840 | .meta = "STRING", 841 | }), 842 | PositionalArg.init(Enum, .{ 843 | .meta = "ENUM", 844 | }), 845 | PositionalArg.init(f32, .{ 846 | .meta = "F32", 847 | }), 848 | }, 849 | }; 850 | 851 | const Result = options.Result(); 852 | const undef: Result = undefined; 853 | try expectEqual(5, std.meta.fields(@FieldType(Result, "named")).len); 854 | try expectEqual(4, std.meta.fields(@FieldType(Result, "positional")).len); 855 | try expect(@FieldType(Result, "subcommand") == ?void); 856 | try expectEqual(bool, @TypeOf(undef.named.bool)); 857 | try expectEqual(?u32, @TypeOf(undef.named.u32)); 858 | try expectEqual(?Enum, @TypeOf(undef.named.@"enum")); 859 | try expectEqual(?[]const u8, @TypeOf(undef.named.string)); 860 | try expectEqual(?f32, @TypeOf(undef.named.f32)); 861 | try expectEqual(u8, @TypeOf(undef.positional.U8)); 862 | try expectEqual([]const u8, @TypeOf(undef.positional.STRING)); 863 | try expectEqual(Enum, @TypeOf(undef.positional.ENUM)); 864 | try expectEqual(f32, @TypeOf(undef.positional.F32)); 865 | 866 | // All set 867 | try expectEqual(Result{ 868 | .program_name = "path", 869 | .named = .{ 870 | .bool = true, 871 | .u32 = 123, 872 | .@"enum" = .bar, 873 | .string = "world", 874 | .f32 = 1.5, 875 | }, 876 | .positional = .{ 877 | .U8 = 1, 878 | .STRING = "hello", 879 | .ENUM = .foo, 880 | .F32 = 2.5, 881 | }, 882 | .subcommand = null, 883 | }, try options.parseFromSlice(std.testing.allocator, &.{ 884 | "path", 885 | 886 | "--u32", 887 | "123", 888 | "--enum", 889 | "bar", 890 | "--string", 891 | "world", 892 | "--bool", 893 | "--f32", 894 | "1.5", 895 | 896 | "1", 897 | "hello", 898 | "foo", 899 | "2.5", 900 | })); 901 | 902 | // All null 903 | try expectEqual(Result{ 904 | .program_name = "path1", 905 | .named = .{ 906 | .bool = false, 907 | .u32 = null, 908 | .@"enum" = null, 909 | .string = null, 910 | .f32 = null, 911 | }, 912 | .positional = .{ 913 | .U8 = 1, 914 | .STRING = "hello", 915 | .ENUM = .foo, 916 | .F32 = 10.1, 917 | }, 918 | .subcommand = null, 919 | }, try options.parseFromSlice(std.testing.allocator, &.{ 920 | "path1", 921 | 922 | "--no-u32", 923 | "--no-enum", 924 | "--no-string", 925 | "--no-bool", 926 | "--no-f32", 927 | 928 | "1", 929 | "hello", 930 | "foo", 931 | "10.1", 932 | })); 933 | } 934 | 935 | test "all types defaults" { 936 | const Enum = enum { foo, bar }; 937 | const options: Command = .{ 938 | .name = "command-name", 939 | .named_args = &.{ 940 | NamedArg.init(bool, .{ 941 | .long = "bool", 942 | .default = .{ .value = true }, 943 | }), 944 | NamedArg.init(u32, .{ 945 | .long = "u32", 946 | .default = .{ .value = 123 }, 947 | }), 948 | NamedArg.init(Enum, .{ 949 | .long = "enum", 950 | .default = .{ .value = .bar }, 951 | }), 952 | NamedArg.init([]const u8, .{ 953 | .long = "string", 954 | .default = .{ .value = "default" }, 955 | }), 956 | NamedArg.init(f32, .{ 957 | .long = "f32", 958 | .default = .{ .value = 123.456 }, 959 | }), 960 | }, 961 | .positional_args = &.{ 962 | PositionalArg.init(u8, .{ 963 | .meta = "U8", 964 | }), 965 | PositionalArg.init([]const u8, .{ 966 | .meta = "STRING", 967 | }), 968 | PositionalArg.init(Enum, .{ 969 | .meta = "ENUM", 970 | }), 971 | PositionalArg.init(f32, .{ 972 | .meta = "F32", 973 | }), 974 | }, 975 | }; 976 | 977 | const Result = options.Result(); 978 | const undef: Result = undefined; 979 | try expectEqual(5, std.meta.fields(@FieldType(Result, "named")).len); 980 | try expectEqual(4, std.meta.fields(@FieldType(Result, "positional")).len); 981 | try expect(@FieldType(Result, "subcommand") == ?void); 982 | try expectEqual(bool, @TypeOf(undef.named.bool)); 983 | try expectEqual(u32, @TypeOf(undef.named.u32)); 984 | try expectEqual(Enum, @TypeOf(undef.named.@"enum")); 985 | try expectEqual([]const u8, @TypeOf(undef.named.string)); 986 | try expectEqual(f32, @TypeOf(undef.named.f32)); 987 | try expectEqual(u8, @TypeOf(undef.positional.U8)); 988 | try expectEqual([]const u8, @TypeOf(undef.positional.STRING)); 989 | try expectEqual(Enum, @TypeOf(undef.positional.ENUM)); 990 | try expectEqual(f32, @TypeOf(undef.positional.F32)); 991 | 992 | // All set 993 | try expectEqual(Result{ 994 | .program_name = "path", 995 | .named = .{ 996 | .bool = true, 997 | .u32 = 123, 998 | .@"enum" = .bar, 999 | .string = "world", 1000 | .f32 = 1.5, 1001 | }, 1002 | .positional = .{ 1003 | .U8 = 1, 1004 | .STRING = "hello", 1005 | .ENUM = .foo, 1006 | .F32 = 2.5, 1007 | }, 1008 | .subcommand = null, 1009 | }, try options.parseFromSlice(std.testing.allocator, &.{ 1010 | "path", 1011 | 1012 | "--u32", 1013 | "123", 1014 | "--enum", 1015 | "bar", 1016 | "--string", 1017 | "world", 1018 | "--bool", 1019 | "--f32", 1020 | "1.5", 1021 | 1022 | "1", 1023 | "hello", 1024 | "foo", 1025 | "2.5", 1026 | })); 1027 | 1028 | // All skipped 1029 | try expectEqual(Result{ 1030 | .program_name = "path", 1031 | .named = .{ 1032 | .bool = true, 1033 | .u32 = 123, 1034 | .@"enum" = .bar, 1035 | .string = "default", 1036 | .f32 = 123.456, 1037 | }, 1038 | .positional = .{ 1039 | .U8 = 1, 1040 | .STRING = "hello", 1041 | .ENUM = .foo, 1042 | .F32 = 5.5, 1043 | }, 1044 | .subcommand = null, 1045 | }, try options.parseFromSlice(std.testing.allocator, &.{ 1046 | "path", 1047 | 1048 | "1", 1049 | "hello", 1050 | "foo", 1051 | "5.5", 1052 | })); 1053 | } 1054 | 1055 | test "all types defaults and nullable but not null" { 1056 | const Enum = enum { foo, bar }; 1057 | const options: Command = .{ 1058 | .name = "command-name", 1059 | .named_args = &.{ 1060 | NamedArg.init(bool, .{ 1061 | .long = "bool", 1062 | .default = .{ .value = true }, 1063 | }), 1064 | NamedArg.init(?u32, .{ 1065 | .long = "u32", 1066 | .default = .{ .value = 123 }, 1067 | }), 1068 | NamedArg.init(?Enum, .{ 1069 | .long = "enum", 1070 | .default = .{ .value = .bar }, 1071 | }), 1072 | NamedArg.init(?[]const u8, .{ 1073 | .long = "string", 1074 | .default = .{ .value = "default" }, 1075 | }), 1076 | NamedArg.init(?f32, .{ 1077 | .long = "f32", 1078 | .default = .{ .value = 123.456 }, 1079 | }), 1080 | }, 1081 | .positional_args = &.{ 1082 | PositionalArg.init(u8, .{ 1083 | .meta = "U8", 1084 | }), 1085 | PositionalArg.init([]const u8, .{ 1086 | .meta = "STRING", 1087 | }), 1088 | PositionalArg.init(Enum, .{ 1089 | .meta = "ENUM", 1090 | }), 1091 | PositionalArg.init(f32, .{ 1092 | .meta = "F32", 1093 | }), 1094 | }, 1095 | }; 1096 | 1097 | const Result = options.Result(); 1098 | const undef: Result = undefined; 1099 | try expectEqual(5, std.meta.fields(@FieldType(Result, "named")).len); 1100 | try expectEqual(4, std.meta.fields(@FieldType(Result, "positional")).len); 1101 | try expect(@FieldType(Result, "subcommand") == ?void); 1102 | try expectEqual(bool, @TypeOf(undef.named.bool)); 1103 | try expectEqual(?u32, @TypeOf(undef.named.u32)); 1104 | try expectEqual(?Enum, @TypeOf(undef.named.@"enum")); 1105 | try expectEqual(?[]const u8, @TypeOf(undef.named.string)); 1106 | try expectEqual(?f32, @TypeOf(undef.named.f32)); 1107 | try expectEqual(u8, @TypeOf(undef.positional.U8)); 1108 | try expectEqual([]const u8, @TypeOf(undef.positional.STRING)); 1109 | try expectEqual(Enum, @TypeOf(undef.positional.ENUM)); 1110 | try expectEqual(f32, @TypeOf(undef.positional.F32)); 1111 | 1112 | // All set 1113 | try expectEqual(Result{ 1114 | .program_name = "path", 1115 | .named = .{ 1116 | .bool = true, 1117 | .u32 = 123, 1118 | .@"enum" = .bar, 1119 | .string = "world", 1120 | .f32 = 1.5, 1121 | }, 1122 | .positional = .{ 1123 | .U8 = 1, 1124 | .STRING = "hello", 1125 | .ENUM = .foo, 1126 | .F32 = 2.5, 1127 | }, 1128 | .subcommand = null, 1129 | }, try options.parseFromSlice(std.testing.allocator, &.{ 1130 | "path", 1131 | 1132 | "--u32", 1133 | "123", 1134 | "--enum", 1135 | "bar", 1136 | "--string", 1137 | "world", 1138 | "--bool", 1139 | "--f32", 1140 | "1.5", 1141 | 1142 | "1", 1143 | "hello", 1144 | "foo", 1145 | "2.5", 1146 | })); 1147 | 1148 | // All skipped 1149 | try expectEqual(Result{ 1150 | .program_name = "path", 1151 | .named = .{ 1152 | .bool = true, 1153 | .u32 = 123, 1154 | .@"enum" = .bar, 1155 | .string = "default", 1156 | .f32 = 123.456, 1157 | }, 1158 | .positional = .{ 1159 | .U8 = 1, 1160 | .STRING = "hello", 1161 | .ENUM = .foo, 1162 | .F32 = 2.5, 1163 | }, 1164 | .subcommand = null, 1165 | }, try options.parseFromSlice(std.testing.allocator, &.{ 1166 | "path", 1167 | 1168 | "1", 1169 | "hello", 1170 | "foo", 1171 | "2.5", 1172 | })); 1173 | 1174 | // All null 1175 | try expectEqual(Result{ 1176 | .program_name = "path", 1177 | .named = .{ 1178 | .bool = false, 1179 | .u32 = null, 1180 | .@"enum" = null, 1181 | .string = null, 1182 | .f32 = null, 1183 | }, 1184 | .positional = .{ 1185 | .U8 = 1, 1186 | .STRING = "hello", 1187 | .ENUM = .foo, 1188 | .F32 = 2.5, 1189 | }, 1190 | .subcommand = null, 1191 | }, try options.parseFromSlice(std.testing.allocator, &.{ 1192 | "path", 1193 | 1194 | "--no-u32", 1195 | "--no-enum", 1196 | "--no-string", 1197 | "--no-bool", 1198 | "--no-f32", 1199 | 1200 | "1", 1201 | "hello", 1202 | "foo", 1203 | "2.5", 1204 | })); 1205 | } 1206 | 1207 | test "all types defaults and nullable and null" { 1208 | const Enum = enum { foo, bar }; 1209 | const options: Command = .{ 1210 | .name = "command-name", 1211 | .named_args = &.{ 1212 | NamedArg.init(bool, .{ 1213 | .long = "bool", 1214 | .default = .{ .value = false }, 1215 | }), 1216 | NamedArg.init(?u32, .{ 1217 | .long = "u32", 1218 | .default = .{ .value = null }, 1219 | }), 1220 | NamedArg.init(?Enum, .{ 1221 | .long = "enum", 1222 | .default = .{ .value = null }, 1223 | }), 1224 | NamedArg.init(?[]const u8, .{ 1225 | .long = "string", 1226 | .default = .{ .value = null }, 1227 | }), 1228 | NamedArg.init(?f32, .{ 1229 | .long = "f32", 1230 | .default = .{ .value = null }, 1231 | }), 1232 | }, 1233 | .positional_args = &.{ 1234 | PositionalArg.init(u8, .{ 1235 | .meta = "U8", 1236 | }), 1237 | PositionalArg.init([]const u8, .{ 1238 | .meta = "STRING", 1239 | }), 1240 | PositionalArg.init(Enum, .{ 1241 | .meta = "ENUM", 1242 | }), 1243 | PositionalArg.init(f32, .{ 1244 | .meta = "F32", 1245 | }), 1246 | }, 1247 | }; 1248 | 1249 | const Result = options.Result(); 1250 | const undef: Result = undefined; 1251 | try expectEqual(5, std.meta.fields(@FieldType(Result, "named")).len); 1252 | try expectEqual(4, std.meta.fields(@FieldType(Result, "positional")).len); 1253 | try expect(@FieldType(Result, "subcommand") == ?void); 1254 | try expectEqual(bool, @TypeOf(undef.named.bool)); 1255 | try expectEqual(?u32, @TypeOf(undef.named.u32)); 1256 | try expectEqual(?Enum, @TypeOf(undef.named.@"enum")); 1257 | try expectEqual(?[]const u8, @TypeOf(undef.named.string)); 1258 | try expectEqual(?f32, @TypeOf(undef.named.f32)); 1259 | try expectEqual(u8, @TypeOf(undef.positional.U8)); 1260 | try expectEqual([]const u8, @TypeOf(undef.positional.STRING)); 1261 | try expectEqual(Enum, @TypeOf(undef.positional.ENUM)); 1262 | try expectEqual(f32, @TypeOf(undef.positional.F32)); 1263 | 1264 | // All set 1265 | try expectEqual(Result{ 1266 | .program_name = "path", 1267 | .named = .{ 1268 | .bool = true, 1269 | .u32 = 123, 1270 | .@"enum" = .bar, 1271 | .string = "world", 1272 | .f32 = 1.5, 1273 | }, 1274 | .positional = .{ 1275 | .U8 = 1, 1276 | .STRING = "hello", 1277 | .ENUM = .foo, 1278 | .F32 = 2.5, 1279 | }, 1280 | .subcommand = null, 1281 | }, try options.parseFromSlice(std.testing.allocator, &.{ 1282 | "path", 1283 | 1284 | "--u32", 1285 | "123", 1286 | "--enum", 1287 | "bar", 1288 | "--string", 1289 | "world", 1290 | "--bool", 1291 | "--f32", 1292 | "1.5", 1293 | 1294 | "1", 1295 | "hello", 1296 | "foo", 1297 | "2.5", 1298 | })); 1299 | 1300 | // All skipped 1301 | try expectEqual(Result{ 1302 | .program_name = "path", 1303 | .named = .{ 1304 | .bool = false, 1305 | .u32 = null, 1306 | .@"enum" = null, 1307 | .string = null, 1308 | .f32 = null, 1309 | }, 1310 | .positional = .{ 1311 | .U8 = 1, 1312 | .STRING = "hello", 1313 | .ENUM = .foo, 1314 | .F32 = 2.5, 1315 | }, 1316 | .subcommand = null, 1317 | }, try options.parseFromSlice(std.testing.allocator, &.{ 1318 | "path", 1319 | 1320 | "1", 1321 | "hello", 1322 | "foo", 1323 | "2.5", 1324 | })); 1325 | } 1326 | 1327 | test "all types required" { 1328 | const Enum = enum { foo, bar }; 1329 | const options: Command = .{ 1330 | .name = "command-name", 1331 | .named_args = &.{ 1332 | NamedArg.init(bool, .{ 1333 | .long = "bool", 1334 | .short = 'b', 1335 | }), 1336 | NamedArg.init(u32, .{ 1337 | .long = "u32", 1338 | .short = 'u', 1339 | }), 1340 | NamedArg.init(Enum, .{ 1341 | .long = "enum", 1342 | .short = 'e', 1343 | }), 1344 | NamedArg.init([]const u8, .{ 1345 | .long = "string", 1346 | .short = 's', 1347 | }), 1348 | NamedArg.init(f32, .{ 1349 | .long = "f32", 1350 | .short = 'f', 1351 | }), 1352 | }, 1353 | .positional_args = &.{ 1354 | PositionalArg.init(u8, .{ 1355 | .meta = "U8", 1356 | }), 1357 | PositionalArg.init([]const u8, .{ 1358 | .meta = "STRING", 1359 | }), 1360 | PositionalArg.init(Enum, .{ 1361 | .meta = "ENUM", 1362 | }), 1363 | PositionalArg.init(f32, .{ 1364 | .meta = "F32", 1365 | }), 1366 | }, 1367 | }; 1368 | 1369 | const Result = options.Result(); 1370 | const undef: Result = undefined; 1371 | try expectEqual(5, std.meta.fields(@FieldType(Result, "named")).len); 1372 | try expectEqual(4, std.meta.fields(@FieldType(Result, "positional")).len); 1373 | try expect(@FieldType(Result, "subcommand") == ?void); 1374 | try expectEqual(bool, @TypeOf(undef.named.bool)); 1375 | try expectEqual(u32, @TypeOf(undef.named.u32)); 1376 | try expectEqual(Enum, @TypeOf(undef.named.@"enum")); 1377 | try expectEqual([]const u8, @TypeOf(undef.named.string)); 1378 | try expectEqual(f32, @TypeOf(undef.named.f32)); 1379 | try expectEqual(u8, @TypeOf(undef.positional.U8)); 1380 | try expectEqual([]const u8, @TypeOf(undef.positional.STRING)); 1381 | try expectEqual(Enum, @TypeOf(undef.positional.ENUM)); 1382 | try expectEqual(f32, @TypeOf(undef.positional.F32)); 1383 | 1384 | // All set 1385 | try expectEqual(Result{ 1386 | .program_name = "path", 1387 | .named = .{ 1388 | .bool = true, 1389 | .u32 = 123, 1390 | .@"enum" = .bar, 1391 | .string = "world", 1392 | .f32 = 1.5, 1393 | }, 1394 | .positional = .{ 1395 | .U8 = 1, 1396 | .STRING = "hello", 1397 | .ENUM = .foo, 1398 | .F32 = 2.5, 1399 | }, 1400 | .subcommand = null, 1401 | }, try options.parseFromSlice(std.testing.allocator, &.{ 1402 | "path", 1403 | 1404 | "--u32", 1405 | "123", 1406 | "--enum", 1407 | "bar", 1408 | "--string", 1409 | "world", 1410 | "--bool", 1411 | "--f32", 1412 | "1.5", 1413 | 1414 | "1", 1415 | "hello", 1416 | "foo", 1417 | "2.5", 1418 | })); 1419 | 1420 | // Repeated args 1421 | try expectEqual(Result{ 1422 | .program_name = "path", 1423 | .named = .{ 1424 | .bool = false, 1425 | .u32 = 321, 1426 | .@"enum" = .foo, 1427 | .string = "updated", 1428 | .f32 = 3.5, 1429 | }, 1430 | .positional = .{ 1431 | .U8 = 1, 1432 | .STRING = "hello", 1433 | .ENUM = .foo, 1434 | .F32 = 2.5, 1435 | }, 1436 | .subcommand = null, 1437 | }, try options.parseFromSlice(std.testing.allocator, &.{ 1438 | "path", 1439 | 1440 | "--u32", 1441 | "123", 1442 | "--enum", 1443 | "bar", 1444 | "--string", 1445 | "world", 1446 | "--bool", 1447 | "--f32", 1448 | "1.5", 1449 | 1450 | "--u32", 1451 | "321", 1452 | "--enum", 1453 | "foo", 1454 | "--string", 1455 | "updated", 1456 | "--no-bool", 1457 | "--f32", 1458 | "3.5", 1459 | 1460 | "1", 1461 | "hello", 1462 | "foo", 1463 | "2.5", 1464 | })); 1465 | 1466 | // Short names args 1467 | try expectEqual(Result{ 1468 | .program_name = "path", 1469 | .named = .{ 1470 | .bool = true, 1471 | .u32 = 321, 1472 | .@"enum" = .foo, 1473 | .string = "updated", 1474 | .f32 = 1.5, 1475 | }, 1476 | .positional = .{ 1477 | .U8 = 1, 1478 | .STRING = "hello", 1479 | .ENUM = .foo, 1480 | .F32 = 2.5, 1481 | }, 1482 | .subcommand = null, 1483 | }, try options.parseFromSlice(std.testing.allocator, &.{ 1484 | "path", 1485 | 1486 | "--u", 1487 | "321", 1488 | "-e", 1489 | "foo", 1490 | "-s", 1491 | "updated", 1492 | "-b", 1493 | "-f", 1494 | "1.5", 1495 | 1496 | "1", 1497 | "hello", 1498 | "foo", 1499 | "2.5", 1500 | })); 1501 | } 1502 | 1503 | test "no args" { 1504 | const options: Command = .{ .name = "command-name" }; 1505 | const Result = options.Result(); 1506 | try expectEqual(0, std.meta.fields(@FieldType(Result, "named")).len); 1507 | try expectEqual(0, std.meta.fields(@FieldType(Result, "positional")).len); 1508 | try expect(@FieldType(Result, "subcommand") == ?void); 1509 | try expectEqual( 1510 | Result{ 1511 | .program_name = "path", 1512 | .named = .{}, 1513 | .positional = .{}, 1514 | .subcommand = null, 1515 | }, 1516 | try options.parseFromSlice(std.testing.allocator, &.{"path"}), 1517 | ); 1518 | } 1519 | 1520 | test "only positional" { 1521 | const Enum = enum { foo, bar }; 1522 | const options: Command = .{ 1523 | .name = "command-name", 1524 | .positional_args = &.{ 1525 | PositionalArg.init(u8, .{ 1526 | .meta = "U8", 1527 | }), 1528 | PositionalArg.init([]const u8, .{ 1529 | .meta = "STRING", 1530 | }), 1531 | PositionalArg.init(Enum, .{ 1532 | .meta = "ENUM", 1533 | }), 1534 | PositionalArg.init(f32, .{ 1535 | .meta = "F32", 1536 | }), 1537 | }, 1538 | }; 1539 | 1540 | const Result = options.Result(); 1541 | const undef: Result = undefined; 1542 | try expectEqual(0, std.meta.fields(@FieldType(Result, "named")).len); 1543 | try expectEqual(4, std.meta.fields(@FieldType(Result, "positional")).len); 1544 | try expect(@FieldType(Result, "subcommand") == ?void); 1545 | try expectEqual(u8, @TypeOf(undef.positional.U8)); 1546 | try expectEqual([]const u8, @TypeOf(undef.positional.STRING)); 1547 | try expectEqual(Enum, @TypeOf(undef.positional.ENUM)); 1548 | try expectEqual(f32, @TypeOf(undef.positional.F32)); 1549 | 1550 | // All set 1551 | try expectEqual(Result{ 1552 | .program_name = "path", 1553 | .named = .{}, 1554 | .positional = .{ 1555 | .U8 = 1, 1556 | .STRING = "hello", 1557 | .ENUM = .foo, 1558 | .F32 = 1.5, 1559 | }, 1560 | .subcommand = null, 1561 | }, try options.parseFromSlice(std.testing.allocator, &.{ 1562 | "path", 1563 | 1564 | "1", 1565 | "hello", 1566 | "foo", 1567 | "1.5", 1568 | })); 1569 | } 1570 | 1571 | test "only named" { 1572 | const Enum = enum { foo, bar }; 1573 | const options: Command = .{ 1574 | .name = "command-name", 1575 | .named_args = &.{ 1576 | NamedArg.init(bool, .{ 1577 | .long = "bool", 1578 | }), 1579 | NamedArg.init(?u32, .{ 1580 | .long = "u32", 1581 | }), 1582 | NamedArg.init(?Enum, .{ 1583 | .long = "enum", 1584 | }), 1585 | NamedArg.init(?[]const u8, .{ 1586 | .long = "string", 1587 | }), 1588 | NamedArg.init(?f32, .{ 1589 | .long = "f32", 1590 | }), 1591 | }, 1592 | }; 1593 | 1594 | const Result = options.Result(); 1595 | const undef: Result = undefined; 1596 | try expectEqual(5, std.meta.fields(@FieldType(Result, "named")).len); 1597 | try expectEqual(0, std.meta.fields(@FieldType(Result, "positional")).len); 1598 | try expect(@FieldType(Result, "subcommand") == ?void); 1599 | try expectEqual(bool, @TypeOf(undef.named.bool)); 1600 | try expectEqual(?u32, @TypeOf(undef.named.u32)); 1601 | try expectEqual(?Enum, @TypeOf(undef.named.@"enum")); 1602 | try expectEqual(?[]const u8, @TypeOf(undef.named.string)); 1603 | try expectEqual(?f32, @TypeOf(undef.named.f32)); 1604 | 1605 | // All set 1606 | try expectEqual(Result{ 1607 | .program_name = "path", 1608 | .named = .{ 1609 | .bool = true, 1610 | .u32 = 123, 1611 | .@"enum" = .bar, 1612 | .string = "world", 1613 | .f32 = 1.5, 1614 | }, 1615 | .positional = .{}, 1616 | .subcommand = null, 1617 | }, try options.parseFromSlice(std.testing.allocator, &.{ 1618 | "path", 1619 | 1620 | "--u32", 1621 | "123", 1622 | "--enum", 1623 | "bar", 1624 | "--string", 1625 | "world", 1626 | "--bool", 1627 | "--f32", 1628 | "1.5", 1629 | })); 1630 | } 1631 | 1632 | test "help menu" { 1633 | const Enum = enum { foo, bar }; 1634 | const no_help: Command = .{ 1635 | .name = "command-name", 1636 | .named_args = &.{ 1637 | NamedArg.init(bool, .{ 1638 | .long = "bool", 1639 | }), 1640 | NamedArg.init(?u32, .{ 1641 | .long = "u32", 1642 | }), 1643 | NamedArg.init(?Enum, .{ 1644 | .long = "enum", 1645 | }), 1646 | NamedArg.init(?[]const u8, .{ 1647 | .long = "string", 1648 | }), 1649 | NamedArg.init(?f32, .{ 1650 | .long = "float", 1651 | }), 1652 | NamedArg.initAccum([]const u8, .{ 1653 | .long = "list", 1654 | }), 1655 | }, 1656 | .positional_args = &.{ 1657 | PositionalArg.init(u8, .{ 1658 | .meta = "U8", 1659 | }), 1660 | PositionalArg.init([]const u8, .{ 1661 | .meta = "STRING", 1662 | }), 1663 | PositionalArg.init(Enum, .{ 1664 | .meta = "ENUM", 1665 | }), 1666 | PositionalArg.init(f32, .{ 1667 | .meta = "F32", 1668 | }), 1669 | }, 1670 | }; 1671 | 1672 | const with_help: Command = .{ 1673 | .name = "command-name", 1674 | .description = "command help", 1675 | .named_args = &.{ 1676 | NamedArg.init(bool, .{ 1677 | .long = "bool", 1678 | .description = "bool help", 1679 | }), 1680 | NamedArg.init(?u32, .{ 1681 | .long = "u32", 1682 | .description = "u32 help", 1683 | }), 1684 | NamedArg.init(?Enum, .{ 1685 | .long = "enum", 1686 | .description = "enum help", 1687 | }), 1688 | NamedArg.init(?[]const u8, .{ 1689 | .long = "string", 1690 | .description = "string help", 1691 | }), 1692 | NamedArg.init(?f32, .{ 1693 | .long = "float", 1694 | .description = "float help", 1695 | }), 1696 | NamedArg.initAccum([]const u8, .{ 1697 | .long = "list", 1698 | .description = "string list help", 1699 | }), 1700 | }, 1701 | .positional_args = &.{ 1702 | PositionalArg.init(u8, .{ 1703 | .meta = "U8", 1704 | .description = "u8 help", 1705 | }), 1706 | PositionalArg.init([]const u8, .{ 1707 | .meta = "STRING", 1708 | .description = "string help", 1709 | }), 1710 | PositionalArg.init(Enum, .{ 1711 | .meta = "ENUM", 1712 | .description = "enum help", 1713 | }), 1714 | PositionalArg.init(f32, .{ 1715 | .meta = "F32", 1716 | .description = "f32 help", 1717 | }), 1718 | }, 1719 | }; 1720 | 1721 | const with_help_with_defaults_optional: Command = .{ 1722 | .name = "command-name", 1723 | .description = "command help", 1724 | .named_args = &.{ 1725 | NamedArg.init(bool, .{ 1726 | .long = "bool", 1727 | .description = "bool help", 1728 | .default = .{ .value = true }, 1729 | }), 1730 | NamedArg.init(?u32, .{ 1731 | .long = "u32", 1732 | .description = "u32 help", 1733 | .default = .{ .value = 10 }, 1734 | }), 1735 | NamedArg.init(?Enum, .{ 1736 | .long = "enum", 1737 | .description = "enum help", 1738 | .default = .{ .value = .foo }, 1739 | }), 1740 | NamedArg.init(?[]const u8, .{ 1741 | .long = "string", 1742 | .description = "string help", 1743 | .default = .{ .value = "foo" }, 1744 | }), 1745 | NamedArg.init(?f32, .{ 1746 | .long = "float", 1747 | .description = "float help", 1748 | .default = .{ .value = 1.5 }, 1749 | }), 1750 | NamedArg.initAccum([]const u8, .{ 1751 | .long = "list", 1752 | .description = "string list help", 1753 | }), 1754 | }, 1755 | .positional_args = &.{ 1756 | PositionalArg.init(u8, .{ 1757 | .meta = "U8", 1758 | .description = "u8 help", 1759 | }), 1760 | PositionalArg.init([]const u8, .{ 1761 | .meta = "STRING", 1762 | .description = "string help", 1763 | }), 1764 | PositionalArg.init(Enum, .{ 1765 | .meta = "ENUM", 1766 | .description = "enum help", 1767 | }), 1768 | PositionalArg.init(f32, .{ 1769 | .meta = "F32", 1770 | .description = "f32 help", 1771 | }), 1772 | }, 1773 | }; 1774 | 1775 | const with_help_with_defaults_optional_null: Command = .{ 1776 | .name = "command-name", 1777 | .description = "command help", 1778 | .named_args = &.{ 1779 | NamedArg.init(bool, .{ 1780 | .long = "bool", 1781 | .description = "bool help", 1782 | .default = .{ .value = true }, 1783 | }), 1784 | NamedArg.init(?u32, .{ 1785 | .long = "u32", 1786 | .description = "u32 help", 1787 | .default = .{ .value = null }, 1788 | }), 1789 | NamedArg.init(?Enum, .{ 1790 | .long = "enum", 1791 | .description = "enum help", 1792 | .default = .{ .value = null }, 1793 | }), 1794 | NamedArg.init(?[]const u8, .{ 1795 | .long = "string", 1796 | .description = "string help", 1797 | .default = .{ .value = null }, 1798 | }), 1799 | NamedArg.init(?f32, .{ 1800 | .long = "float", 1801 | .description = "float help", 1802 | .default = .{ .value = null }, 1803 | }), 1804 | NamedArg.initAccum([]const u8, .{ 1805 | .long = "list", 1806 | .description = "string list help", 1807 | }), 1808 | }, 1809 | .positional_args = &.{ 1810 | PositionalArg.init(u8, .{ 1811 | .meta = "U8", 1812 | .description = "u8 help", 1813 | }), 1814 | PositionalArg.init([]const u8, .{ 1815 | .meta = "STRING", 1816 | .description = "string help", 1817 | }), 1818 | PositionalArg.init(Enum, .{ 1819 | .meta = "ENUM", 1820 | .description = "enum help", 1821 | }), 1822 | PositionalArg.init(f32, .{ 1823 | .meta = "F32", 1824 | .description = "f32 help", 1825 | }), 1826 | }, 1827 | }; 1828 | 1829 | const with_help_with_defaults: Command = .{ 1830 | .name = "command-name", 1831 | .description = "command help", 1832 | .named_args = &.{ 1833 | NamedArg.init(bool, .{ 1834 | .long = "bool", 1835 | .description = "bool help", 1836 | .default = .{ .value = true }, 1837 | }), 1838 | NamedArg.init(?u32, .{ 1839 | .long = "u32", 1840 | .description = "u32 help", 1841 | .default = .{ .value = 10 }, 1842 | }), 1843 | NamedArg.init(?Enum, .{ 1844 | .long = "enum", 1845 | .description = "enum help", 1846 | .default = .{ .value = .foo }, 1847 | }), 1848 | NamedArg.init(?[]const u8, .{ 1849 | .long = "string", 1850 | .description = "string help", 1851 | .default = .{ .value = "foo" }, 1852 | }), 1853 | NamedArg.init(?f32, .{ 1854 | .long = "float", 1855 | .description = "float help", 1856 | .default = .{ .value = 1.5 }, 1857 | }), 1858 | NamedArg.initAccum([]const u8, .{ 1859 | .long = "list", 1860 | .description = "string list help", 1861 | }), 1862 | }, 1863 | .positional_args = &.{ 1864 | PositionalArg.init(u8, .{ 1865 | .meta = "U8", 1866 | .description = "u8 help", 1867 | }), 1868 | PositionalArg.init([]const u8, .{ 1869 | .meta = "STRING", 1870 | .description = "string help", 1871 | }), 1872 | PositionalArg.init(Enum, .{ 1873 | .meta = "ENUM", 1874 | .description = "enum help", 1875 | }), 1876 | PositionalArg.init(f32, .{ 1877 | .meta = "F32", 1878 | .description = "f32 help", 1879 | }), 1880 | }, 1881 | }; 1882 | 1883 | const with_subcommand: Command = .{ 1884 | .name = "command-name", 1885 | .description = "command help", 1886 | .named_args = &.{ 1887 | NamedArg.init(bool, .{ 1888 | .long = "bool", 1889 | .description = "bool help", 1890 | .default = .{ .value = true }, 1891 | }), 1892 | }, 1893 | .positional_args = &.{ 1894 | PositionalArg.init(u8, .{ 1895 | .meta = "U8", 1896 | .description = "u8 help", 1897 | }), 1898 | }, 1899 | .subcommands = &.{.{ 1900 | .name = "subcommand", 1901 | .description = "subcommand help", 1902 | .named_args = &.{ 1903 | NamedArg.init(bool, .{ 1904 | .long = "bool2", 1905 | .description = "bool2 help", 1906 | .default = .{ .value = true }, 1907 | }), 1908 | }, 1909 | }}, 1910 | }; 1911 | 1912 | { 1913 | const found = try std.fmt.allocPrint( 1914 | std.testing.allocator, 1915 | "{}", 1916 | .{no_help.fmtUsage(true)}, 1917 | ); 1918 | defer std.testing.allocator.free(found); 1919 | try expectEqualStrings( 1920 | \\usage: command-name 1921 | \\--help for more info 1922 | , found); 1923 | } 1924 | 1925 | { 1926 | const found = try std.fmt.allocPrint( 1927 | std.testing.allocator, 1928 | "{}", 1929 | .{with_help.fmtUsage(true)}, 1930 | ); 1931 | defer std.testing.allocator.free(found); 1932 | try expectEqualStrings( 1933 | \\usage: command-name 1934 | \\--help for more info 1935 | , found); 1936 | } 1937 | 1938 | { 1939 | const found = try std.fmt.allocPrint( 1940 | std.testing.allocator, 1941 | "{}", 1942 | .{no_help.fmtUsage(false)}, 1943 | ); 1944 | defer std.testing.allocator.free(found); 1945 | try expectEqualStrings( 1946 | \\usage: command-name 1947 | \\ 1948 | \\options: 1949 | \\ --bool 1950 | \\ --no-bool 1951 | \\ --u32 1952 | \\ --no-u32 1953 | \\ --enum ? 1954 | \\ foo 1955 | \\ bar 1956 | \\ --no-enum 1957 | \\ --string 1958 | \\ --no-string 1959 | \\ --float 1960 | \\ --no-float 1961 | \\ --list (accum) 1962 | \\ --no-list 1963 | \\ 1964 | \\positional arguments: 1965 | \\ U8 1966 | \\ STRING 1967 | \\ ENUM 1968 | \\ foo 1969 | \\ bar 1970 | \\ F32 1971 | \\ 1972 | , found); 1973 | } 1974 | 1975 | { 1976 | const found = try std.fmt.allocPrint( 1977 | std.testing.allocator, 1978 | "{}", 1979 | .{with_help.fmtUsage(false)}, 1980 | ); 1981 | defer std.testing.allocator.free(found); 1982 | try expectEqualStrings( 1983 | \\usage: command-name 1984 | \\ 1985 | \\command help 1986 | \\ 1987 | \\options: 1988 | \\ --bool bool help 1989 | \\ --no-bool 1990 | \\ --u32 u32 help 1991 | \\ --no-u32 1992 | \\ --enum ? enum help 1993 | \\ foo 1994 | \\ bar 1995 | \\ --no-enum 1996 | \\ --string string help 1997 | \\ --no-string 1998 | \\ --float float help 1999 | \\ --no-float 2000 | \\ --list (accum) string list help 2001 | \\ --no-list 2002 | \\ 2003 | \\positional arguments: 2004 | \\ U8 u8 help 2005 | \\ STRING string help 2006 | \\ ENUM enum help 2007 | \\ foo 2008 | \\ bar 2009 | \\ F32 f32 help 2010 | \\ 2011 | , found); 2012 | } 2013 | 2014 | { 2015 | const found = try std.fmt.allocPrint( 2016 | std.testing.allocator, 2017 | "{}", 2018 | .{with_help_with_defaults_optional.fmtUsage(false)}, 2019 | ); 2020 | defer std.testing.allocator.free(found); 2021 | try expectEqualStrings( 2022 | \\usage: command-name 2023 | \\ 2024 | \\command help 2025 | \\ 2026 | \\options: 2027 | \\ --bool (=true) bool help 2028 | \\ --no-bool 2029 | \\ --u32 (=10) u32 help 2030 | \\ --no-u32 2031 | \\ --enum ? (=foo) enum help 2032 | \\ foo 2033 | \\ bar 2034 | \\ --no-enum 2035 | \\ --string (=foo) string help 2036 | \\ --no-string 2037 | \\ --float (=1.5) float help 2038 | \\ --no-float 2039 | \\ --list (accum) string list help 2040 | \\ --no-list 2041 | \\ 2042 | \\positional arguments: 2043 | \\ U8 u8 help 2044 | \\ STRING string help 2045 | \\ ENUM enum help 2046 | \\ foo 2047 | \\ bar 2048 | \\ F32 f32 help 2049 | \\ 2050 | , found); 2051 | } 2052 | 2053 | { 2054 | const found = try std.fmt.allocPrint( 2055 | std.testing.allocator, 2056 | "{}", 2057 | .{with_help_with_defaults_optional_null.fmtUsage(false)}, 2058 | ); 2059 | defer std.testing.allocator.free(found); 2060 | try expectEqualStrings( 2061 | \\usage: command-name 2062 | \\ 2063 | \\command help 2064 | \\ 2065 | \\options: 2066 | \\ --bool (=true) bool help 2067 | \\ --no-bool 2068 | \\ --u32 (=null) u32 help 2069 | \\ --no-u32 2070 | \\ --enum ? (=null) enum help 2071 | \\ foo 2072 | \\ bar 2073 | \\ --no-enum 2074 | \\ --string (=null) string help 2075 | \\ --no-string 2076 | \\ --float (=null) float help 2077 | \\ --no-float 2078 | \\ --list (accum) string list help 2079 | \\ --no-list 2080 | \\ 2081 | \\positional arguments: 2082 | \\ U8 u8 help 2083 | \\ STRING string help 2084 | \\ ENUM enum help 2085 | \\ foo 2086 | \\ bar 2087 | \\ F32 f32 help 2088 | \\ 2089 | , found); 2090 | } 2091 | 2092 | { 2093 | const found = try std.fmt.allocPrint( 2094 | std.testing.allocator, 2095 | "{}", 2096 | .{with_help_with_defaults.fmtUsage(false)}, 2097 | ); 2098 | defer std.testing.allocator.free(found); 2099 | try expectEqualStrings( 2100 | \\usage: command-name 2101 | \\ 2102 | \\command help 2103 | \\ 2104 | \\options: 2105 | \\ --bool (=true) bool help 2106 | \\ --no-bool 2107 | \\ --u32 (=10) u32 help 2108 | \\ --no-u32 2109 | \\ --enum ? (=foo) enum help 2110 | \\ foo 2111 | \\ bar 2112 | \\ --no-enum 2113 | \\ --string (=foo) string help 2114 | \\ --no-string 2115 | \\ --float (=1.5) float help 2116 | \\ --no-float 2117 | \\ --list (accum) string list help 2118 | \\ --no-list 2119 | \\ 2120 | \\positional arguments: 2121 | \\ U8 u8 help 2122 | \\ STRING string help 2123 | \\ ENUM enum help 2124 | \\ foo 2125 | \\ bar 2126 | \\ F32 f32 help 2127 | \\ 2128 | , found); 2129 | } 2130 | 2131 | { 2132 | const found = try std.fmt.allocPrint( 2133 | std.testing.allocator, 2134 | "{}", 2135 | .{with_subcommand.fmtUsage(false)}, 2136 | ); 2137 | defer std.testing.allocator.free(found); 2138 | try expectEqualStrings( 2139 | \\usage: command-name 2140 | \\ 2141 | \\command help 2142 | \\ 2143 | \\options: 2144 | \\ --bool (=true) bool help 2145 | \\ --no-bool 2146 | \\ 2147 | \\positional arguments: 2148 | \\ U8 u8 help 2149 | \\ 2150 | \\subcommands: 2151 | \\ subcommand subcommand help 2152 | \\ 2153 | , found); 2154 | } 2155 | } 2156 | 2157 | test "help argument" { 2158 | const options: Command = .{ 2159 | .name = "command-name", 2160 | .named_args = &.{ 2161 | NamedArg.init([]const u8, .{ 2162 | .long = "named-1", 2163 | }), 2164 | NamedArg.init([]const u8, .{ 2165 | .long = "named-2", 2166 | }), 2167 | }, 2168 | .positional_args = &.{ 2169 | PositionalArg.init([]const u8, .{ 2170 | .meta = "POS-1", 2171 | }), 2172 | PositionalArg.init([]const u8, .{ 2173 | .meta = "POS-2", 2174 | }), 2175 | }, 2176 | }; 2177 | 2178 | try expectError(error.Help, options.parseFromSlice(std.testing.allocator, &.{ 2179 | "path", 2180 | "--help", 2181 | })); 2182 | 2183 | try expectError(error.Help, options.parseFromSlice(std.testing.allocator, &.{ 2184 | "path", 2185 | "-h", 2186 | })); 2187 | 2188 | try expectError(error.Help, options.parseFromSlice(std.testing.allocator, &.{ 2189 | "path", 2190 | "--named-1", 2191 | "--help", 2192 | })); 2193 | 2194 | try expectError(error.Help, options.parseFromSlice(std.testing.allocator, &.{ 2195 | "path", 2196 | "--named-1", 2197 | "-h", 2198 | })); 2199 | 2200 | try expectError(error.Help, options.parseFromSlice(std.testing.allocator, &.{ 2201 | "path", 2202 | "--named-1", 2203 | "foo", 2204 | "--named-2", 2205 | "--help", 2206 | })); 2207 | 2208 | try expectError(error.Help, options.parseFromSlice(std.testing.allocator, &.{ 2209 | "path", 2210 | "--named-1", 2211 | "foo", 2212 | "--named-2", 2213 | "-h", 2214 | })); 2215 | 2216 | try expectError(error.Help, options.parseFromSlice(std.testing.allocator, &.{ 2217 | "path", 2218 | "--named-1", 2219 | "foo", 2220 | "--named-2", 2221 | "bar", 2222 | "--help", 2223 | })); 2224 | 2225 | try expectError(error.Help, options.parseFromSlice(std.testing.allocator, &.{ 2226 | "path", 2227 | "--named-1", 2228 | "foo", 2229 | "--named-2", 2230 | "bar", 2231 | "-h", 2232 | })); 2233 | 2234 | try expectError(error.Help, options.parseFromSlice(std.testing.allocator, &.{ 2235 | "path", 2236 | "--named-1", 2237 | "foo", 2238 | "--named-2", 2239 | "bar", 2240 | "baz", 2241 | "--help", 2242 | })); 2243 | 2244 | try expectError(error.Help, options.parseFromSlice(std.testing.allocator, &.{ 2245 | "path", 2246 | "--named-1", 2247 | "foo", 2248 | "--named-2", 2249 | "bar", 2250 | "baz", 2251 | "-h", 2252 | })); 2253 | 2254 | try expectError(error.Help, options.parseFromSlice(std.testing.allocator, &.{ 2255 | "path", 2256 | "--named-1", 2257 | "foo", 2258 | "--named-2", 2259 | "bar", 2260 | "baz", 2261 | "qux", 2262 | "--help", 2263 | })); 2264 | 2265 | try expectError(error.Help, options.parseFromSlice(std.testing.allocator, &.{ 2266 | "path", 2267 | "--named-1", 2268 | "foo", 2269 | "--named-2", 2270 | "bar", 2271 | "baz", 2272 | "qux", 2273 | "-h", 2274 | })); 2275 | } 2276 | 2277 | test "default field values" { 2278 | const options: Command = .{ 2279 | .name = "command-name", 2280 | .named_args = &.{ 2281 | NamedArg.init(?u8, .{ 2282 | .long = "named-1", 2283 | .default = .{ .value = null }, 2284 | }), 2285 | NamedArg.init(?u8, .{ 2286 | .long = "named-2", 2287 | .default = .{ .value = 10 }, 2288 | }), 2289 | NamedArg.init(?u8, .{ 2290 | .long = "named-3", 2291 | .default = .required, 2292 | }), 2293 | NamedArg.init(u8, .{ 2294 | .long = "named-4", 2295 | .default = .{ .value = 10 }, 2296 | }), 2297 | NamedArg.init(?u8, .{ 2298 | .long = "named-5", 2299 | .default = .required, 2300 | }), 2301 | }, 2302 | .positional_args = &.{ 2303 | PositionalArg.init(u8, .{ 2304 | .meta = "POS-1", 2305 | }), 2306 | }, 2307 | }; 2308 | const Result = options.Result(); 2309 | const Named = @FieldType(Result, "named"); 2310 | const Positional = @FieldType(Result, "positional"); 2311 | try expectEqual( 2312 | null, 2313 | @as(*const ?u8, @ptrCast(std.meta.fieldInfo(Named, .@"named-1").default_value_ptr.?)).*, 2314 | ); 2315 | try expectEqual( 2316 | 10, 2317 | @as(*const ?u8, @ptrCast(std.meta.fieldInfo(Named, .@"named-2").default_value_ptr.?)).*.?, 2318 | ); 2319 | try expectEqual(null, std.meta.fieldInfo(Named, .@"named-3").default_value_ptr); 2320 | try expectEqual( 2321 | 10, 2322 | @as(*const u8, @ptrCast(std.meta.fieldInfo(Named, .@"named-4").default_value_ptr.?)).*, 2323 | ); 2324 | try expectEqual(null, std.meta.fieldInfo(Named, .@"named-5").default_value_ptr); 2325 | try expectEqual(null, std.meta.fieldInfo(Positional, .@"POS-1").default_value_ptr); 2326 | } 2327 | 2328 | test "lists" { 2329 | const options: Command = .{ 2330 | .name = "command-name", 2331 | .named_args = &.{ 2332 | NamedArg.initAccum([]const u8, .{ 2333 | .long = "list", 2334 | }), 2335 | }, 2336 | }; 2337 | 2338 | { 2339 | const result = try options.parseFromSlice(std.testing.allocator, &.{ 2340 | "path", 2341 | }); 2342 | defer options.parseFree(result); 2343 | try expectEqualSlices([]const u8, &.{}, result.named.list.items); 2344 | } 2345 | 2346 | { 2347 | const result = try options.parseFromSlice(std.testing.allocator, &.{ 2348 | "path", 2349 | 2350 | "--list", 2351 | "foo", 2352 | "--list", 2353 | "bar", 2354 | }); 2355 | defer options.parseFree(result); 2356 | try expectEqualSlices([]const u8, &.{ "foo", "bar" }, result.named.list.items); 2357 | } 2358 | 2359 | { 2360 | const result = try options.parseFromSlice(std.testing.allocator, &.{ 2361 | "path", 2362 | 2363 | "--list", 2364 | "foo", 2365 | "--list", 2366 | "bar", 2367 | 2368 | "--no-list", 2369 | }); 2370 | defer options.parseFree(result); 2371 | try expectEqualSlices([]const u8, &.{}, result.named.list.items); 2372 | } 2373 | 2374 | { 2375 | const result = try options.parseFromSlice(std.testing.allocator, &.{ 2376 | "path", 2377 | 2378 | "--list", 2379 | "foo", 2380 | "--list", 2381 | "bar", 2382 | 2383 | "--no-list", 2384 | 2385 | "--list", 2386 | "baz", 2387 | }); 2388 | defer options.parseFree(result); 2389 | try expectEqualSlices([]const u8, &.{"baz"}, result.named.list.items); 2390 | } 2391 | } 2392 | 2393 | test "null terminated strings" { 2394 | const options: Command = .{ 2395 | .name = "command-name", 2396 | .named_args = &.{ 2397 | NamedArg.init([:0]const u8, .{ 2398 | .long = "foo", 2399 | }), 2400 | }, 2401 | .positional_args = &.{ 2402 | PositionalArg.init([:0]const u8, .{ 2403 | .meta = "BAR", 2404 | }), 2405 | }, 2406 | }; 2407 | { 2408 | const result = try options.parseFromSlice(std.testing.allocator, &.{ 2409 | "command-name", 2410 | "--foo", 2411 | "foo-value", 2412 | "bar-value", 2413 | }); 2414 | defer options.parseFree(result); 2415 | try expectEqualStrings("foo-value", result.named.foo); 2416 | try expectEqualStrings("bar-value", result.positional.BAR); 2417 | } 2418 | } 2419 | 2420 | test "subcommands" { 2421 | const options: Command = .{ 2422 | .name = "command-name", 2423 | .named_args = &.{ 2424 | NamedArg.init([]const u8, .{ 2425 | .long = "foo", 2426 | }), 2427 | }, 2428 | .positional_args = &.{ 2429 | PositionalArg.init(u8, .{ 2430 | .meta = "BAR", 2431 | }), 2432 | }, 2433 | .subcommands = &.{ 2434 | .{ 2435 | .name = "sub1", 2436 | .named_args = &.{ 2437 | NamedArg.init([]const u8, .{ 2438 | .long = "sub1a", 2439 | }), 2440 | }, 2441 | .positional_args = &.{ 2442 | PositionalArg.init(u8, .{ 2443 | .meta = "SUB1B", 2444 | }), 2445 | }, 2446 | }, 2447 | .{ 2448 | .name = "sub2", 2449 | .named_args = &.{ 2450 | NamedArg.init([]const u8, .{ 2451 | .long = "sub2a", 2452 | }), 2453 | }, 2454 | .positional_args = &.{}, 2455 | }, 2456 | }, 2457 | }; 2458 | 2459 | const Result = options.Result(); 2460 | const undef: Result = undefined; 2461 | try expectEqual(1, std.meta.fields(@FieldType(Result, "named")).len); 2462 | try expectEqual(1, std.meta.fields(@FieldType(Result, "positional")).len); 2463 | try expectEqual( 2464 | 2, 2465 | std.meta.fields(@typeInfo(@FieldType(Result, "subcommand")).optional.child).len, 2466 | ); 2467 | try expectEqual([]const u8, @TypeOf(undef.named.foo)); 2468 | try expectEqual(u8, @TypeOf(undef.positional.BAR)); 2469 | 2470 | const Sub1 = options.subcommands[0].Result(); 2471 | const undef1: Sub1 = undefined; 2472 | try expectEqual(1, std.meta.fields(@FieldType(Sub1, "named")).len); 2473 | try expectEqual(1, std.meta.fields(@FieldType(Sub1, "positional")).len); 2474 | try expect(@FieldType(Sub1, "subcommand") == ?void); 2475 | try expectEqual([]const u8, @TypeOf(undef1.named.sub1a)); 2476 | try expectEqual(u8, @TypeOf(undef1.positional.SUB1B)); 2477 | 2478 | const Sub2 = options.subcommands[1].Result(); 2479 | const undef2: Sub2 = undefined; 2480 | try expectEqual(1, std.meta.fields(@FieldType(Sub2, "named")).len); 2481 | try expectEqual(0, std.meta.fields(@FieldType(Sub2, "positional")).len); 2482 | try expect(@FieldType(Sub2, "subcommand") == ?void); 2483 | try expectEqual([]const u8, @TypeOf(undef2.named.sub2a)); 2484 | 2485 | // No subcommand 2486 | try expectEqual(Result{ 2487 | .program_name = "path", 2488 | .named = .{ 2489 | .foo = "foo", 2490 | }, 2491 | .positional = .{ 2492 | .BAR = 10, 2493 | }, 2494 | .subcommand = null, 2495 | }, try options.parseFromSlice(std.testing.allocator, &.{ 2496 | "path", 2497 | 2498 | "--foo", 2499 | "foo", 2500 | "10", 2501 | })); 2502 | 2503 | // First subcommand 2504 | try expectEqual(Result{ 2505 | .program_name = "path", 2506 | .named = .{ 2507 | .foo = "foo", 2508 | }, 2509 | .positional = .{ 2510 | .BAR = 10, 2511 | }, 2512 | .subcommand = .{ 2513 | .sub1 = .{ 2514 | .named = .{ 2515 | .sub1a = "nested", 2516 | }, 2517 | .positional = .{ 2518 | .SUB1B = 24, 2519 | }, 2520 | .subcommand = null, 2521 | }, 2522 | }, 2523 | }, try options.parseFromSlice(std.testing.allocator, &.{ 2524 | "path", 2525 | 2526 | "--foo", 2527 | "foo", 2528 | "10", 2529 | 2530 | "sub1", 2531 | "--sub1a", 2532 | "nested", 2533 | "24", 2534 | })); 2535 | 2536 | // Second subcommand 2537 | try expectEqual(Result{ 2538 | .program_name = "path", 2539 | .named = .{ 2540 | .foo = "foo", 2541 | }, 2542 | .positional = .{ 2543 | .BAR = 10, 2544 | }, 2545 | .subcommand = .{ 2546 | .sub2 = .{ 2547 | .named = .{ 2548 | .sub2a = "nested2", 2549 | }, 2550 | .positional = .{}, 2551 | .subcommand = null, 2552 | }, 2553 | }, 2554 | }, try options.parseFromSlice(std.testing.allocator, &.{ 2555 | "path", 2556 | 2557 | "--foo", 2558 | "foo", 2559 | "10", 2560 | 2561 | "sub2", 2562 | "--sub2a", 2563 | "nested2", 2564 | })); 2565 | } 2566 | --------------------------------------------------------------------------------