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
This commit is contained in:
torque 2024-04-06 11:38:55 -07:00
parent b77a1f59c2
commit 5f45290423
Signed by: torque
SSH Key Fingerprint: SHA256:nCrXefBNo6EbjNSQhv0nXmEg/VuNq3sMF5b8zETw3Tk
2 changed files with 53 additions and 30 deletions

View File

@ -8,15 +8,15 @@ const Choice = enum { first, second };
const cli = cmd: { const cli = cmd: {
var cmd = CommandBuilder(*u32){ var cmd = CommandBuilder(*u32){
.description = .description =
\\The definitive noclip demonstration utility \\The definitive noclip demonstration utility.
\\ \\
\\This command demonstrates the functionality of the noclip library. cool! \\This command demonstrates the functionality of the noclip library. cool!
\\ \\
\\> // implementing factorial recursively is a silly thing to do \\> // implementing factorial recursively is a silly thing to do
\\> pub fn fact(n: u64) u64 { \\> pub fn fact(n: u64) u64 {
\\> if (n == 0) return 1; \\> if (n == 0) return 1;
\\> return n*fact(n - 1); \\> return n*fact(n - 1);
\\> } \\> }
\\ \\
\\Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor \\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 \\incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis
@ -135,6 +135,16 @@ pub fn main() !u8 {
try base.addSubcommand("main", try cli.createInterface(allocator, cliHandler, &context)); try base.addSubcommand("main", try cli.createInterface(allocator, cliHandler, &context));
try base.addSubcommand("other", try subcommand.createInterface(allocator, subHandler, &sc)); try base.addSubcommand("other", try subcommand.createInterface(allocator, subHandler, &sc));
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));
try base.execute(); try base.execute();
return 0; return 0;

View File

@ -10,8 +10,8 @@ const NoclipError = errors.NoclipError;
pub const ParserInterface = struct { pub const ParserInterface = struct {
const Vtable = struct { const Vtable = struct {
execute: *const fn (parser: *anyopaque, context: *anyopaque) anyerror!void, 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, 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!void, finish: *const fn (parser: *anyopaque, context: *anyopaque) anyerror!?ParserInterface,
addSubcommand: *const fn (parser: *anyopaque, name: []const u8, subcommand: ParserInterface) std.mem.Allocator.Error!void, addSubcommand: *const fn (parser: *anyopaque, name: []const u8, subcommand: ParserInterface) std.mem.Allocator.Error!void,
getSubcommand: *const fn (parser: *anyopaque, name: []const u8) ?ParserInterface, getSubcommand: *const fn (parser: *anyopaque, name: []const u8) ?ParserInterface,
describe: *const fn () []const u8, describe: *const fn () []const u8,
@ -44,11 +44,11 @@ pub const ParserInterface = struct {
return try self.methods.execute(self.parser, self.context); 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); 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); return try self.methods.finish(self.parser, self.context);
} }
@ -74,6 +74,7 @@ pub const ParserInterface = struct {
}; };
pub const CommandMap = std.StringArrayHashMap(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 // the parser is generated by the bind method of the CommandBuilder, so we can
// be extremely type-sloppy here, which simplifies the signature. // be extremely type-sloppy here, which simplifies the signature.
@ -106,13 +107,35 @@ pub fn Parser(comptime command: anytype, comptime callback: anytype) type {
// cognitively clutter this struct. // cognitively clutter this struct.
pub usingnamespace InterfaceWrappers(@This()); pub usingnamespace InterfaceWrappers(@This());
pub fn execute(self: *@This(), context: UserContext) anyerror!void {
const args = try std.process.argsAlloc(self.allocator);
const env = try std.process.getEnvMap(self.allocator);
if (args.len < 1) 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 = try next.parser.parse(next.name, next.args, env);
}
}
{
var subc = try self.finish(context);
while (subc) |next| {
subc = try next.finish();
}
}
}
pub fn subparse( pub fn subparse(
self: *@This(), self: *@This(),
context: UserContext, context: UserContext,
name: []const u8, name: []const u8,
args: [][:0]u8, args: [][:0]u8,
env: std.process.EnvMap, env: std.process.EnvMap,
) anyerror!void { ) anyerror!?ParseResult {
const sliceto = try self.parse(name, args); const sliceto = try self.parse(name, args);
try self.readEnvironment(env); try self.readEnvironment(env);
try self.convertEager(context); try self.convertEager(context);
@ -121,21 +144,23 @@ pub fn Parser(comptime command: anytype, comptime callback: anytype) type {
const grafted_name = try std.mem.join( const grafted_name = try std.mem.join(
self.allocator, self.allocator,
" ", " ",
&[_][]const u8{ name, args[sliceto - 1] }, &.{ name, args[sliceto - 1] },
); );
try subcommand.parse(grafted_name, args[sliceto..], env); return .{ .name = grafted_name, .args = args[sliceto..], .parser = subcommand };
} else if (self.subcommands.count() > 0 and command.subcommand_required) { } else if (self.subcommands.count() > 0 and command.subcommand_required) {
const stderr = std.io.getStdErr().writer(); 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); 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 self.convert(context);
try callback(context, self.output); try callback(context, self.output);
if (self.subcommand) |subcommand| try subcommand.finish(); return self.subcommand;
} }
pub fn deinit(self: @This()) void { pub fn deinit(self: @This()) void {
@ -158,18 +183,6 @@ pub fn Parser(comptime command: anytype, comptime callback: anytype) type {
return self.subcommands.get(name); return self.subcommands.get(name);
} }
pub fn execute(self: *@This(), context: UserContext) anyerror!void {
const args = try std.process.argsAlloc(self.allocator);
const 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);
}
fn printValue(self: @This(), value: anytype, comptime indent: []const u8) void { fn printValue(self: @This(), value: anytype, comptime indent: []const u8) void {
if (comptime @hasField(@TypeOf(value), "items")) { if (comptime @hasField(@TypeOf(value), "items")) {
std.debug.print("{s}[\n", .{indent}); std.debug.print("{s}[\n", .{indent});
@ -497,13 +510,13 @@ fn InterfaceWrappers(comptime ParserType: type) type {
name: []const u8, name: []const u8,
args: [][:0]u8, args: [][:0]u8,
env: std.process.EnvMap, env: std.process.EnvMap,
) anyerror!void { ) anyerror!?ParseResult {
const self = castInterfaceParser(parser); const self = castInterfaceParser(parser);
const context = self.castContext(ctx); const context = self.castContext(ctx);
return try self.subparse(context, name, args, env); return try self.subparse(context, name, args, env);
} }
fn _wrapFinish(parser: *anyopaque, ctx: *anyopaque) anyerror!void { fn _wrapFinish(parser: *anyopaque, ctx: *anyopaque) anyerror!?ParserInterface {
const self = castInterfaceParser(parser); const self = castInterfaceParser(parser);
const context = self.castContext(ctx); const context = self.castContext(ctx);
return try self.finish(context); return try self.finish(context);