| ... | ... | @@ -11,8 +11,8 @@ const mem = std.mem; |
| 11 | 11 | const Allocator = mem.Allocator; |
| 12 | 12 | |
| 13 | 13 | pub const Options = struct { |
| 14 | | /// When returning error.Usage, print a short error message to this writer, defaults to stderr. |
| 15 | | /// When returning error.Help, print the long help documentation to this writer, defaults to stdout. |
| 14 | /// Parsing/validation errors and the long `--help` documentation will be written to this writer. |
| 15 | /// By default, parsing/validation errors are written to stderr, and the long `--help` documentation is written to stdout. |
| 16 | 16 | /// Any error while writing is silently ignored. |
| 17 | 17 | writer: ?*Writer = null, |
| 18 | 18 | |
| ... | ... | @@ -20,6 +20,10 @@ pub const Options = struct { |
| 20 | 20 | /// By default uses the last path component of the process's first argument (`argv[0]`). |
| 21 | 21 | /// When there is no `argv[0]` (such as with `parseSlice`), the default is `"<prog>"`. |
| 22 | 22 | prog: ?[]const u8 = null, |
| 23 | |
| 24 | /// Call `std.process.exit` with an error status instead of returning `error.Usage` or `error.Help`. |
| 25 | /// The default is `true` for `parse` and `@"error"`, and `false` otherwise. |
| 26 | exit: ?bool = null, |
| 23 | 27 | }; |
| 24 | 28 | |
| 25 | 29 | pub const Error = error{ |
| ... | ... | @@ -56,7 +60,7 @@ pub const Error = error{ |
| 56 | 60 | /// <other> (7) |
| 57 | 61 | /// ``` |
| 58 | 62 | /// Forms (1), (2), and (3) must correspond to a field `Args.named.<name>`; see below for named argument handling. |
| 59 | | /// Form (4) immediately prints the long help documentation and returns `error.Help`. |
| 63 | /// Form (4) immediately prints the long help documentation and exits or returns `error.Help` depending on options.exit. |
| 60 | 64 | /// Form (6) signals that all following arg strings are positional. |
| 61 | 65 | /// Form (7) and all arg strings following form (6) are appended into the `positional` array in order. |
| 62 | 66 | /// |
| ... | ... | @@ -107,12 +111,20 @@ pub const Error = error{ |
| 107 | 111 | /// The first arg returned by the `ArgIterator` (`argv[0]`) is skipped by all the above parsing logic. |
| 108 | 112 | /// If `options.prog` is `null`, then the final path component of `argv[0]` is used by default. |
| 109 | 113 | /// |
| 114 | /// If a parsing/validation error occurs or the `--help` arg is given, |
| 115 | /// this function calls `std.process.exit` with an error status unless `options.exit` is set to `false`, |
| 116 | /// in which case parsing/validation errors return `error.Usage` and `--help` returns `error.Help`. |
| 117 | /// Allocator errors are always returned from the function. |
| 118 | /// |
| 110 | 119 | /// It is not possible to precisely deallocate the memory allocated by this function. |
| 111 | 120 | /// An `ArenaAllocator` is recommended to prevent memory leaks. |
| 112 | 121 | pub fn parse(comptime Args: type, arena: Allocator, options: Options) Error!Args { |
| 113 | 122 | var iter: ArgIterator = try .initWithAllocator(arena); |
| 114 | 123 | // Do not call iter.deinit(). It holds the string data returned in the Args. |
| 115 | | return parseIter(Args, arena, &iter, options); |
| 124 | |
| 125 | const argv0 = iter.next(); |
| 126 | const prog = options.prog orelse if (argv0) |arg| std.fs.path.basename(arg) else "<prog>"; |
| 127 | return innerParse(Args, arena, &iter, prog, options.writer, options.exit orelse true); |
| 116 | 128 | } |
| 117 | 129 | |
| 118 | 130 | test parse { |
| ... | ... | @@ -151,12 +163,18 @@ test parse { |
| 151 | 163 | /// The first string arg returned by the `iter` (`argv[0]`) is skipped by all the parsing logic. |
| 152 | 164 | /// If `options.prog` is `null`, then the final path component of `argv[0]` is used by default. |
| 153 | 165 | /// |
| 166 | /// If a parsing/validation error occurs or the `--help` arg is given, |
| 167 | /// this function returns `error.Usage` or `error.Help` respectively, |
| 168 | /// unless `options.exit` is set to `true`, in which case `std.process.exit` is called with an error status instead. |
| 169 | /// Allocator errors are always returned from the function. |
| 170 | /// |
| 154 | 171 | /// An `ArenaAllocator` is recommended to cleanup the memory allocated from this function; |
| 155 | 172 | /// however, it's also possible to free all the memory by freeing every slice field `[]const C` (other than `u8`) |
| 156 | 173 | /// in the returned `args.named` as well as freeing `args.positional`. |
| 157 | 174 | pub fn parseIter(comptime Args: type, arena: Allocator, iter: anytype, options: Options) Error!Args { |
| 158 | | const prog = options.prog orelse if (iter.next()) |arg0| std.fs.path.basename(arg0) else "<prog>"; |
| 159 | | return innerParse(Args, arena, iter, prog, options.writer); |
| 175 | const argv0 = iter.next(); |
| 176 | const prog = options.prog orelse if (argv0) |arg| std.fs.path.basename(arg) else "<prog>"; |
| 177 | return innerParse(Args, arena, iter, prog, options.writer, options.exit orelse false); |
| 160 | 178 | } |
| 161 | 179 | |
| 162 | 180 | /// Like `parse`, but takes a slice of strings in place of using an `ArgIterator`. |
| ... | ... | @@ -167,6 +185,11 @@ pub fn parseIter(comptime Args: type, arena: Allocator, iter: anytype, options: |
| 167 | 185 | /// Unlike `parse` and `parseIter`, this function does not skip the first item of `argv`. |
| 168 | 186 | /// Use `options.prog` instead. |
| 169 | 187 | /// |
| 188 | /// If a parsing/validation error occurs or the `--help` arg is given, |
| 189 | /// this function returns `error.Usage` or `error.Help` respectively, |
| 190 | /// unless `options.exit` is set to `true`, in which case `std.process.exit` is called with an error status instead. |
| 191 | /// Allocator errors are always returned from the function. |
| 192 | /// |
| 170 | 193 | /// An `ArenaAllocator` is recommended to cleanup the memory allocated from this function; |
| 171 | 194 | /// however, it's also possible to free all the memory by freeing every slice field `[]const C` (other than `u8`) |
| 172 | 195 | /// in the returned `args.named` as well as freeing `args.positional`. |
| ... | ... | @@ -179,7 +202,7 @@ pub fn parseSlice(comptime Args: type, arena: Allocator, argv: anytype, options: |
| 179 | 202 | else |
| 180 | 203 | @compileError("expected argv to be `*const [_]String` or `[]const String` where `String` is `[]const u8` or similar"); |
| 181 | 204 | var iter = ArgIteratorSlice(String){ .slice = argv }; |
| 182 | | return innerParse(Args, arena, &iter, options.prog orelse "<prog>", options.writer); |
| 205 | return innerParse(Args, arena, &iter, options.prog orelse "<prog>", options.writer, options.exit orelse false); |
| 183 | 206 | } |
| 184 | 207 | |
| 185 | 208 | test parseSlice { |
| ... | ... | @@ -219,8 +242,8 @@ test parseSlice { |
| 219 | 242 | }, args); |
| 220 | 243 | } |
| 221 | 244 | |
| 222 | | fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: []const u8, writer: ?*Writer) Error!Args { |
| 223 | | // arg0 has already been consumed. |
| 245 | fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: []const u8, writer: ?*Writer, exit_on_error: bool) Error!Args { |
| 246 | // argv0 has already been consumed. |
| 224 | 247 | |
| 225 | 248 | // Do all comptime checks up front so that we can be sure any compile error the user sees is the one we wrote. |
| 226 | 249 | comptime checkArgsType(Args); |
| ... | ... | @@ -272,13 +295,16 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: [] |
| 272 | 295 | } else { |
| 273 | 296 | printGeneratedHelp(writer, prog, named_info); |
| 274 | 297 | } |
| 298 | if (exit_on_error) { |
| 299 | std.process.exit(1); |
| 300 | } |
| 275 | 301 | return error.Help; |
| 276 | 302 | } |
| 277 | 303 | |
| 278 | 304 | if (arg.len >= 2 and arg[0] == '-' and isAlphabetic(arg[1])) { |
| 279 | 305 | // Always invalid. |
| 280 | 306 | // Examples: -h, -flag, -I/path |
| 281 | | return usageError(writer, "unrecognized argument: {s}", .{arg}); |
| 307 | return usageError(writer, "unrecognized argument: {s}", .{arg}, exit_on_error); |
| 282 | 308 | } |
| 283 | 309 | if (mem.eql(u8, arg, "--")) { |
| 284 | 310 | // Stop recognizing named arguments. Everything else is positional. |
| ... | ... | @@ -312,31 +338,31 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: [] |
| 312 | 338 | inline for (named_info.fields, 0..) |field, i| { |
| 313 | 339 | if (mem.eql(u8, field.name, arg_name)) { |
| 314 | 340 | if (field.type == bool) { |
| 315 | | if (immediate_value != null) return usageError(writer, "cannot specify value for bool argument: {s}", .{arg}); |
| 341 | if (immediate_value != null) return usageError(writer, "cannot specify value for bool argument: {s}", .{arg}, exit_on_error); |
| 316 | 342 | @field(result.named, field.name) = !no_prefixed; |
| 317 | 343 | fields_seen[i] = true; |
| 318 | 344 | break; |
| 319 | 345 | } |
| 320 | | if (no_prefixed) return usageError(writer, "unrecognized argument: {s}", .{arg}); |
| 346 | if (no_prefixed) return usageError(writer, "unrecognized argument: {s}", .{arg}, exit_on_error); |
| 321 | 347 | |
| 322 | 348 | // All other argument types require a value. |
| 323 | | const arg_value = immediate_value orelse iter.next() orelse return usageError(writer, "expected argument after --{s}", .{field.name}); |
| 349 | const arg_value = immediate_value orelse iter.next() orelse return usageError(writer, "expected argument after --{s}", .{field.name}, exit_on_error); |
| 324 | 350 | |
| 325 | 351 | switch (@typeInfo(field.type)) { |
| 326 | 352 | .bool => unreachable, // Handled above. |
| 327 | 353 | .float => { |
| 328 | 354 | @field(result.named, field.name) = std.fmt.parseFloat(field.type, arg_value) catch |err| { |
| 329 | | return usageError(writer, "unable to parse --{s}={s}: {s}", .{ field.name, arg_value, @errorName(err) }); |
| 355 | return usageError(writer, "unable to parse --{s}={s}: {s}", .{ field.name, arg_value, @errorName(err) }, exit_on_error); |
| 330 | 356 | }; |
| 331 | 357 | }, |
| 332 | 358 | .int => { |
| 333 | 359 | @field(result.named, field.name) = std.fmt.parseInt(field.type, arg_value, 0) catch |err| { |
| 334 | | return usageError(writer, "unable to parse --{s}={s}: {s}", .{ field.name, arg_value, @errorName(err) }); |
| 360 | return usageError(writer, "unable to parse --{s}={s}: {s}", .{ field.name, arg_value, @errorName(err) }, exit_on_error); |
| 335 | 361 | }; |
| 336 | 362 | }, |
| 337 | 363 | .@"enum" => { |
| 338 | 364 | @field(result.named, field.name) = std.meta.stringToEnum(field.type, arg_value) orelse { |
| 339 | | return usageError(writer, "unrecognized value: --{s}={s}, expected one of: {s}", .{ field.name, arg_value, enumValuesExpr(field.type) }); |
| 365 | return usageError(writer, "unrecognized value: --{s}={s}, expected one of: {s}", .{ field.name, arg_value, enumValuesExpr(field.type) }, exit_on_error); |
| 340 | 366 | }; |
| 341 | 367 | }, |
| 342 | 368 | .pointer => |ptrInfo| { |
| ... | ... | @@ -349,12 +375,12 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: [] |
| 349 | 375 | .bool => comptime unreachable, // Nicer compile error emitted in checkArgsType(). |
| 350 | 376 | .float => { |
| 351 | 377 | try array_list.append(allocator, std.fmt.parseFloat(ptrInfo.child, arg_value) catch |err| { |
| 352 | | return usageError(writer, "unable to parse --{s}={s}: {s}", .{ field.name, arg_value, @errorName(err) }); |
| 378 | return usageError(writer, "unable to parse --{s}={s}: {s}", .{ field.name, arg_value, @errorName(err) }, exit_on_error); |
| 353 | 379 | }); |
| 354 | 380 | }, |
| 355 | 381 | .int => { |
| 356 | 382 | try array_list.append(allocator, std.fmt.parseInt(ptrInfo.child, arg_value, 0) catch |err| { |
| 357 | | return usageError(writer, "unable to parse --{s}={s}: {s}", .{ field.name, arg_value, @errorName(err) }); |
| 383 | return usageError(writer, "unable to parse --{s}={s}: {s}", .{ field.name, arg_value, @errorName(err) }, exit_on_error); |
| 358 | 384 | }); |
| 359 | 385 | }, |
| 360 | 386 | .@"enum" => comptime unreachable, |
| ... | ... | @@ -376,7 +402,7 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: [] |
| 376 | 402 | } |
| 377 | 403 | } else { |
| 378 | 404 | // Didn't match anything. |
| 379 | | return usageError(writer, "unrecognized argument: {s}", .{arg}); |
| 405 | return usageError(writer, "unrecognized argument: {s}", .{arg}, exit_on_error); |
| 380 | 406 | } |
| 381 | 407 | } |
| 382 | 408 | |
| ... | ... | @@ -387,9 +413,9 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: [] |
| 387 | 413 | @field(result.named, field.name) = default; |
| 388 | 414 | } else { |
| 389 | 415 | if (field.type == bool) { |
| 390 | | return usageError(writer, "missing required argument: --" ++ field.name ++ " or --no-" ++ field.name, .{}); |
| 416 | return usageError(writer, "missing required argument: --" ++ field.name ++ " or --no-" ++ field.name, .{}, exit_on_error); |
| 391 | 417 | } else { |
| 392 | | return usageError(writer, "missing required argument: --" ++ field.name, .{}); |
| 418 | return usageError(writer, "missing required argument: --" ++ field.name, .{}, exit_on_error); |
| 393 | 419 | } |
| 394 | 420 | } |
| 395 | 421 | } |
| ... | ... | @@ -456,8 +482,11 @@ fn checkArgsType(comptime Args: type) void { |
| 456 | 482 | /// An error message will be written to `options.writer` or stderr by default, and `error.Usage` is returned. |
| 457 | 483 | /// The given `msg` template is prefixed by `"error: "` and suffixed by a newline and a prompt to try passing in `--help`. |
| 458 | 484 | /// `options.prog` is not used by this function, but could be in the future. |
| 485 | /// |
| 486 | /// This function calls `std.process.exit` with an error status unless `options.exit` is set to `false`, in which case it returns `error.Usage`. |
| 487 | /// This matches the default behavior of `parse`, not `parseIter` or `parseSlice`. |
| 459 | 488 | pub fn @"error"(comptime msg: []const u8, args: anytype, options: Options) error{Usage} { |
| 460 | | return usageError(options.writer, msg, args); |
| 489 | return usageError(options.writer, msg, args, options.exit orelse true); |
| 461 | 490 | } |
| 462 | 491 | |
| 463 | 492 | test @"error" { |
| ... | ... | @@ -472,15 +501,15 @@ test @"error" { |
| 472 | 501 | defer arena.deinit(); |
| 473 | 502 | const args = try parseSlice(Args, arena.allocator(), &[_][]const u8{ "--output=o.txt", "i.txt" }, .{}); |
| 474 | 503 | |
| 475 | | if (std.fs.path.isAbsolutePosix(args.named.output)) { |
| 476 | | return std.cli.@"error"("--output must not be absolute: {s}", .{args.named.output}, .{}); |
| 504 | if (std.fs.path.isAbsolute(args.named.output)) { |
| 505 | return std.cli.@"error"("--output must not be absolute: {s}", .{args.named.output}, .{ .exit_on_error = false }); |
| 477 | 506 | } |
| 478 | 507 | if (args.positional.len > 1) { |
| 479 | | return std.cli.@"error"("expected exactly 1 positional arg", .{}, .{}); |
| 508 | return std.cli.@"error"("expected exactly 1 positional arg", .{}, .{ .exit_on_error = false }); |
| 480 | 509 | } |
| 481 | 510 | } |
| 482 | 511 | |
| 483 | | fn usageError(writer: ?*Writer, comptime msg: []const u8, args: anytype) error{Usage} { |
| 512 | fn usageError(writer: ?*Writer, comptime msg: []const u8, args: anytype, exit_on_error: bool) error{Usage} { |
| 484 | 513 | const whole_msg = |
| 485 | 514 | "error: " ++ msg ++ "\n" ++ |
| 486 | 515 | \\try --help for full help info |
| ... | ... | @@ -491,6 +520,9 @@ fn usageError(writer: ?*Writer, comptime msg: []const u8, args: anytype) error{U |
| 491 | 520 | } else { |
| 492 | 521 | std.debug.print(whole_msg, args); |
| 493 | 522 | } |
| 523 | if (exit_on_error) { |
| 524 | std.process.exit(1); |
| 525 | } |
| 494 | 526 | return error.Usage; |
| 495 | 527 | } |
| 496 | 528 | |