Skip to content

Commit

Permalink
Make Parser return defined error
Browse files Browse the repository at this point in the history
  • Loading branch information
sam701 committed Dec 31, 2023
1 parent 07cd3fa commit f627c8a
Show file tree
Hide file tree
Showing 5 changed files with 167 additions and 83 deletions.
48 changes: 44 additions & 4 deletions src/app_runner.zig
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ const ArenaAllocator = std.heap.ArenaAllocator;
const value_ref = @import("value_ref.zig");
const ValueRef = value_ref.ValueRef;
const App = @import("command.zig").App;
const Parser = @import("parser.zig").Parser;
const parser = @import("parser.zig");
const Parser = parser.Parser;
const Printer = @import("Printer.zig");

pub const AppRunner = struct {
// This arena and its allocator is intended to be used only for the value references
Expand Down Expand Up @@ -38,8 +40,46 @@ pub const AppRunner = struct {
var cr = try Parser(std.process.ArgIterator).init(app, iter, self.arena.child_allocator);
defer cr.deinit();

const action = try cr.parse();
self.deinit();
return action();
if (cr.parse()) |action| {
self.deinit();
return action();
} else |err| {
processError(err, cr.error_data orelse unreachable, app);
}
}
};

fn processError(err: parser.ParseError, err_data: parser.ErrorData, app: *const App) void {
switch (err) {
error.UnknownOption => printError(app, "unknown option '--{s}'", .{err_data.provided_string}),
error.UnknownOptionAlias => printError(app, "unknown option alias '-{c}'", .{err_data.option_alias}),
error.UnknownSubcommand => printError(app, "unknown subcommand '{s}'", .{err_data.provided_string}),
error.MissingRequiredOption => printError(app, "missing required option '--{s}'", .{err_data.entity_name}),
error.MissingRequiredPositionalArgument => printError(app, "missing required positional argument '{s}'", .{err_data.entity_name}),
error.MissingSubcommand => printError(app, "command '{s}' requires subcommand", .{err_data.entity_name}),
error.MissingOptionValue => printError(app, "option ('--{s}') requires value", .{err_data.entity_name}),
error.UnexpectedPositionalArgument => printError(app, "unexpected positional argument '{s}'", .{err_data.provided_string}),
error.CommandDoesNotHavePositionalArguments => printError(app, "command '{s}' does not have positional arguments", .{err_data.entity_name}),
error.InvalidValue => {
const iv = err_data.invalid_value;
const et = if (iv.entity_type == .option) "option" else "positional argument";
const px = if (iv.entity_type == .option) "--" else "";
if (iv.envvar) |ev| {
printError(app, "failed to parse option (--{s}) value '{s}' as {s} read from envvar {s}", .{ iv.entity_name, iv.provided_string, iv.value_type, ev });
} else {
printError(app, "failed to parse {s} ({s}{s}) provided value '{s}' as {s}", .{ et, px, iv.entity_name, iv.provided_string, iv.value_type });
}
},
error.OutOfMemory => printError(app, "out of memory", .{}),
}
}

fn printError(app: *const App, comptime fmt: []const u8, args: anytype) void {
var p = Printer.init(std.io.getStdErr(), app.help_config.color_usage);

p.printInColor(app.help_config.color_error, "ERROR");
p.format(": ", .{});
p.format(fmt, args);
p.write(&.{'\n'});
std.os.exit(1);
}
6 changes: 3 additions & 3 deletions src/arg.zig
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ pub const ArgumentInterpretation = union(enum) {
other: []const u8,
};

pub fn interpret(arg: []const u8) !ArgumentInterpretation {
pub fn interpret(arg: []const u8) error{MissingOptionValue}!ArgumentInterpretation {
if (arg.len == 0) return ArgumentInterpretation{ .other = arg };

if (arg[0] == '-') {
Expand All @@ -32,7 +32,7 @@ pub fn interpret(arg: []const u8) !ArgumentInterpretation {
}

if (std.mem.indexOfScalar(u8, name, '=')) |ix| {
if (name.len < ix + 2) return error.MissingOptionArgument;
if (name.len < ix + 2) return error.MissingOptionValue;
return ArgumentInterpretation{ .option = OptionInterpretation{
.option_type = option_type,
.name = name[0..ix],
Expand Down Expand Up @@ -103,6 +103,6 @@ test "missing option value" {
if (interpret("--abc=")) |_| {
try expect(false);
} else |err| {
try expect(err == error.MissingOptionArgument);
try expect(err == error.MissingOptionValue);
}
}
156 changes: 98 additions & 58 deletions src/parser.zig
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,35 @@ const PositionalArgsHelper = @import("PositionalArgsHelper.zig");

pub const ParseResult = command.ExecFn;

pub const EntityType = enum {
option,
positional_argument,
};
pub const ErrorData = union {
provided_string: []const u8,
entity_name: []const u8,
option_alias: u8,
invalid_value: struct {
entity_type: EntityType,
entity_name: []const u8,
provided_string: []const u8,
value_type: []const u8,
envvar: ?[]const u8 = null,
},
};

pub const ParseError = error{
UnknownOption,
UnknownOptionAlias,
UnknownSubcommand,
MissingRequiredOption,
MissingRequiredPositionalArgument,
MissingSubcommand,
MissingOptionValue,
UnexpectedPositionalArgument,
CommandDoesNotHavePositionalArguments,
} || Allocator.Error || value_parser.ValueParseError;

pub fn Parser(comptime Iterator: type) type {
return struct {
const Self = @This();
Expand All @@ -26,6 +55,7 @@ pub fn Parser(comptime Iterator: type) type {
position_argument_ix: usize = 0,
next_arg: ?[]const u8 = null,
global_options: *GlobalOptions,
error_data: ?ErrorData = null,

pub fn init(app: *const command.App, it: Iterator, alloc: Allocator) !Self {
return Self{
Expand All @@ -46,7 +76,7 @@ pub fn Parser(comptime Iterator: type) type {
return self.command_path.items[self.command_path.items.len - 1];
}

pub fn parse(self: *Self) anyerror!ParseResult {
pub fn parse(self: *Self) ParseError!ParseResult {
try self.command_path.append(&self.app.command);

_ = self.nextArg();
Expand All @@ -57,23 +87,23 @@ pub fn Parser(comptime Iterator: type) type {
} else if (argp.interpret(arg)) |interpretation| {
args_only = try self.process_interpretation(&interpretation);
} else |err| {
switch (err) {
error.MissingOptionArgument => self.fail("missing argument: '{s}'", .{arg}),
}
self.error_data = ErrorData{ .provided_string = arg };
return err;
}
}
return self.finalize();
}

fn finalize(self: *Self) !ParseResult {
fn finalize(self: *Self) ParseError!ParseResult {
for (self.command_path.items) |cmd| {
if (cmd.options) |options| {
for (options) |*opt| {
try self.set_option_value_from_envvar(opt);
try opt.value_ref.finalize(self.alloc);

if (opt.required and opt.value_ref.element_count == 0) {
self.fail("missing required option '{s}'", .{opt.long_name});
self.error_data = ErrorData{ .entity_name = opt.long_name };
return error.MissingRequiredOption;
}
}
}
Expand All @@ -87,7 +117,8 @@ pub fn Parser(comptime Iterator: type) type {
try parg.value_ref.finalize(self.alloc);

if (it.index <= required_args_no and parg.value_ref.element_count == 0) {
self.fail("missing required positional argument '{s}'", .{parg.name});
self.error_data = ErrorData{ .entity_name = parg.name };
return error.MissingRequiredPositionalArgument;
}
}
}
Expand All @@ -101,30 +132,39 @@ pub fn Parser(comptime Iterator: type) type {
return act.exec;
},
.subcommands => {
self.fail("command '{s}': no subcommand provided", .{self.current_command().name});
unreachable;
self.error_data = ErrorData{ .entity_name = self.current_command().name };
return error.MissingSubcommand;
},
}
}

fn handlePositionalArgument(self: *Self, arg: []const u8) !void {
fn handlePositionalArgument(self: *Self, arg: []const u8) ParseError!void {
const cmd = self.current_command();
switch (cmd.target) {
.subcommands => {
self.fail("command '{s}' cannot have positional arguments", .{cmd.name});
self.error_data = ErrorData{ .entity_name = cmd.name };
return error.CommandDoesNotHavePositionalArguments;
},
.action => |act| {
if (act.positional_args) |*posArgs| {
var posH = PositionalArgsHelper{ .inner = posArgs };
if (self.position_argument_ix >= posH.len()) {
self.fail("unexpected positional argument '{s}'", .{arg});
self.error_data = ErrorData{ .provided_string = arg };
return error.UnexpectedPositionalArgument;
}

const posArg = posH.at(self.position_argument_ix);
var posArgRef = posArg.value_ref;
posArgRef.put(arg, self.alloc) catch |err| {
self.fail("positional argument ({s}): cannot parse '{s}' as {s}: {s}", .{ posArg.name, arg, posArgRef.value_data.type_name, @errorName(err) });
unreachable;
self.error_data = ErrorData{
.invalid_value = .{
.entity_type = .positional_argument,
.entity_name = posArg.name,
.provided_string = arg,
.value_type = posArgRef.value_data.type_name,
},
};
return err;
};
if (posArgRef.value_type == vref.ValueType.single) {
self.position_argument_ix += 1;
Expand All @@ -134,21 +174,23 @@ pub fn Parser(comptime Iterator: type) type {
}
}

fn set_option_value_from_envvar(self: *const Self, opt: *const command.Option) !void {
fn set_option_value_from_envvar(self: *Self, opt: *const command.Option) ParseError!void {
if (opt.value_ref.element_count > 0) return;

if (opt.envvar) |envvar_name| {
if (std.process.getEnvVarOwned(self.alloc, envvar_name)) |value| {
defer self.alloc.free(value);
opt.value_ref.put(value, self.alloc) catch |err| {
self.fail("envvar({s}): cannot parse {s} value '{s}': {s}", .{ envvar_name, opt.value_ref.value_data.type_name, value, @errorName(err) });
unreachable;
};
} else |err| {
if (err != std.process.GetEnvVarOwnedError.EnvironmentVariableNotFound) {
self.error_data = ErrorData{ .invalid_value = .{
.entity_type = .option,
.entity_name = opt.long_name,
.provided_string = value,
.value_type = opt.value_ref.value_data.type_name,
.envvar = envvar_name,
} };
return err;
}
}
};
} else |_| {}
} else if (self.app.option_envvar_prefix) |prefix| {
var envvar_name = try self.alloc.alloc(u8, opt.long_name.len + prefix.len);
defer self.alloc.free(envvar_name);
Expand All @@ -164,18 +206,20 @@ pub fn Parser(comptime Iterator: type) type {
if (std.process.getEnvVarOwned(self.alloc, envvar_name)) |value| {
defer self.alloc.free(value);
opt.value_ref.put(value, self.alloc) catch |err| {
self.fail("envvar({s}): cannot parse {s} value '{s}': {s}", .{ envvar_name, opt.value_ref.value_data.type_name, value, @errorName(err) });
unreachable;
};
} else |err| {
if (err != std.process.GetEnvVarOwnedError.EnvironmentVariableNotFound) {
self.error_data = ErrorData{ .invalid_value = .{
.entity_type = .option,
.entity_name = opt.long_name,
.provided_string = value,
.value_type = opt.value_ref.value_data.type_name,
.envvar = envvar_name,
} };
return err;
}
}
};
} else |_| {}
}
}

fn process_interpretation(self: *Self, int: *const argp.ArgumentInterpretation) !bool {
fn process_interpretation(self: *Self, int: *const argp.ArgumentInterpretation) ParseError!bool {
var args_only = false;
try switch (int.*) {
.option => |opt| self.process_option(&opt),
Expand All @@ -192,7 +236,8 @@ pub fn Parser(comptime Iterator: type) type {
return false;
}
}
self.fail("no such subcommand '{s}'", .{some_name});
self.error_data = ErrorData{ .provided_string = some_name };
return error.UnknownSubcommand;
},
.action => {
try self.handlePositionalArgument(some_name);
Expand All @@ -216,12 +261,12 @@ pub fn Parser(comptime Iterator: type) type {
self.next_arg = value;
}

fn process_option(self: *Self, option_interpretation: *const argp.OptionInterpretation) !void {
fn process_option(self: *Self, option_interpretation: *const argp.OptionInterpretation) ParseError!void {
var opt: *const command.Option = switch (option_interpretation.option_type) {
.long => self.find_option_by_name(option_interpretation.name),
.long => try self.find_option_by_name(option_interpretation.name),
.short => a: {
self.set_concatenated_boolean_options(self.current_command(), option_interpretation.name[0 .. option_interpretation.name.len - 1]);
break :a self.find_option_by_alias(self.current_command(), option_interpretation.name[option_interpretation.name.len - 1]);
try self.set_concatenated_boolean_options(self.current_command(), option_interpretation.name[0 .. option_interpretation.name.len - 1]);
break :a try self.find_option_by_alias(self.current_command(), option_interpretation.name[option_interpretation.name.len - 1]);
},
};

Expand Down Expand Up @@ -257,27 +302,22 @@ pub fn Parser(comptime Iterator: type) type {
try opt.value_ref.put(str_true, self.alloc);
} else {
const arg = option_interpretation.value orelse self.nextArg() orelse {
self.fail("missing argument for {s}", .{opt.long_name});
unreachable;
self.error_data = ErrorData{ .entity_name = opt.long_name };
return error.MissingOptionValue;
};
opt.value_ref.put(arg, self.alloc) catch |err| {
self.fail("option({s}): cannot parse {s} value: {s}", .{ opt.long_name, opt.value_ref.value_data.type_name, @errorName(err) });
unreachable;
self.error_data = ErrorData{ .invalid_value = .{
.entity_type = .option,
.entity_name = opt.long_name,
.provided_string = arg,
.value_type = opt.value_ref.value_data.type_name,
} };
return err;
};
}
}

fn fail(self: *const Self, comptime fmt: []const u8, args: anytype) void {
var p = Printer.init(std.io.getStdErr(), self.app.help_config.color_usage);

p.printInColor(self.app.help_config.color_error, "ERROR");
p.format(": ", .{});
p.format(fmt, args);
p.write(&.{'\n'});
std.os.exit(1);
}

fn find_option_by_name(self: *const Self, option_name: []const u8) *const command.Option {
fn find_option_by_name(self: *Self, option_name: []const u8) error{UnknownOption}!*const command.Option {
for (0..self.command_path.items.len) |ix| {
const cmd = self.command_path.items[self.command_path.items.len - ix - 1];
if (cmd.options) |option_list| {
Expand All @@ -293,11 +333,11 @@ pub fn Parser(comptime Iterator: type) type {
return option;
}
}
self.fail("no such option '--{s}'", .{option_name});
unreachable;
self.error_data = ErrorData{ .provided_string = option_name };
return error.UnknownOption;
}

fn find_option_by_alias(self: *const Self, cmd: *const command.Command, option_alias: u8) *const command.Option {
fn find_option_by_alias(self: *Self, cmd: *const command.Command, option_alias: u8) error{UnknownOptionAlias}!*const command.Option {
if (option_alias == 'h') {
return self.global_options.option_show_help;
}
Expand All @@ -310,18 +350,18 @@ pub fn Parser(comptime Iterator: type) type {
}
}
}
self.fail("no such option alias '-{c}'", .{option_alias});
unreachable;
self.error_data = ErrorData{ .option_alias = option_alias };
return error.UnknownOptionAlias;
}

/// Set boolean options provided like `-acde`
fn set_concatenated_boolean_options(self: *const Self, cmd: *const command.Command, options: []const u8) void {
fn set_concatenated_boolean_options(self: *Self, cmd: *const command.Command, options: []const u8) ParseError!void {
for (options) |alias| {
var opt = self.find_option_by_alias(cmd, alias);
var opt = try self.find_option_by_alias(cmd, alias);
if (opt.value_ref.value_data.is_bool) {
opt.value_ref.put("true", self.alloc) catch unreachable;
} else {
self.fail("'-{c}' is not a boolean option", .{alias});
return error.MissingOptionValue;
}
}
}
Expand Down
Loading

0 comments on commit f627c8a

Please sign in to comment.