| ... | @@ -11,8 +11,8 @@ const mem = std.mem; | ... | @@ -11,8 +11,8 @@ const mem = std.mem; |
| 11 | const Allocator = mem.Allocator; | 11 | const Allocator = mem.Allocator; |
| 12 | | 12 | |
| 13 | pub const Options = struct { | 13 | pub const Options = struct { |
| 14 | /// When returning error.Usage, print a short error message to this writer, defaults to stderr. | 14 | /// Parsing/validation errors and the long `--help` documentation will be written to this writer. |
| 15 | /// When returning error.Help, print the long help documentation to this writer, defaults to stdout. | 15 | /// By default, parsing/validation errors are written to stderr, and the long `--help` documentation is written to stdout. |
| 16 | /// Any error while writing is silently ignored. | 16 | /// Any error while writing is silently ignored. |
| 17 | writer: ?*Writer = null, | 17 | writer: ?*Writer = null, |
| 18 | | 18 | |
| ... | @@ -20,6 +20,10 @@ pub const Options = struct { | ... | @@ -20,6 +20,10 @@ pub const Options = struct { |
| 20 | /// By default uses the last path component of the process's first argument (`argv[0]`). | 20 | /// By default uses the last path component of the process's first argument (`argv[0]`). |
| 21 | /// When there is no `argv[0]` (such as with `parseSlice`), the default is `"<prog>"`. | 21 | /// When there is no `argv[0]` (such as with `parseSlice`), the default is `"<prog>"`. |
| 22 | prog: ?[]const u8 = null, | 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 | pub const Error = error{ | 29 | pub const Error = error{ |
| ... | @@ -56,7 +60,7 @@ pub const Error = error{ | ... | @@ -56,7 +60,7 @@ pub const Error = error{ |
| 56 | /// <other> (7) | 60 | /// <other> (7) |
| 57 | /// ``` | 61 | /// ``` |
| 58 | /// Forms (1), (2), and (3) must correspond to a field `Args.named.<name>`; see below for named argument handling. | 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 | /// Form (6) signals that all following arg strings are positional. | 64 | /// Form (6) signals that all following arg strings are positional. |
| 61 | /// Form (7) and all arg strings following form (6) are appended into the `positional` array in order. | 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,12 +111,20 @@ pub const Error = error{ |
| 107 | /// The first arg returned by the `ArgIterator` (`argv[0]`) is skipped by all the above parsing logic. | 111 | /// The first arg returned by the `ArgIterator` (`argv[0]`) is skipped by all the above parsing logic. |
| 108 | /// If `options.prog` is `null`, then the final path component of `argv[0]` is used by default. | 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 | /// It is not possible to precisely deallocate the memory allocated by this function. | 119 | /// It is not possible to precisely deallocate the memory allocated by this function. |
| 111 | /// An `ArenaAllocator` is recommended to prevent memory leaks. | 120 | /// An `ArenaAllocator` is recommended to prevent memory leaks. |
| 112 | pub fn parse(comptime Args: type, arena: Allocator, options: Options) Error!Args { | 121 | pub fn parse(comptime Args: type, arena: Allocator, options: Options) Error!Args { |
| 113 | var iter: ArgIterator = try .initWithAllocator(arena); | 122 | var iter: ArgIterator = try .initWithAllocator(arena); |
| 114 | // Do not call iter.deinit(). It holds the string data returned in the Args. | 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 | test parse { | 130 | test parse { |
| ... | @@ -151,12 +163,18 @@ test parse { | ... | @@ -151,12 +163,18 @@ test parse { |
| 151 | /// The first string arg returned by the `iter` (`argv[0]`) is skipped by all the parsing logic. | 163 | /// The first string arg returned by the `iter` (`argv[0]`) is skipped by all the parsing logic. |
| 152 | /// If `options.prog` is `null`, then the final path component of `argv[0]` is used by default. | 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 | /// An `ArenaAllocator` is recommended to cleanup the memory allocated from this function; | 171 | /// An `ArenaAllocator` is recommended to cleanup the memory allocated from this function; |
| 155 | /// however, it's also possible to free all the memory by freeing every slice field `[]const C` (other than `u8`) | 172 | /// however, it's also possible to free all the memory by freeing every slice field `[]const C` (other than `u8`) |
| 156 | /// in the returned `args.named` as well as freeing `args.positional`. | 173 | /// in the returned `args.named` as well as freeing `args.positional`. |
| 157 | pub fn parseIter(comptime Args: type, arena: Allocator, iter: anytype, options: Options) Error!Args { | 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>"; | 175 | const argv0 = iter.next(); |
| 159 | return innerParse(Args, arena, iter, prog, options.writer); | 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 | /// Like `parse`, but takes a slice of strings in place of using an `ArgIterator`. | 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,6 +185,11 @@ pub fn parseIter(comptime Args: type, arena: Allocator, iter: anytype, options: |
| 167 | /// Unlike `parse` and `parseIter`, this function does not skip the first item of `argv`. | 185 | /// Unlike `parse` and `parseIter`, this function does not skip the first item of `argv`. |
| 168 | /// Use `options.prog` instead. | 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 | /// An `ArenaAllocator` is recommended to cleanup the memory allocated from this function; | 193 | /// An `ArenaAllocator` is recommended to cleanup the memory allocated from this function; |
| 171 | /// however, it's also possible to free all the memory by freeing every slice field `[]const C` (other than `u8`) | 194 | /// however, it's also possible to free all the memory by freeing every slice field `[]const C` (other than `u8`) |
| 172 | /// in the returned `args.named` as well as freeing `args.positional`. | 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,7 +202,7 @@ pub fn parseSlice(comptime Args: type, arena: Allocator, argv: anytype, options: |
| 179 | else | 202 | else |
| 180 | @compileError("expected argv to be `*const [_]String` or `[]const String` where `String` is `[]const u8` or similar"); | 203 | @compileError("expected argv to be `*const [_]String` or `[]const String` where `String` is `[]const u8` or similar"); |
| 181 | var iter = ArgIteratorSlice(String){ .slice = argv }; | 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 | test parseSlice { | 208 | test parseSlice { |
| ... | @@ -219,8 +242,8 @@ test parseSlice { | ... | @@ -219,8 +242,8 @@ test parseSlice { |
| 219 | }, args); | 242 | }, args); |
| 220 | } | 243 | } |
| 221 | | 244 | |
| 222 | fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: []const u8, writer: ?*Writer) Error!Args { | 245 | fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: []const u8, writer: ?*Writer, exit_on_error: bool) Error!Args { |
| 223 | // arg0 has already been consumed. | 246 | // argv0 has already been consumed. |
| 224 | | 247 | |
| 225 | // Do all comptime checks up front so that we can be sure any compile error the user sees is the one we wrote. | 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 | comptime checkArgsType(Args); | 249 | comptime checkArgsType(Args); |
| ... | @@ -272,13 +295,16 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: [] | ... | @@ -272,13 +295,16 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: [] |
| 272 | } else { | 295 | } else { |
| 273 | printGeneratedHelp(writer, prog, named_info); | 296 | printGeneratedHelp(writer, prog, named_info); |
| 274 | } | 297 | } |
| | 298 | if (exit_on_error) { |
| | 299 | std.process.exit(1); |
| | 300 | } |
| 275 | return error.Help; | 301 | return error.Help; |
| 276 | } | 302 | } |
| 277 | | 303 | |
| 278 | if (arg.len >= 2 and arg[0] == '-' and isAlphabetic(arg[1])) { | 304 | if (arg.len >= 2 and arg[0] == '-' and isAlphabetic(arg[1])) { |
| 279 | // Always invalid. | 305 | // Always invalid. |
| 280 | // Examples: -h, -flag, -I/path | 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 | if (mem.eql(u8, arg, "--")) { | 309 | if (mem.eql(u8, arg, "--")) { |
| 284 | // Stop recognizing named arguments. Everything else is positional. | 310 | // Stop recognizing named arguments. Everything else is positional. |
| ... | @@ -312,31 +338,31 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: [] | ... | @@ -312,31 +338,31 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: [] |
| 312 | inline for (named_info.fields, 0..) |field, i| { | 338 | inline for (named_info.fields, 0..) |field, i| { |
| 313 | if (mem.eql(u8, field.name, arg_name)) { | 339 | if (mem.eql(u8, field.name, arg_name)) { |
| 314 | if (field.type == bool) { | 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 | @field(result.named, field.name) = !no_prefixed; | 342 | @field(result.named, field.name) = !no_prefixed; |
| 317 | fields_seen[i] = true; | 343 | fields_seen[i] = true; |
| 318 | break; | 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 | // All other argument types require a value. | 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 | switch (@typeInfo(field.type)) { | 351 | switch (@typeInfo(field.type)) { |
| 326 | .bool => unreachable, // Handled above. | 352 | .bool => unreachable, // Handled above. |
| 327 | .float => { | 353 | .float => { |
| 328 | @field(result.named, field.name) = std.fmt.parseFloat(field.type, arg_value) catch |err| { | 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 | .int => { | 358 | .int => { |
| 333 | @field(result.named, field.name) = std.fmt.parseInt(field.type, arg_value, 0) catch |err| { | 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 | .@"enum" => { | 363 | .@"enum" => { |
| 338 | @field(result.named, field.name) = std.meta.stringToEnum(field.type, arg_value) orelse { | 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 | .pointer => |ptrInfo| { | 368 | .pointer => |ptrInfo| { |
| ... | @@ -349,12 +375,12 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: [] | ... | @@ -349,12 +375,12 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: [] |
| 349 | .bool => comptime unreachable, // Nicer compile error emitted in checkArgsType(). | 375 | .bool => comptime unreachable, // Nicer compile error emitted in checkArgsType(). |
| 350 | .float => { | 376 | .float => { |
| 351 | try array_list.append(allocator, std.fmt.parseFloat(ptrInfo.child, arg_value) catch |err| { | 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 | .int => { | 381 | .int => { |
| 356 | try array_list.append(allocator, std.fmt.parseInt(ptrInfo.child, arg_value, 0) catch |err| { | 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 | .@"enum" => comptime unreachable, | 386 | .@"enum" => comptime unreachable, |
| ... | @@ -376,7 +402,7 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: [] | ... | @@ -376,7 +402,7 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: [] |
| 376 | } | 402 | } |
| 377 | } else { | 403 | } else { |
| 378 | // Didn't match anything. | 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,9 +413,9 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: [] |
| 387 | @field(result.named, field.name) = default; | 413 | @field(result.named, field.name) = default; |
| 388 | } else { | 414 | } else { |
| 389 | if (field.type == bool) { | 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 | } else { | 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,8 +482,11 @@ fn checkArgsType(comptime Args: type) void { |
| 456 | /// An error message will be written to `options.writer` or stderr by default, and `error.Usage` is returned. | 482 | /// An error message will be written to `options.writer` or stderr by default, and `error.Usage` is returned. |
| 457 | /// The given `msg` template is prefixed by `"error: "` and suffixed by a newline and a prompt to try passing in `--help`. | 483 | /// The given `msg` template is prefixed by `"error: "` and suffixed by a newline and a prompt to try passing in `--help`. |
| 458 | /// `options.prog` is not used by this function, but could be in the future. | 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 | pub fn @"error"(comptime msg: []const u8, args: anytype, options: Options) error{Usage} { | 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 | test @"error" { | 492 | test @"error" { |
| ... | @@ -472,15 +501,15 @@ test @"error" { | ... | @@ -472,15 +501,15 @@ test @"error" { |
| 472 | defer arena.deinit(); | 501 | defer arena.deinit(); |
| 473 | const args = try parseSlice(Args, arena.allocator(), &[_][]const u8{ "--output=o.txt", "i.txt" }, .{}); | 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)) { | 504 | if (std.fs.path.isAbsolute(args.named.output)) { |
| 476 | return std.cli.@"error"("--output must not be absolute: {s}", .{args.named.output}, .{}); | 505 | return std.cli.@"error"("--output must not be absolute: {s}", .{args.named.output}, .{ .exit_on_error = false }); |
| 477 | } | 506 | } |
| 478 | if (args.positional.len > 1) { | 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 | const whole_msg = | 513 | const whole_msg = |
| 485 | "error: " ++ msg ++ "\n" ++ | 514 | "error: " ++ msg ++ "\n" ++ |
| 486 | \\try --help for full help info | 515 | \\try --help for full help info |
| ... | @@ -491,6 +520,9 @@ fn usageError(writer: ?*Writer, comptime msg: []const u8, args: anytype) error{U | ... | @@ -491,6 +520,9 @@ fn usageError(writer: ?*Writer, comptime msg: []const u8, args: anytype) error{U |
| 491 | } else { | 520 | } else { |
| 492 | std.debug.print(whole_msg, args); | 521 | std.debug.print(whole_msg, args); |
| 493 | } | 522 | } |
| | 523 | if (exit_on_error) { |
| | 524 | std.process.exit(1); |
| | 525 | } |
| 494 | return error.Usage; | 526 | return error.Usage; |
| 495 | } | 527 | } |
| 496 | | 528 | |