Compare commits

...

4 Commits

Author SHA1 Message Date
419d8994ba
demo: update to use interfaces directly
This adds a group and saves some lines.
2023-09-10 14:54:41 -07:00
a8652f71c4
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 14:52:49 -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
4 changed files with 119 additions and 38 deletions

View File

@ -113,17 +113,16 @@ 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;
try base.execute();
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(),
@ -101,11 +172,13 @@ pub fn CommandBuilder(comptime UserContext: type) type {
};
}
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);
@ -119,7 +192,7 @@ pub fn CommandBuilder(comptime UserContext: type) type {
.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);
@ -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 {

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");
@ -227,8 +229,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,

View File

@ -99,7 +99,7 @@ pub fn Parser(comptime command: anytype, comptime callback: anytype) type {
// 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);
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
@ -536,32 +536,35 @@ fn InterfaceWrappers(comptime ParserType: type) type {
};
}
fn InterfaceGen(comptime ParserType: type, comptime UserContext: type) type {
const CtxInfo = @typeInfo(UserContext);
// 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{}));
}
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{};
}
},
.pointer => struct {
pub fn interface(self: *ParserType, context: ICC.InputType().?) ParserInterface {
return ParserInterface.create(ParserType, self, @constCast(context));
}
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) 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) 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))).*;
}
fn castContext(_: ParserType, ctx: *anyopaque) ICC.OutputType() {
return @as(ICC.InputType().?, @ptrCast(@alignCast(ctx))).*;
}
},
};
}