diff --git a/demo/demo.zig b/demo/demo.zig index 7be2a99..e5a5c13 100644 --- a/demo/demo.zig +++ b/demo/demo.zig @@ -8,15 +8,15 @@ 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); - \\> } + \\> // 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 @@ -135,6 +135,16 @@ pub fn main() !u8 { try base.addSubcommand("main", try cli.createInterface(allocator, cliHandler, &context)); 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(); return 0; diff --git a/source/parser.zig b/source/parser.zig index 64e8dca..cb1d38c 100644 --- a/source/parser.zig +++ b/source/parser.zig @@ -10,8 +10,8 @@ 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, + 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, 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, @@ -44,11 +44,11 @@ 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); } @@ -74,6 +74,7 @@ pub const ParserInterface = struct { }; 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. @@ -106,13 +107,35 @@ pub fn Parser(comptime command: anytype, comptime callback: anytype) type { // cognitively clutter this struct. 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( self: *@This(), context: UserContext, name: []const u8, args: [][:0]u8, env: std.process.EnvMap, - ) anyerror!void { + ) anyerror!?ParseResult { const sliceto = try self.parse(name, args); try self.readEnvironment(env); 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( 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) { 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) |subcommand| try subcommand.finish(); + return self.subcommand; } 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); } - 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 { if (comptime @hasField(@TypeOf(value), "items")) { std.debug.print("{s}[\n", .{indent}); @@ -497,13 +510,13 @@ fn InterfaceWrappers(comptime ParserType: type) type { name: []const u8, args: [][:0]u8, env: std.process.EnvMap, - ) anyerror!void { + ) 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!void { + fn _wrapFinish(parser: *anyopaque, ctx: *anyopaque) anyerror!?ParserInterface { const self = castInterfaceParser(parser); const context = self.castContext(ctx); return try self.finish(context);