mirror of
https://github.com/ziglang/zig.git
synced 2026-02-15 13:58:27 +00:00
add options.exit_on_error
This commit is contained in:
parent
8cb3eb3a56
commit
7b386cc93f
@ -11,8 +11,8 @@ const mem = std.mem;
|
||||
const Allocator = mem.Allocator;
|
||||
|
||||
pub const Options = struct {
|
||||
/// When returning error.Usage, print a short error message to this writer, defaults to stderr.
|
||||
/// When returning error.Help, print the long help documentation to this writer, defaults to stdout.
|
||||
/// Parsing/validation errors and the long `--help` documentation will be written to this writer.
|
||||
/// By default, parsing/validation errors are written to stderr, and the long `--help` documentation is written to stdout.
|
||||
/// Any error while writing is silently ignored.
|
||||
writer: ?*Writer = null,
|
||||
|
||||
@ -20,6 +20,10 @@ pub const Options = struct {
|
||||
/// By default uses the last path component of the process's first argument (`argv[0]`).
|
||||
/// When there is no `argv[0]` (such as with `parseSlice`), the default is `"<prog>"`.
|
||||
prog: ?[]const u8 = null,
|
||||
|
||||
/// Call `std.process.exit` with an error status instead of returning `error.Usage` or `error.Help`.
|
||||
/// The default is `true` for `parse` and `@"error"`, and `false` otherwise.
|
||||
exit: ?bool = null,
|
||||
};
|
||||
|
||||
pub const Error = error{
|
||||
@ -56,7 +60,7 @@ pub const Error = error{
|
||||
/// <other> (7)
|
||||
/// ```
|
||||
/// Forms (1), (2), and (3) must correspond to a field `Args.named.<name>`; see below for named argument handling.
|
||||
/// Form (4) immediately prints the long help documentation and returns `error.Help`.
|
||||
/// Form (4) immediately prints the long help documentation and exits or returns `error.Help` depending on options.exit.
|
||||
/// Form (6) signals that all following arg strings are positional.
|
||||
/// Form (7) and all arg strings following form (6) are appended into the `positional` array in order.
|
||||
///
|
||||
@ -107,12 +111,20 @@ pub const Error = error{
|
||||
/// The first arg returned by the `ArgIterator` (`argv[0]`) is skipped by all the above parsing logic.
|
||||
/// If `options.prog` is `null`, then the final path component of `argv[0]` is used by default.
|
||||
///
|
||||
/// If a parsing/validation error occurs or the `--help` arg is given,
|
||||
/// this function calls `std.process.exit` with an error status unless `options.exit` is set to `false`,
|
||||
/// in which case parsing/validation errors return `error.Usage` and `--help` returns `error.Help`.
|
||||
/// Allocator errors are always returned from the function.
|
||||
///
|
||||
/// It is not possible to precisely deallocate the memory allocated by this function.
|
||||
/// An `ArenaAllocator` is recommended to prevent memory leaks.
|
||||
pub fn parse(comptime Args: type, arena: Allocator, options: Options) Error!Args {
|
||||
var iter: ArgIterator = try .initWithAllocator(arena);
|
||||
// Do not call iter.deinit(). It holds the string data returned in the Args.
|
||||
return parseIter(Args, arena, &iter, options);
|
||||
|
||||
const argv0 = iter.next();
|
||||
const prog = options.prog orelse if (argv0) |arg| std.fs.path.basename(arg) else "<prog>";
|
||||
return innerParse(Args, arena, &iter, prog, options.writer, options.exit orelse true);
|
||||
}
|
||||
|
||||
test parse {
|
||||
@ -151,12 +163,18 @@ test parse {
|
||||
/// The first string arg returned by the `iter` (`argv[0]`) is skipped by all the parsing logic.
|
||||
/// If `options.prog` is `null`, then the final path component of `argv[0]` is used by default.
|
||||
///
|
||||
/// If a parsing/validation error occurs or the `--help` arg is given,
|
||||
/// this function returns `error.Usage` or `error.Help` respectively,
|
||||
/// unless `options.exit` is set to `true`, in which case `std.process.exit` is called with an error status instead.
|
||||
/// Allocator errors are always returned from the function.
|
||||
///
|
||||
/// An `ArenaAllocator` is recommended to cleanup the memory allocated from this function;
|
||||
/// however, it's also possible to free all the memory by freeing every slice field `[]const C` (other than `u8`)
|
||||
/// in the returned `args.named` as well as freeing `args.positional`.
|
||||
pub fn parseIter(comptime Args: type, arena: Allocator, iter: anytype, options: Options) Error!Args {
|
||||
const prog = options.prog orelse if (iter.next()) |arg0| std.fs.path.basename(arg0) else "<prog>";
|
||||
return innerParse(Args, arena, iter, prog, options.writer);
|
||||
const argv0 = iter.next();
|
||||
const prog = options.prog orelse if (argv0) |arg| std.fs.path.basename(arg) else "<prog>";
|
||||
return innerParse(Args, arena, iter, prog, options.writer, options.exit orelse false);
|
||||
}
|
||||
|
||||
/// 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:
|
||||
/// Unlike `parse` and `parseIter`, this function does not skip the first item of `argv`.
|
||||
/// Use `options.prog` instead.
|
||||
///
|
||||
/// If a parsing/validation error occurs or the `--help` arg is given,
|
||||
/// this function returns `error.Usage` or `error.Help` respectively,
|
||||
/// unless `options.exit` is set to `true`, in which case `std.process.exit` is called with an error status instead.
|
||||
/// Allocator errors are always returned from the function.
|
||||
///
|
||||
/// An `ArenaAllocator` is recommended to cleanup the memory allocated from this function;
|
||||
/// however, it's also possible to free all the memory by freeing every slice field `[]const C` (other than `u8`)
|
||||
/// 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:
|
||||
else
|
||||
@compileError("expected argv to be `*const [_]String` or `[]const String` where `String` is `[]const u8` or similar");
|
||||
var iter = ArgIteratorSlice(String){ .slice = argv };
|
||||
return innerParse(Args, arena, &iter, options.prog orelse "<prog>", options.writer);
|
||||
return innerParse(Args, arena, &iter, options.prog orelse "<prog>", options.writer, options.exit orelse false);
|
||||
}
|
||||
|
||||
test parseSlice {
|
||||
@ -219,8 +242,8 @@ test parseSlice {
|
||||
}, args);
|
||||
}
|
||||
|
||||
fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: []const u8, writer: ?*Writer) Error!Args {
|
||||
// arg0 has already been consumed.
|
||||
fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: []const u8, writer: ?*Writer, exit_on_error: bool) Error!Args {
|
||||
// argv0 has already been consumed.
|
||||
|
||||
// Do all comptime checks up front so that we can be sure any compile error the user sees is the one we wrote.
|
||||
comptime checkArgsType(Args);
|
||||
@ -272,13 +295,16 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: []
|
||||
} else {
|
||||
printGeneratedHelp(writer, prog, named_info);
|
||||
}
|
||||
if (exit_on_error) {
|
||||
std.process.exit(1);
|
||||
}
|
||||
return error.Help;
|
||||
}
|
||||
|
||||
if (arg.len >= 2 and arg[0] == '-' and isAlphabetic(arg[1])) {
|
||||
// Always invalid.
|
||||
// Examples: -h, -flag, -I/path
|
||||
return usageError(writer, "unrecognized argument: {s}", .{arg});
|
||||
return usageError(writer, "unrecognized argument: {s}", .{arg}, exit_on_error);
|
||||
}
|
||||
if (mem.eql(u8, arg, "--")) {
|
||||
// Stop recognizing named arguments. Everything else is positional.
|
||||
@ -312,31 +338,31 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: []
|
||||
inline for (named_info.fields, 0..) |field, i| {
|
||||
if (mem.eql(u8, field.name, arg_name)) {
|
||||
if (field.type == bool) {
|
||||
if (immediate_value != null) return usageError(writer, "cannot specify value for bool argument: {s}", .{arg});
|
||||
if (immediate_value != null) return usageError(writer, "cannot specify value for bool argument: {s}", .{arg}, exit_on_error);
|
||||
@field(result.named, field.name) = !no_prefixed;
|
||||
fields_seen[i] = true;
|
||||
break;
|
||||
}
|
||||
if (no_prefixed) return usageError(writer, "unrecognized argument: {s}", .{arg});
|
||||
if (no_prefixed) return usageError(writer, "unrecognized argument: {s}", .{arg}, exit_on_error);
|
||||
|
||||
// All other argument types require a value.
|
||||
const arg_value = immediate_value orelse iter.next() orelse return usageError(writer, "expected argument after --{s}", .{field.name});
|
||||
const arg_value = immediate_value orelse iter.next() orelse return usageError(writer, "expected argument after --{s}", .{field.name}, exit_on_error);
|
||||
|
||||
switch (@typeInfo(field.type)) {
|
||||
.bool => unreachable, // Handled above.
|
||||
.float => {
|
||||
@field(result.named, field.name) = std.fmt.parseFloat(field.type, arg_value) catch |err| {
|
||||
return usageError(writer, "unable to parse --{s}={s}: {s}", .{ field.name, arg_value, @errorName(err) });
|
||||
return usageError(writer, "unable to parse --{s}={s}: {s}", .{ field.name, arg_value, @errorName(err) }, exit_on_error);
|
||||
};
|
||||
},
|
||||
.int => {
|
||||
@field(result.named, field.name) = std.fmt.parseInt(field.type, arg_value, 0) catch |err| {
|
||||
return usageError(writer, "unable to parse --{s}={s}: {s}", .{ field.name, arg_value, @errorName(err) });
|
||||
return usageError(writer, "unable to parse --{s}={s}: {s}", .{ field.name, arg_value, @errorName(err) }, exit_on_error);
|
||||
};
|
||||
},
|
||||
.@"enum" => {
|
||||
@field(result.named, field.name) = std.meta.stringToEnum(field.type, arg_value) orelse {
|
||||
return usageError(writer, "unrecognized value: --{s}={s}, expected one of: {s}", .{ field.name, arg_value, enumValuesExpr(field.type) });
|
||||
return usageError(writer, "unrecognized value: --{s}={s}, expected one of: {s}", .{ field.name, arg_value, enumValuesExpr(field.type) }, exit_on_error);
|
||||
};
|
||||
},
|
||||
.pointer => |ptrInfo| {
|
||||
@ -349,12 +375,12 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: []
|
||||
.bool => comptime unreachable, // Nicer compile error emitted in checkArgsType().
|
||||
.float => {
|
||||
try array_list.append(allocator, std.fmt.parseFloat(ptrInfo.child, arg_value) catch |err| {
|
||||
return usageError(writer, "unable to parse --{s}={s}: {s}", .{ field.name, arg_value, @errorName(err) });
|
||||
return usageError(writer, "unable to parse --{s}={s}: {s}", .{ field.name, arg_value, @errorName(err) }, exit_on_error);
|
||||
});
|
||||
},
|
||||
.int => {
|
||||
try array_list.append(allocator, std.fmt.parseInt(ptrInfo.child, arg_value, 0) catch |err| {
|
||||
return usageError(writer, "unable to parse --{s}={s}: {s}", .{ field.name, arg_value, @errorName(err) });
|
||||
return usageError(writer, "unable to parse --{s}={s}: {s}", .{ field.name, arg_value, @errorName(err) }, exit_on_error);
|
||||
});
|
||||
},
|
||||
.@"enum" => comptime unreachable,
|
||||
@ -376,7 +402,7 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: []
|
||||
}
|
||||
} else {
|
||||
// Didn't match anything.
|
||||
return usageError(writer, "unrecognized argument: {s}", .{arg});
|
||||
return usageError(writer, "unrecognized argument: {s}", .{arg}, exit_on_error);
|
||||
}
|
||||
}
|
||||
|
||||
@ -387,9 +413,9 @@ fn innerParse(comptime Args: type, allocator: Allocator, iter: anytype, prog: []
|
||||
@field(result.named, field.name) = default;
|
||||
} else {
|
||||
if (field.type == bool) {
|
||||
return usageError(writer, "missing required argument: --" ++ field.name ++ " or --no-" ++ field.name, .{});
|
||||
return usageError(writer, "missing required argument: --" ++ field.name ++ " or --no-" ++ field.name, .{}, exit_on_error);
|
||||
} else {
|
||||
return usageError(writer, "missing required argument: --" ++ field.name, .{});
|
||||
return usageError(writer, "missing required argument: --" ++ field.name, .{}, exit_on_error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -456,8 +482,11 @@ fn checkArgsType(comptime Args: type) void {
|
||||
/// An error message will be written to `options.writer` or stderr by default, and `error.Usage` is returned.
|
||||
/// The given `msg` template is prefixed by `"error: "` and suffixed by a newline and a prompt to try passing in `--help`.
|
||||
/// `options.prog` is not used by this function, but could be in the future.
|
||||
///
|
||||
/// This function calls `std.process.exit` with an error status unless `options.exit` is set to `false`, in which case it returns `error.Usage`.
|
||||
/// This matches the default behavior of `parse`, not `parseIter` or `parseSlice`.
|
||||
pub fn @"error"(comptime msg: []const u8, args: anytype, options: Options) error{Usage} {
|
||||
return usageError(options.writer, msg, args);
|
||||
return usageError(options.writer, msg, args, options.exit orelse true);
|
||||
}
|
||||
|
||||
test @"error" {
|
||||
@ -472,15 +501,15 @@ test @"error" {
|
||||
defer arena.deinit();
|
||||
const args = try parseSlice(Args, arena.allocator(), &[_][]const u8{ "--output=o.txt", "i.txt" }, .{});
|
||||
|
||||
if (std.fs.path.isAbsolutePosix(args.named.output)) {
|
||||
return std.cli.@"error"("--output must not be absolute: {s}", .{args.named.output}, .{});
|
||||
if (std.fs.path.isAbsolute(args.named.output)) {
|
||||
return std.cli.@"error"("--output must not be absolute: {s}", .{args.named.output}, .{ .exit_on_error = false });
|
||||
}
|
||||
if (args.positional.len > 1) {
|
||||
return std.cli.@"error"("expected exactly 1 positional arg", .{}, .{});
|
||||
return std.cli.@"error"("expected exactly 1 positional arg", .{}, .{ .exit_on_error = false });
|
||||
}
|
||||
}
|
||||
|
||||
fn usageError(writer: ?*Writer, comptime msg: []const u8, args: anytype) error{Usage} {
|
||||
fn usageError(writer: ?*Writer, comptime msg: []const u8, args: anytype, exit_on_error: bool) error{Usage} {
|
||||
const whole_msg =
|
||||
"error: " ++ msg ++ "\n" ++
|
||||
\\try --help for full help info
|
||||
@ -491,6 +520,9 @@ fn usageError(writer: ?*Writer, comptime msg: []const u8, args: anytype) error{U
|
||||
} else {
|
||||
std.debug.print(whole_msg, args);
|
||||
}
|
||||
if (exit_on_error) {
|
||||
std.process.exit(1);
|
||||
}
|
||||
return error.Usage;
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user