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;
1111const Allocator = mem.Allocator;
1212
1313pub 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.
1616 /// Any error while writing is silently ignored.
1717 writer: ?*Writer = null,
1818
......@@ -20,6 +20,10 @@ pub const Options = struct {
2020 /// By default uses the last path component of the process's first argument (`argv[0]`).
2121 /// When there is no `argv[0]` (such as with `parseSlice`), the default is `"<prog>"`.
2222 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,
2327};
2428
2529pub const Error = error{
......@@ -56,7 +60,7 @@ pub const Error = error{
5660/// <other> (7)
5761/// ```
5862/// 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.
6064/// Form (6) signals that all following arg strings are positional.
6165/// Form (7) and all arg strings following form (6) are appended into the `positional` array in order.
6266///
......@@ -107,12 +111,20 @@ pub const Error = error{
107111/// The first arg returned by the `ArgIterator` (`argv[0]`) is skipped by all the above parsing logic.
108112/// If `options.prog` is `null`, then the final path component of `argv[0]` is used by default.
109113///
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///
110119/// It is not possible to precisely deallocate the memory allocated by this function.
111120/// An `ArenaAllocator` is recommended to prevent memory leaks.
112121pub fn parse(comptime Args: type, arena: Allocator, options: Options) Error!Args {
113122 var iter: ArgIterator = try .initWithAllocator(arena);
114123 // 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);
116128}
117129
118130test parse {
......@@ -151,12 +163,18 @@ test parse {
151163/// The first string arg returned by the `iter` (`argv[0]`) is skipped by all the parsing logic.
152164/// If `options.prog` is `null`, then the final path component of `argv[0]` is used by default.
153165///
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///
154171/// An `ArenaAllocator` is recommended to cleanup the memory allocated from this function;
155172/// however, it's also possible to free all the memory by freeing every slice field `[]const C` (other than `u8`)
156173/// in the returned `args.named` as well as freeing `args.positional`.
157174pub 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);
160178}
161179
162180/// 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:
167185/// Unlike `parse` and `parseIter`, this function does not skip the first item of `argv`.
168186/// Use `options.prog` instead.
169187///
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///
170193/// An `ArenaAllocator` is recommended to cleanup the memory allocated from this function;
171194/// however, it's also possible to free all the memory by freeing every slice field `[]const C` (other than `u8`)
172195/// 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:
179202 else
180203 @compileError("expected argv to be `*const [_]String` or `[]const String` where `String` is `[]const u8` or similar");
181204 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);
183206}
184207
185208test parseSlice {
......@@ -219,8 +242,8 @@ test parseSlice {
219242 }, args);
220243}
221244
222fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: []const u8, writer: ?*Writer) Error!Args {
223 // arg0 has already been consumed.
245fn 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.
224247
225248 // Do all comptime checks up front so that we can be sure any compile error the user sees is the one we wrote.
226249 comptime checkArgsType(Args);
......@@ -272,13 +295,16 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: []
272295 } else {
273296 printGeneratedHelp(writer, prog, named_info);
274297 }
298 if (exit_on_error) {
299 std.process.exit(1);
300 }
275301 return error.Help;
276302 }
277303
278304 if (arg.len >= 2 and arg[0] == '-' and isAlphabetic(arg[1])) {
279305 // Always invalid.
280306 // Examples: -h, -flag, -I/path
281 return usageError(writer, "unrecognized argument: {s}", .{arg});
307 return usageError(writer, "unrecognized argument: {s}", .{arg}, exit_on_error);
282308 }
283309 if (mem.eql(u8, arg, "--")) {
284310 // Stop recognizing named arguments. Everything else is positional.
......@@ -312,31 +338,31 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: []
312338 inline for (named_info.fields, 0..) |field, i| {
313339 if (mem.eql(u8, field.name, arg_name)) {
314340 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);
316342 @field(result.named, field.name) = !no_prefixed;
317343 fields_seen[i] = true;
318344 break;
319345 }
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
322348 // 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
325351 switch (@typeInfo(field.type)) {
326352 .bool => unreachable, // Handled above.
327353 .float => {
328354 @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);
330356 };
331357 },
332358 .int => {
333359 @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);
335361 };
336362 },
337363 .@"enum" => {
338364 @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);
340366 };
341367 },
342368 .pointer => |ptrInfo| {
......@@ -349,12 +375,12 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: []
349375 .bool => comptime unreachable, // Nicer compile error emitted in checkArgsType().
350376 .float => {
351377 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);
353379 });
354380 },
355381 .int => {
356382 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);
358384 });
359385 },
360386 .@"enum" => comptime unreachable,
......@@ -376,7 +402,7 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: []
376402 }
377403 } else {
378404 // Didn't match anything.
379 return usageError(writer, "unrecognized argument: {s}", .{arg});
405 return usageError(writer, "unrecognized argument: {s}", .{arg}, exit_on_error);
380406 }
381407 }
382408
......@@ -387,9 +413,9 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: []
387413 @field(result.named, field.name) = default;
388414 } else {
389415 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);
391417 } else {
392 return usageError(writer, "missing required argument: --" ++ field.name, .{});
418 return usageError(writer, "missing required argument: --" ++ field.name, .{}, exit_on_error);
393419 }
394420 }
395421 }
......@@ -456,8 +482,11 @@ fn checkArgsType(comptime Args: type) void {
456482/// An error message will be written to `options.writer` or stderr by default, and `error.Usage` is returned.
457483/// The given `msg` template is prefixed by `"error: "` and suffixed by a newline and a prompt to try passing in `--help`.
458484/// `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`.
459488pub 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);
461490}
462491
463492test @"error" {
......@@ -472,15 +501,15 @@ test @"error" {
472501 defer arena.deinit();
473502 const args = try parseSlice(Args, arena.allocator(), &[_][]const u8{ "--output=o.txt", "i.txt" }, .{});
474503
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 });
477506 }
478507 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 });
480509 }
481510}
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} {
484513 const whole_msg =
485514 "error: " ++ msg ++ "\n" ++
486515 \\try --help for full help info
......@@ -491,6 +520,9 @@ fn usageError(writer: ?*Writer, comptime msg: []const u8, args: anytype) error{U
491520 } else {
492521 std.debug.print(whole_msg, args);
493522 }
523 if (exit_on_error) {
524 std.process.exit(1);
525 }
494526 return error.Usage;
495527}
496528