14 Commits

Author SHA1 Message Date
0d091611dd parser: produce helpful error messages on command line errors
This is the first cut at providing human-readable context for command
line parsing failures. Since these failures are due to incorrect
input (normally produced by a human), closing the information loop at
the human layer makes a hell of a lot more sense than dumping an error
traceback with a (possibly cryptic) error name and calling it a day.

This approach doesn't print anything out by default and still depends
on the user to choose exactly how the handle and print the error
message. Errors are propagated from subcommands, though they end up
being copied, which shouldn't be strictly necessary. Maybe this can be
improved in the future. OutOfMemory has been added to ParseError to
simplify the code a bit.

The demo has been updated with a simplistic example of what presenting
error messages to the user may look like. I don't know that this
produces useful messages for every possible failure scenario, but it
does for the most common ones.
2024-04-07 11:23:25 -07:00
5f45290423 parser: run subcommand parsing/callbacks iteratively
This changes the parse/callback flow so that subcommands are run
iteratively from the base command rather than recursively. The primary
advantages of this approach are some stack space savings and much less
convoluted backtraces for deeply nested command hierarchies. The
overall order of operations has not changed, i.e. the full command
line is parsed before command callback dispatch starts.

Fixes: #12
2024-04-06 23:36:43 -07:00
b77a1f59c2 command: coerce field names to null-terminated strings
ziglang/zig@a02bd81760 changed
builtin.StructField to necessitate this, but this should also be
backwards compatible since these should decay to plain slices just
fine.
2024-01-15 22:42:26 -08:00
03a4404a17 parser: stray var -> const
Surprised there wasn't more of this in here, to be honest.
2024-01-15 22:41:01 -08:00
6e1199afa9 meta: fix redundant qualifiers and improve tuple field generation 2024-01-15 22:40:26 -08:00
70c6cea591 demo: demonstrate preformatted description text functionality
Throw in a lipsum as well to show off the wrapped text better, while
we're at it.
2023-11-08 22:58:01 -08:00
645ef24a4a command: preserve subcommand definition order
This was as simple as switching to an order preserving hashmap. This
lets the user decide which order their subcommands should be presented
in.
2023-11-08 22:56:11 -08:00
768a81e2bd help: hack in support for preformatted lines
This works, but there are probably interesting edge cases around how it
works when directly adjacent to other paragraphs. I will have to think
about it a bit. This wrapping code in general would benefit from term
queries.

Perhaps violating the principle of least astonishment quite severely is
the fact that "> a" and ">" are detected as preformatted, but ">a" is
normal wrapped text. Supporting both ">a" and "> a" leads to
nonobvious whitespace behavior, but this code should not be able to
runtime error outside of the writer dying. This may be reevaluated in
the future, but I will leave it as-is for now.
2023-11-08 22:52:28 -08:00
35915191fb demo: update to use interfaces directly
This adds a group and saves some lines.
2023-09-10 15:27:03 -07:00
76e8dedf14 command: add commandGroup function
This just creates an empty command with an auto-assigned noop callback.
This is useful sugar for creating a group of commands under a common
name because previously the user would have to define their own noop
callback and bind it. This just takes a description string
(and, optionally, a help flag override).
2023-09-10 15:27:03 -07:00
8ac610ae71 command, parser: try to clean up UserContext type handling
This is a feeble attempt to unify some logic, as I realized that
Command.createInterface had different logic for handling the user
context than Parser did, which broke certain use cases (using a slice
as the context for example).

I'm not convinced this really unifies the logic as much as wraps it in
another layer of indirection, but at least the core problem is solved.
2023-09-10 14:50:44 -07:00
8bba68e5a9 help: still print 0-length argument descriptions
I think I had initially intended 0-length descriptions to be "hidden"
options, but this doesn't really work well with arguments, and it also
doesn't make intention clear. Perhaps an additional field should be
added to the parameter specification to support hiding options
(this does not make sense for non-named options).
2023-09-10 14:45:34 -07:00
390a1ba4fd parser: parse into 0-terminated strings
This was kind of an annoying change to make since 0.11.0 has issues
where it will point to the wrong srcloc on compile errors in generic
code (which this 100% is) fortunately fixed in master. The motivation
for this change is that the arg vector already contains 0-terminated
strings, so we can avoid a lot of copies. This makes forwarding
command-line arguments to C-functions that expect zero-terminated
strings much more straightforward, and they also automatically decay
to normal slices.

Unfortunately, environment variable values are NOT zero-terminated, so
they are currently copied with zero-termination. This seems to be the
fault of Windows/WASI, both of which already are performing
allocations (Windows to convert from UTF-16 to UTF-8, and WASI to get
a copy of the environment). By duplicating the std EnvMap
implementation, we could make a version that generates 0-terminated
env vars without extra copies, but I'll skip on doing that for now.
2023-08-27 13:53:14 -07:00
0695743a1f parser: add addSubcommand to ParserInterface
This means that ParserInterface can do all of the important things that
Parser can do, which makes Command.createInterface a lot more useful
(there wasn't previously a way to add subcommands to an interface
created that way without a mass of extremely suspect casting).

This commit also makes the language around this. They're subcommands,
not children, and they have names, not verbs, associated with them.
Glad we could clear that up.
2023-08-22 21:05:27 -07:00
9 changed files with 510 additions and 180 deletions

View File

@@ -8,9 +8,22 @@ const Choice = enum { first, second };
const cli = cmd: {
var cmd = CommandBuilder(*u32){
.description =
\\The definitive noclip demonstration utility
\\The definitive noclip demonstration utility.
\\
\\This command demonstrates the functionality of the noclip library. cool!
\\
\\> // implementing factorial recursively is a silly thing to do
\\> pub fn fact(n: u64) u64 {
\\> if (n == 0) return 1;
\\> return n*fact(n - 1);
\\> }
\\
\\Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
\\incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis
\\nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
\\Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore
\\eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident,
\\sunt in culpa qui officia deserunt mollit anim id est laborum.
,
};
cmd.addOption(.{ .OutputType = struct { u8, u8 } }, .{
@@ -30,6 +43,13 @@ const cli = cmd: {
.description = "enum choice option",
.nice_type_name = "choice",
});
cmd.stringOption(.{
.name = "string",
.short_tag = "-s",
.long_tag = "--string",
.env_var = "NOCLIP_STRING",
.description = "A string value option",
});
cmd.addOption(.{ .OutputType = u32 }, .{
.name = "default",
.short_tag = "-d",
@@ -80,8 +100,8 @@ const subcommand = cmd: {
.falsy = .{ .long_tag = "--no-flag" },
.env_var = "NOCLIP_SUBFLAG",
});
cmd.addArgument(.{ .OutputType = []const u8 }, .{ .name = "argument" });
cmd.addArgument(.{ .OutputType = []const u8 }, .{
cmd.stringArgument(.{ .name = "argument" });
cmd.stringArgument(.{
.name = "arg",
.description = "This is an argument that doesn't really do anything, but it's very important.",
});
@@ -95,6 +115,7 @@ fn subHandler(context: []const u8, result: subcommand.Output()) !void {
fn cliHandler(context: *u32, result: cli.Output()) !void {
std.debug.print("context: {d}\n", .{context.*});
std.debug.print("callback is working {s}\n", .{result.string orelse "null"});
std.debug.print("callback is working {any}\n", .{result.choice});
std.debug.print("callback is working {d}\n", .{result.default});
context.* += 1;
@@ -105,17 +126,29 @@ pub fn main() !u8 {
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var parser = try cli.createParser(cliHandler, allocator);
defer parser.deinitTree();
const base = try noclip.commandGroup(allocator, .{ .description = "base group" });
defer base.deinitTree();
var context: u32 = 2;
const sc: []const u8 = "whassup";
var subcon = try subcommand.createParser(subHandler, allocator);
try parser.addSubcommand("verb", subcon.interface(&sc));
try base.addSubcommand("main", try cli.createInterface(allocator, cliHandler, &context));
try base.addSubcommand("other", try subcommand.createInterface(allocator, subHandler, &sc));
const iface = parser.interface(&context);
iface.execute() catch return 1;
const group = try noclip.commandGroup(allocator, .{ .description = "final level of a deeply nested subcommand" });
const subcon = try noclip.commandGroup(allocator, .{ .description = "third level of a deeply nested subcommand" });
const nested = try noclip.commandGroup(allocator, .{ .description = "second level of a deeply nested subcommand" });
const deeply = try noclip.commandGroup(allocator, .{ .description = "start of a deeply nested subcommand" });
try base.addSubcommand("deeply", deeply);
try deeply.addSubcommand("nested", nested);
try nested.addSubcommand("subcommand", subcon);
try subcon.addSubcommand("group", group);
try group.addSubcommand("run", try cli.createInterface(allocator, cliHandler, &context));
base.execute() catch |err| {
std.io.getStdErr().writeAll(base.getParseError()) catch {};
return err;
};
return 0;
}

View File

@@ -72,6 +72,76 @@ fn BuilderGenerics(comptime UserContext: type) type {
};
}
pub const GroupOptions = struct {
help_flag: ShortLongPair = .{ .short_tag = "-h", .long_tag = "--help" },
description: []const u8,
};
pub fn commandGroup(allocator: std.mem.Allocator, comptime options: GroupOptions) !ParserInterface {
const cmd = comptime CommandBuilder(void){
.help_flag = options.help_flag,
.description = options.description,
.subcommand_required = true,
};
return try cmd.createInterface(allocator, cmd.noopCallback());
}
fn InterfaceCreator(comptime Command: type) type {
return if (Command.ICC.InputType()) |Type|
struct {
pub fn createInterface(
comptime self: Command,
allocator: std.mem.Allocator,
comptime callback: self.CallbackSignature(),
context: Type,
) !ParserInterface {
return try self._createInterfaceImpl(allocator, callback, context);
}
}
else
struct {
pub fn createInterface(
comptime self: Command,
allocator: std.mem.Allocator,
comptime callback: self.CallbackSignature(),
) !ParserInterface {
return try self._createInterfaceImpl(allocator, callback, void{});
}
};
}
pub const InterfaceContextCategory = union(enum) {
empty,
pointer: type,
value: type,
pub fn fromType(comptime ContextType: type) InterfaceContextCategory {
return switch (@typeInfo(ContextType)) {
.Void => .empty,
.Pointer => |info| if (info.size == .Slice) .{ .value = ContextType } else .{ .pointer = ContextType },
// technically, i0, u0, and struct{} should be treated as empty, probably
else => .{ .value = ContextType },
};
}
pub fn InputType(comptime self: InterfaceContextCategory) ?type {
return switch (self) {
.empty => null,
.pointer => |Type| Type,
.value => |Type| *const Type,
};
}
pub fn OutputType(comptime self: InterfaceContextCategory) type {
return switch (self) {
.empty => void,
.pointer => |Type| Type,
.value => |Type| Type,
};
}
};
pub fn CommandBuilder(comptime UserContext: type) type {
return struct {
param_spec: ncmeta.TupleBuilder = .{},
@@ -82,6 +152,7 @@ pub fn CommandBuilder(comptime UserContext: type) type {
description: []const u8,
pub const UserContextType = UserContext;
pub const ICC: InterfaceContextCategory = InterfaceContextCategory.fromType(UserContextType);
pub fn createParser(
comptime self: @This(),
@@ -96,16 +167,18 @@ pub fn CommandBuilder(comptime UserContext: type) type {
return Parser(self, callback){
.arena = arena,
.allocator = arena_alloc,
.subcommands = std.hash_map.StringHashMap(ParserInterface).init(arena_alloc),
.subcommands = parser.CommandMap.init(arena_alloc),
.help_builder = help.HelpBuilder(self).init(arena_alloc),
};
}
pub fn createInterface(
pub usingnamespace InterfaceCreator(@This());
fn _createInterfaceImpl(
comptime self: @This(),
comptime callback: self.CallbackSignature(),
allocator: std.mem.Allocator,
context: UserContextType,
comptime callback: self.CallbackSignature(),
context: (ICC.InputType() orelse void),
) !ParserInterface {
var arena = try allocator.create(std.heap.ArenaAllocator);
arena.* = std.heap.ArenaAllocator.init(allocator);
@@ -115,11 +188,11 @@ pub fn CommandBuilder(comptime UserContext: type) type {
this_parser.* = .{
.arena = arena,
.allocator = arena_alloc,
.subcommands = std.hash_map.StringHashMap(ParserInterface).init(arena_alloc),
.subcommands = parser.CommandMap.init(arena_alloc),
.help_builder = help.HelpBuilder(self).init(arena_alloc),
};
if (UserContextType == void) {
if (comptime ICC == .empty) {
return this_parser.interface();
} else {
return this_parser.interface(context);
@@ -133,7 +206,7 @@ pub fn CommandBuilder(comptime UserContext: type) type {
self.help_flag = tags;
}
const string_generics = BuilderGenerics(UserContext){ .OutputType = []const u8 };
const string_generics = BuilderGenerics(UserContext){ .OutputType = [:0]const u8 };
pub fn stringOption(
comptime self: *@This(),
@@ -271,8 +344,14 @@ pub fn CommandBuilder(comptime UserContext: type) type {
return self.param_spec.realTuple();
}
pub fn noopCallback(comptime self: @This()) self.CallbackSignature() {
return struct {
fn callback(_: UserContextType, _: self.Output()) !void {}
}.callback;
}
pub fn CallbackSignature(comptime self: @This()) type {
return *const fn (UserContext, self.Output()) anyerror!void;
return *const fn (UserContextType, self.Output()) anyerror!void;
}
pub fn Output(comptime self: @This()) type {
@@ -293,7 +372,9 @@ pub fn CommandBuilder(comptime UserContext: type) type {
// global tags and env_vars would conflict, which is less common.
if (param.short_tag) |short|
tag_fields = tag_fields ++ &[_]StructField{.{
.name = short,
// this goofy construct coerces the comptime []const u8 to
// [:0]const u8.
.name = short ++ "",
.type = void,
.default_value = null,
.is_comptime = false,
@@ -302,7 +383,7 @@ pub fn CommandBuilder(comptime UserContext: type) type {
if (param.long_tag) |long|
tag_fields = tag_fields ++ &[_]StructField{.{
.name = long,
.name = long ++ "",
.type = void,
.default_value = null,
.is_comptime = false,
@@ -311,7 +392,7 @@ pub fn CommandBuilder(comptime UserContext: type) type {
if (param.env_var) |env_var|
env_var_fields = env_var_fields ++ &[_]StructField{.{
.name = env_var,
.name = env_var ++ "",
.type = void,
.default_value = null,
.is_comptime = false,
@@ -356,7 +437,7 @@ pub fn CommandBuilder(comptime UserContext: type) type {
const default = if (param.default) |def| &@as(FieldType, def) else @as(?*const anyopaque, null);
fields = fields ++ &[_]StructField{.{
.name = param.name,
.name = param.name ++ "",
.type = FieldType,
.default_value = @ptrCast(default),
.is_comptime = false,
@@ -426,7 +507,7 @@ pub fn CommandBuilder(comptime UserContext: type) type {
?PType.G.IntermediateType();
fields = &(@as([fields.len]StructField, fields[0..fields.len].*) ++ [1]StructField{.{
.name = param.name,
.name = param.name ++ "",
.type = FieldType,
.default_value = @ptrCast(&@as(
FieldType,

View File

@@ -61,7 +61,7 @@ fn MultiConverter(comptime gen: ParameterGenerics) ?ConverterSignature(gen) {
fn FlagConverter(comptime gen: ParameterGenerics) ConverterSignature(gen) {
return struct {
pub fn handler(_: gen.UserContext, input: []const u8, _: ErrorWriter) ConversionError!bool {
pub fn handler(_: gen.UserContext, input: [:0]const u8, _: ErrorWriter) ConversionError!bool {
// treat an empty string as falsy
if (input.len == 0) return false;
@@ -81,7 +81,7 @@ fn FlagConverter(comptime gen: ParameterGenerics) ConverterSignature(gen) {
fn StringConverter(comptime gen: ParameterGenerics) ConverterSignature(gen) {
return struct {
pub fn handler(_: gen.UserContext, input: []const u8, _: ErrorWriter) ConversionError![]const u8 {
pub fn handler(_: gen.UserContext, input: [:0]const u8, _: ErrorWriter) ConversionError![:0]const u8 {
return input;
}
}.handler;
@@ -91,7 +91,7 @@ fn IntConverter(comptime gen: ParameterGenerics) ConverterSignature(gen) {
const IntType = gen.OutputType;
return struct {
pub fn handler(_: gen.UserContext, input: []const u8, failure: ErrorWriter) ConversionError!IntType {
pub fn handler(_: gen.UserContext, input: [:0]const u8, failure: ErrorWriter) ConversionError!IntType {
return std.fmt.parseInt(IntType, input, 0) catch {
try failure.print("cannot interpret \"{s}\" as an integer", .{input});
return ConversionError.ConversionFailed;
@@ -137,7 +137,7 @@ fn ChoiceConverter(comptime gen: ParameterGenerics) ConverterSignature(gen) {
const EnumType = gen.OutputType;
return struct {
pub fn handler(_: gen.UserContext, input: []const u8, failure: ErrorWriter) ConversionError!EnumType {
pub fn handler(_: gen.UserContext, input: [:0]const u8, failure: ErrorWriter) ConversionError!EnumType {
return std.meta.stringToEnum(gen.ConvertedType(), input) orelse {
try failure.print("\"{s}\" is not a valid choice", .{input});
return ConversionError.ConversionFailed;

View File

@@ -12,6 +12,7 @@ pub const ParseError = error{
UnknownLongTagParameter,
UnknownShortTagParameter,
RequiredParameterMissing,
OutOfMemory,
};
pub const NoclipError = ParseError || ConversionError;

View File

@@ -53,6 +53,8 @@ pub fn StructuredPrinter(comptime Writer: type) type {
// TODO: lol return a real error
if (indent >= self.wrap_width) return NoclipError.UnexpectedFailure;
if (text.len == 0) return;
// this assumes output stream has already had the first line properly
// indented.
var splitter = std.mem.split(u8, text, "\n");
@@ -63,6 +65,17 @@ pub fn StructuredPrinter(comptime Writer: type) type {
// we have a trailing line that needs to be cleaned up
if (location > indent)
_ = try self.clearLine(indent);
location = try self.clearLine(indent);
continue;
}
if (line[0] == '>') maybe: {
if (line.len > 1) {
if (line[1] == ' ') {
try self.writer.writeAll(line[2..]);
} else break :maybe;
}
location = try self.clearLine(indent);
continue;
}
@@ -99,6 +112,7 @@ pub fn StructuredPrinter(comptime Writer: type) type {
}
if (location > indent)
try self.writer.writeByte(' ');
try self.writer.writeAll(choppee[0..split]);
location = try self.clearLine(indent);
choppee = choppee[split + 1 ..];
@@ -227,8 +241,6 @@ pub fn HelpBuilder(comptime command: anytype) type {
var just: usize = 0;
inline for (comptime help_info.arguments) |arg| {
if (comptime arg.description.len == 0) continue;
const pair: AlignablePair = .{
.left = arg.name,
.right = arg.description,
@@ -348,11 +360,10 @@ pub fn HelpBuilder(comptime command: anytype) type {
defer pairs.deinit();
var just: usize = 0;
var iter = subcommands.keyIterator();
while (iter.next()) |key| {
for (subcommands.keys()) |key| {
const pair: AlignablePair = .{
.left = key.*,
.right = subcommands.get(key.*).?.describe(),
.left = key,
.right = subcommands.get(key).?.describe(),
};
if (pair.left.len > just) just = pair.left.len;
try pairs.append(pair);

View File

@@ -174,10 +174,43 @@ pub fn SliceIterator(comptime T: type) type {
};
}
pub fn MutatingZSplitter(comptime T: type) type {
return struct {
buffer: [:0]T,
delimiter: T,
index: ?usize = 0,
const Self = @This();
/// Returns a slice of the next field, or null if splitting is complete.
pub fn next(self: *Self) ?[:0]T {
const start = self.index orelse return null;
const end = if (std.mem.indexOfScalarPos(T, self.buffer, start, self.delimiter)) |delim_idx| blk: {
self.buffer[delim_idx] = 0;
self.index = delim_idx + 1;
break :blk delim_idx;
} else blk: {
self.index = null;
break :blk self.buffer.len;
};
return self.buffer[start..end :0];
}
/// Returns a slice of the remaining bytes. Does not affect iterator state.
pub fn rest(self: Self) [:0]T {
const end = self.buffer.len;
const start = self.index orelse end;
return self.buffer[start..end :0];
}
};
}
pub fn copyStruct(comptime T: type, source: T, field_overrides: anytype) T {
var result: T = undefined;
comptime inline for (@typeInfo(@TypeOf(field_overrides)).Struct.fields) |field| {
comptime for (@typeInfo(@TypeOf(field_overrides)).Struct.fields) |field| {
if (!@hasField(T, field.name)) @compileError("override contains bad field" ++ field);
};
@@ -221,9 +254,8 @@ pub const TupleBuilder = struct {
comptime {
var fields: [self.types.len]StructField = undefined;
for (self.types, 0..) |Type, idx| {
var num_buf: [128]u8 = undefined;
fields[idx] = .{
.name = std.fmt.bufPrint(&num_buf, "{d}", .{idx}) catch @compileError("failed to write field"),
.name = std.fmt.comptimePrint("{d}", .{idx}),
.type = Type,
.default_value = null,
// TODO: is this the right thing to do?

View File

@@ -7,4 +7,5 @@ pub const parameters = @import("./parameters.zig");
pub const parser = @import("./parser.zig");
pub const CommandBuilder = command.CommandBuilder;
pub const commandGroup = command.commandGroup;
pub const ParserInterface = parser.ParserInterface;

View File

@@ -18,7 +18,7 @@ pub const FlagBias = enum {
truthy,
unbiased,
pub fn string(comptime self: @This()) []const u8 {
pub fn string(comptime self: @This()) [:0]const u8 {
return switch (comptime self) {
.truthy => "true",
.falsy => "false",
@@ -110,12 +110,12 @@ pub const ParameterGenerics = struct {
pub fn IntermediateValue(comptime self: @This()) type {
return comptime switch (self.value_count) {
.flag => []const u8,
.flag => [:0]const u8,
.count => usize,
.fixed => |count| switch (count) {
0 => @compileError("bad fixed-zero parameter"),
1 => []const u8,
else => std.ArrayList([]const u8),
1 => [:0]const u8,
else => std.ArrayList([:0]const u8),
},
};
}
@@ -223,6 +223,26 @@ fn OptionType(comptime generics: ParameterGenerics) type {
/// want weird things to happen.
flag_bias: FlagBias,
pub fn describe(self: @This(), allocator: std.mem.Allocator) std.mem.Allocator.Error![]const u8 {
var buf = std.ArrayList(u8).init(allocator);
try buf.append('"');
try buf.appendSlice(self.name);
try buf.append('"');
if (self.short_tag != null or self.long_tag != null) {
try buf.appendSlice(" (");
if (self.short_tag) |short|
try buf.appendSlice(short);
if (self.short_tag != null and self.long_tag != null)
try buf.appendSlice(", ");
if (self.long_tag) |long|
try buf.appendSlice(long);
try buf.append(')');
}
return try buf.toOwnedSlice();
}
pub fn IntermediateValue(comptime _: @This()) type {
return generics.IntermediateValue();
}

View File

@@ -10,9 +10,11 @@ const NoclipError = errors.NoclipError;
pub const ParserInterface = struct {
const Vtable = struct {
execute: *const fn (parser: *anyopaque, context: *anyopaque) anyerror!void,
parse: *const fn (parser: *anyopaque, context: *anyopaque, name: []const u8, args: [][:0]u8, env: std.process.EnvMap) anyerror!void,
finish: *const fn (parser: *anyopaque, context: *anyopaque) anyerror!void,
getChild: *const fn (parser: *anyopaque, name: []const u8) ?ParserInterface,
parse: *const fn (parser: *anyopaque, context: *anyopaque, name: []const u8, args: [][:0]u8, env: std.process.EnvMap) anyerror!?ParseResult,
finish: *const fn (parser: *anyopaque, context: *anyopaque) anyerror!?ParserInterface,
getParseError: *const fn (parser: *anyopaque) []const u8,
addSubcommand: *const fn (parser: *anyopaque, name: []const u8, subcommand: ParserInterface) std.mem.Allocator.Error!void,
getSubcommand: *const fn (parser: *anyopaque, name: []const u8) ?ParserInterface,
describe: *const fn () []const u8,
deinit: *const fn (parser: *anyopaque) void,
deinitTree: *const fn (parser: *anyopaque) void,
@@ -30,7 +32,9 @@ pub const ParserInterface = struct {
.execute = ParserType._wrapExecute,
.parse = ParserType._wrapParse,
.finish = ParserType._wrapFinish,
.getChild = ParserType._wrapGetChild,
.getParseError = ParserType._wrapGetParseError,
.addSubcommand = ParserType._wrapAddSubcommand,
.getSubcommand = ParserType._wrapGetSubcommand,
.describe = ParserType._wrapDescribe,
.deinit = ParserType._wrapDeinit,
.deinitTree = ParserType._wrapDeinitTree,
@@ -42,16 +46,24 @@ pub const ParserInterface = struct {
return try self.methods.execute(self.parser, self.context);
}
pub fn parse(self: @This(), name: []const u8, args: [][:0]u8, env: std.process.EnvMap) anyerror!void {
pub fn parse(self: @This(), name: []const u8, args: [][:0]u8, env: std.process.EnvMap) anyerror!?ParseResult {
return try self.methods.parse(self.parser, self.context, name, args, env);
}
pub fn finish(self: @This()) anyerror!void {
pub fn finish(self: @This()) anyerror!?ParserInterface {
return try self.methods.finish(self.parser, self.context);
}
pub fn getChild(self: @This(), name: []const u8) ?ParserInterface {
return self.methods.getChild(self.parser, name);
pub fn getParseError(self: @This()) []const u8 {
return self.methods.getParseError(self.parser);
}
pub fn addSubcommand(self: @This(), name: []const u8, subcommand: ParserInterface) std.mem.Allocator.Error!void {
return try self.methods.addSubcommand(self.parser, name, subcommand);
}
pub fn getSubcommand(self: @This(), name: []const u8) ?ParserInterface {
return self.methods.getSubcommand(self.parser, name);
}
pub fn describe(self: @This()) []const u8 {
@@ -67,89 +79,8 @@ pub const ParserInterface = struct {
}
};
fn InterfaceWrappers(comptime ParserType: type) type {
return struct {
inline fn castInterfaceParser(parser: *anyopaque) *ParserType {
return @ptrCast(@alignCast(parser));
}
fn _wrapExecute(parser: *anyopaque, ctx: *anyopaque) anyerror!void {
const self = castInterfaceParser(parser);
const context = self.castContext(ctx);
return try self.execute(context);
}
fn _wrapParse(
parser: *anyopaque,
ctx: *anyopaque,
name: []const u8,
args: [][:0]u8,
env: std.process.EnvMap,
) anyerror!void {
const self = castInterfaceParser(parser);
const context = self.castContext(ctx);
return try self.subparse(context, name, args, env);
}
fn _wrapFinish(parser: *anyopaque, ctx: *anyopaque) anyerror!void {
const self = castInterfaceParser(parser);
const context = self.castContext(ctx);
return try self.finish(context);
}
fn _wrapGetChild(parser: *anyopaque, name: []const u8) ?ParserInterface {
const self = castInterfaceParser(parser);
return self.getChild(name);
}
fn _wrapDeinit(parser: *anyopaque) void {
const self = castInterfaceParser(parser);
self.deinit();
}
fn _wrapDeinitTree(parser: *anyopaque) void {
const self = castInterfaceParser(parser);
self.deinitTree();
}
fn _wrapDescribe() []const u8 {
return ParserType.command_description;
}
};
}
fn InterfaceGen(comptime ParserType: type, comptime UserContext: type) type {
const CtxInfo = @typeInfo(UserContext);
return if (CtxInfo == .Void) struct {
pub fn interface(self: *ParserType) ParserInterface {
return ParserInterface.create(ParserType, self, @constCast(&void{}));
}
fn castContext(_: ParserType, _: *anyopaque) void {
return void{};
}
} else if (CtxInfo == .Pointer and CtxInfo.Pointer.size != .Slice) struct {
pub fn interface(self: *ParserType, context: UserContext) ParserInterface {
return ParserInterface.create(ParserType, self, @constCast(context));
}
fn castContext(_: ParserType, ctx: *anyopaque) UserContext {
return @ptrCast(@alignCast(ctx));
}
} else struct {
pub fn interface(self: *ParserType, context: *const UserContext) ParserInterface {
return ParserInterface.create(ParserType, self, @ptrCast(@constCast(context)));
}
fn castContext(_: ParserType, ctx: *anyopaque) UserContext {
return @as(*const UserContext, @ptrCast(@alignCast(ctx))).*;
}
};
}
pub const CommandMap = std.hash_map.StringHashMap(ParserInterface);
pub const CommandMap = std.StringArrayHashMap(ParserInterface);
const ParseResult = struct { name: []const u8, args: [][:0]u8, parser: ParserInterface };
// the parser is generated by the bind method of the CommandBuilder, so we can
// be extremely type-sloppy here, which simplifies the signature.
@@ -171,41 +102,103 @@ pub fn Parser(comptime command: anytype, comptime callback: anytype) type {
allocator: std.mem.Allocator,
subcommands: CommandMap,
subcommand: ?ParserInterface = null,
error_message: std.ArrayListUnmanaged(u8) = .{},
help_builder: help.HelpBuilder(command),
pub fn addSubcommand(self: *@This(), verb: []const u8, parser: ParserInterface) !void {
try self.subcommands.put(verb, parser);
}
// This is a slightly annoying hack to work around the fact that there's no way to
// provide a method signature conditionally.
pub usingnamespace InterfaceGen(@This(), UserContext);
// This is a slightly annoying hack to work around the fact that there's no way
// to provide a method signature conditionally.
pub usingnamespace InterfaceGen(@This(), @TypeOf(command).ICC);
// This is attached to the struct this way because these are all "private"
// methods that exist exclusively to cast the type-erased interface object back
// into something usable. Their implementations aren't meaningful and just
// cognitively clutter this struct.
pub usingnamespace InterfaceWrappers(@This());
pub fn subparse(self: *@This(), context: UserContext, name: []const u8, args: [][:0]u8, env: std.process.EnvMap) anyerror!void {
pub fn execute(self: *@This(), context: UserContext) anyerror!void {
const args = std.process.argsAlloc(self.allocator) catch |err| {
try self.error_message.appendSlice(
self.allocator,
"Failed to allocate process arg vector\n",
);
return err;
};
const env = std.process.getEnvMap(self.allocator) catch |err| {
try self.error_message.appendSlice(
self.allocator,
"Failed to allocate process environment variable map\n",
);
return err;
};
if (args.len < 1) {
try self.error_message.appendSlice(
self.allocator,
"The argument list for the base CLI entry point is empty.\n",
);
return ParseError.EmptyArgs;
}
self.progname = std.fs.path.basename(args[0]);
{
var subc = try self.subparse(context, self.progname.?, args[1..], env);
while (subc) |next| {
subc = next.parser.parse(next.name, next.args, env) catch |err| {
try self.error_message.appendSlice(self.allocator, next.parser.getParseError());
return err;
};
}
}
{
var subc = try self.finish(context);
while (subc) |next| {
subc = next.finish() catch |err| {
try self.error_message.appendSlice(self.allocator, next.getParseError());
return err;
};
}
}
}
pub fn subparse(
self: *@This(),
context: UserContext,
name: []const u8,
args: [][:0]u8,
env: std.process.EnvMap,
) anyerror!?ParseResult {
const sliceto = try self.parse(name, args);
try self.readEnvironment(env);
try self.convertEager(context);
if (self.subcommand) |verb| {
const verbname = try std.mem.join(
if (self.subcommand) |subcommand| {
const grafted_name = try std.mem.join(
self.allocator,
" ",
&[_][]const u8{ name, args[sliceto - 1] },
&.{ name, args[sliceto - 1] },
);
try verb.parse(verbname, args[sliceto..], env);
return .{ .name = grafted_name, .args = args[sliceto..], .parser = subcommand };
} else if (self.subcommands.count() > 0 and command.subcommand_required) {
const stderr = std.io.getStdErr().writer();
try stderr.writeAll("A subcommand is required.\n\n");
try stderr.print("'{s}' requires a subcommand.\n\n", .{name});
self.printHelp(name);
}
return null;
}
pub fn finish(self: *@This(), context: UserContext) anyerror!void {
pub fn finish(self: *@This(), context: UserContext) anyerror!?ParserInterface {
try self.convert(context);
try callback(context, self.output);
if (self.subcommand) |verb| try verb.finish();
return self.subcommand;
}
pub fn getParseError(self: @This()) []const u8 {
return if (self.error_message.items.len == 0)
"An unexpected error occurred.\n"
else
self.error_message.items;
}
pub fn deinit(self: @This()) void {
@@ -214,27 +207,18 @@ pub fn Parser(comptime command: anytype, comptime callback: anytype) type {
}
pub fn deinitTree(self: @This()) void {
var iterator = self.subcommands.valueIterator();
while (iterator.next()) |subcommand| {
for (self.subcommands.values()) |subcommand| {
subcommand.deinitTree();
}
self.deinit();
}
pub fn getChild(self: @This(), name: []const u8) ?ParserInterface {
return self.subcommands.get(name);
pub fn addSubcommand(self: *@This(), name: []const u8, parser: ParserInterface) !void {
try self.subcommands.put(name, parser);
}
pub fn execute(self: *@This(), context: UserContext) anyerror!void {
const args = try std.process.argsAlloc(self.allocator);
var env = try std.process.getEnvMap(self.allocator);
if (args.len < 1) return ParseError.EmptyArgs;
self.progname = std.fs.path.basename(args[0]);
try self.subparse(context, self.progname.?, args[1..], env);
try self.finish(context);
pub fn getSubcommand(self: @This(), name: []const u8) ?ParserInterface {
return self.subcommands.get(name);
}
fn printValue(self: @This(), value: anytype, comptime indent: []const u8) void {
@@ -309,8 +293,8 @@ pub fn Parser(comptime command: anytype, comptime callback: anytype) type {
forced_ordinal = true;
}
if (try self.parseOrdinals(arg, &argit)) |verb| {
self.subcommand = verb;
if (try self.parseOrdinals(arg, &argit)) |subcommand| {
self.subcommand = subcommand;
// TODO: return slice of remaining or offset index
return argit.index;
}
@@ -322,7 +306,7 @@ pub fn Parser(comptime command: anytype, comptime callback: anytype) type {
fn parseLongTag(
self: *@This(),
name: []const u8,
arg: []const u8,
arg: [:0]u8,
argit: *ncmeta.SliceIterator([][:0]u8),
) ParseError!void {
if (comptime command.help_flag.long_tag) |long|
@@ -346,6 +330,10 @@ pub fn Parser(comptime command: anytype, comptime callback: anytype) type {
}
}
try self.error_message.writer(self.allocator).print(
"Could not parse command line: unknown option \"{s}\"\n",
.{arg},
);
return ParseError.UnknownLongTagParameter;
}
@@ -368,20 +356,29 @@ pub fn Parser(comptime command: anytype, comptime callback: anytype) type {
if (arg == tag[1]) {
if (comptime !PType.is_flag)
if (remaining > 0)
if (remaining > 0) {
try self.error_message.writer(self.allocator).print(
"Could not parse command line: \"-{c}\" is fused to another flag, but it requires a value\n",
.{arg},
);
return ParseError.FusedShortTagValueMissing;
};
try self.applyParamValues(param, argit, false);
return;
}
}
try self.error_message.writer(self.allocator).print(
"Could not parse command line: unknown option \"-{c}\"\n",
.{arg},
);
return ParseError.UnknownShortTagParameter;
}
fn parseOrdinals(
self: *@This(),
arg: []const u8,
arg: [:0]u8,
argit: *ncmeta.SliceIterator([][:0]u8),
) ParseError!?ParserInterface {
comptime var arg_index: u32 = 0;
@@ -402,7 +399,20 @@ pub fn Parser(comptime command: anytype, comptime callback: anytype) type {
arg_index += 1;
}
return self.subcommands.get(arg) orelse ParseError.ExtraValue;
return self.subcommands.get(arg) orelse {
const writer = self.error_message.writer(self.allocator);
if (self.subcommands.count() > 0)
try writer.print(
"Could not parse command line: unknown subcommand \"{s}\"\n",
.{arg},
)
else
try writer.print(
"Could not parse command line: unexpected extra argument \"{s}\"\n",
.{arg},
);
return ParseError.ExtraValue;
};
}
fn pushIntermediateValue(
@@ -417,7 +427,7 @@ pub fn Parser(comptime command: anytype, comptime callback: anytype) type {
if (@field(self.intermediate, param.name) == null) {
@field(self.intermediate, param.name) = gen.IntermediateType().init(self.allocator);
}
@field(self.intermediate, param.name).?.append(value) catch return ParseError.UnexpectedFailure;
try @field(self.intermediate, param.name).?.append(value);
} else if (comptime @TypeOf(param).G.nonscalar()) {
if (@field(self.intermediate, param.name)) |list| list.deinit();
@field(self.intermediate, param.name) = value;
@@ -436,18 +446,54 @@ pub fn Parser(comptime command: anytype, comptime callback: anytype) type {
.flag => try self.pushIntermediateValue(param, comptime param.flag_bias.string()),
.count => @field(self.intermediate, param.name) += 1,
.fixed => |count| switch (count) {
0 => return ParseError.ExtraValue,
1 => try self.pushIntermediateValue(param, argit.next() orelse return ParseError.MissingValue),
0 => {
const writer = self.error_message.writer(self.allocator);
const desc = try param.describe(self.allocator);
defer self.allocator.free(desc);
try writer.print(
"Could not parse command line: {s} takes no value.\n",
.{desc},
);
return ParseError.ExtraValue;
},
1 => try self.pushIntermediateValue(param, argit.next() orelse {
const writer = self.error_message.writer(self.allocator);
const desc = try param.describe(self.allocator);
defer self.allocator.free(desc);
try writer.print(
"Could not parse command line: {s} requires a value.\n",
.{desc},
);
return ParseError.MissingValue;
}),
else => |total| {
var list = std.ArrayList([]const u8).initCapacity(self.allocator, total) catch
return ParseError.UnexpectedFailure;
var list = try std.ArrayList([:0]const u8).initCapacity(self.allocator, total);
var consumed: u32 = 0;
while (consumed < total) : (consumed += 1) {
const next = argit.next() orelse return ParseError.MissingValue;
list.append(next) catch return ParseError.UnexpectedFailure;
const next = argit.next() orelse {
const writer = self.error_message.writer(self.allocator);
const desc = try param.describe(self.allocator);
defer self.allocator.free(desc);
try writer.print(
"Could not parse command line: {s} is missing one or more values (need {d}, got {d}).\n",
.{ desc, total, consumed },
);
return ParseError.MissingValue;
};
list.appendAssumeCapacity(next);
}
if (bounded and argit.next() != null) {
const writer = self.error_message.writer(self.allocator);
const desc = try param.describe(self.allocator);
defer self.allocator.free(desc);
try writer.print(
"Could not parse command line: {s} has too many values (need {d}).\n",
.{ desc, total },
);
return ParseError.ExtraValue;
}
if (bounded and argit.next() != null) return ParseError.ExtraValue;
try self.pushIntermediateValue(param, list);
},
@@ -458,9 +504,9 @@ pub fn Parser(comptime command: anytype, comptime callback: anytype) type {
fn applyFusedValues(
self: *@This(),
comptime param: anytype,
value: []const u8,
value: [:0]u8,
) ParseError!void {
var iter = std.mem.split(u8, value, ",");
var iter = ncmeta.MutatingZSplitter(u8){ .buffer = value, .delimiter = ',' };
return try self.applyParamValues(param, &iter, true);
}
@@ -468,7 +514,9 @@ pub fn Parser(comptime command: anytype, comptime callback: anytype) type {
inline for (comptime parameters) |param| {
if (comptime param.env_var) |env_var| blk: {
if (@field(self.intermediate, param.name) != null) break :blk;
const val = env.get(env_var) orelse break :blk;
const val = try self.allocator.dupeZ(u8, env.get(env_var) orelse break :blk);
if (comptime @TypeOf(param).G.value_count == .flag) {
try self.pushIntermediateValue(param, val);
} else {
@@ -501,19 +549,27 @@ pub fn Parser(comptime command: anytype, comptime callback: anytype) type {
if (comptime @TypeOf(param).has_output) {
@field(self.output, param.name) = param.converter(context, intermediate, writer) catch |err| {
const stderr = std.io.getStdErr().writer();
stderr.print("Error parsing option \"{s}\": {s}\n", .{ param.name, buffer.items }) catch {};
const err_writer = self.error_message.writer(self.allocator);
const desc = try param.describe(self.allocator);
defer self.allocator.free(desc);
try err_writer.print("Error parsing option {s}: {s}\n", .{ desc, buffer.items });
return err;
};
} else {
param.converter(context, intermediate, writer) catch |err| {
const stderr = std.io.getStdErr().writer();
stderr.print("Error parsing option \"{s}\": {s}\n", .{ param.name, buffer.items }) catch {};
const err_writer = self.error_message.writer(self.allocator);
const desc = try param.describe(self.allocator);
defer self.allocator.free(desc);
try err_writer.print("Error parsing option {s}: {s}\n", .{ desc, buffer.items });
return err;
};
}
} else {
if (comptime param.required) {
const err_writer = self.error_message.writer(self.allocator);
const desc = try param.describe(self.allocator);
defer self.allocator.free(desc);
try err_writer.print("Could not parse command line: required parameter {s} is missing\n", .{desc});
return ParseError.RequiredParameterMissing;
} else if (comptime @TypeOf(param).has_output) {
if (comptime param.default) |def| {
@@ -540,3 +596,98 @@ pub fn Parser(comptime command: anytype, comptime callback: anytype) type {
}
};
}
fn InterfaceWrappers(comptime ParserType: type) type {
return struct {
inline fn castInterfaceParser(parser: *anyopaque) *ParserType {
return @ptrCast(@alignCast(parser));
}
fn _wrapExecute(parser: *anyopaque, ctx: *anyopaque) anyerror!void {
const self = castInterfaceParser(parser);
const context = self.castContext(ctx);
return try self.execute(context);
}
fn _wrapParse(
parser: *anyopaque,
ctx: *anyopaque,
name: []const u8,
args: [][:0]u8,
env: std.process.EnvMap,
) anyerror!?ParseResult {
const self = castInterfaceParser(parser);
const context = self.castContext(ctx);
return try self.subparse(context, name, args, env);
}
fn _wrapFinish(parser: *anyopaque, ctx: *anyopaque) anyerror!?ParserInterface {
const self = castInterfaceParser(parser);
const context = self.castContext(ctx);
return try self.finish(context);
}
fn _wrapGetParseError(parser: *anyopaque) []const u8 {
const self = castInterfaceParser(parser);
return self.getParseError();
}
fn _wrapAddSubcommand(parser: *anyopaque, name: []const u8, subcommand: ParserInterface) !void {
const self = castInterfaceParser(parser);
return self.addSubcommand(name, subcommand);
}
fn _wrapGetSubcommand(parser: *anyopaque, name: []const u8) ?ParserInterface {
const self = castInterfaceParser(parser);
return self.getSubcommand(name);
}
fn _wrapDeinit(parser: *anyopaque) void {
const self = castInterfaceParser(parser);
self.deinit();
}
fn _wrapDeinitTree(parser: *anyopaque) void {
const self = castInterfaceParser(parser);
self.deinitTree();
}
fn _wrapDescribe() []const u8 {
return ParserType.command_description;
}
};
}
// TODO: figure out a better way of consolidating this logic with that in command.zig?
fn InterfaceGen(comptime ParserType: type, comptime ICC: anytype) type {
return switch (ICC) {
.empty => struct {
pub fn interface(self: *ParserType) ParserInterface {
return ParserInterface.create(ParserType, self, @constCast(&void{}));
}
fn castContext(_: ParserType, _: *anyopaque) void {
return void{};
}
},
.pointer => struct {
pub fn interface(self: *ParserType, context: ICC.InputType().?) ParserInterface {
return ParserInterface.create(ParserType, self, @constCast(context));
}
fn castContext(_: ParserType, ctx: *anyopaque) ICC.OutputType() {
return @ptrCast(@alignCast(ctx));
}
},
.value => struct {
pub fn interface(self: *ParserType, context: ICC.InputType().?) ParserInterface {
return ParserInterface.create(ParserType, self, @ptrCast(@constCast(context)));
}
fn castContext(_: ParserType, ctx: *anyopaque) ICC.OutputType() {
return @as(ICC.InputType().?, @ptrCast(@alignCast(ctx))).*;
}
},
};
}