authorgravatar for thejoshwolfe@gmail.comJosh Wolfe <thejoshwolfe@gmail.com> 2025-08-17 09:00:12-04:00
committergravatar for thejoshwolfe@gmail.comJosh Wolfe <thejoshwolfe@gmail.com> 2025-08-30 22:36:30-04:00
log7b386cc93f9f8ca723720d0905f24506b8b9f852
tree2c65c381fedc5b751d8c5b0318c9bc829fea8748
parent8cb3eb3a56bcaed627447c968cbeb55cbb050c58

add options.exit_on_error


1 files changed, 58 insertions(+), 26 deletions(-)

lib/std/cli.zig+58-26
...@@ -11,8 +11,8 @@ const mem = std.mem;...@@ -11,8 +11,8 @@ const mem = std.mem;
11const Allocator = mem.Allocator;11const Allocator = mem.Allocator;
1212
13pub const Options = struct {13pub 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,
1818
...@@ -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};
2428
25pub const Error = error{29pub 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.
112pub fn parse(comptime Args: type, arena: Allocator, options: Options) Error!Args {121pub 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}
117129
118test parse {130test 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`.
157pub fn parseIter(comptime Args: type, arena: Allocator, iter: anytype, options: Options) Error!Args {174pub 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}
161179
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 else202 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}
184207
185test parseSlice {208test parseSlice {
...@@ -219,8 +242,8 @@ test parseSlice {...@@ -219,8 +242,8 @@ test parseSlice {
219 }, args);242 }, args);
220}243}
221244
222fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: []const u8, writer: ?*Writer) Error!Args {245fn 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.
224247
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 }
277303
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/path306 // 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);
321347
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);
324350
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 }
382408
...@@ -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`.
459pub fn @"error"(comptime msg: []const u8, args: anytype, options: Options) error{Usage} {488pub 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}
462491
463test @"error" {492test @"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" }, .{});
474503
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}
482511
483fn usageError(writer: ?*Writer, comptime msg: []const u8, args: anytype) error{Usage} {512fn 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 info515 \\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}
496528