diff --git a/cli.zig b/cli.zig index 6ee7fde..d1884e4 100644 --- a/cli.zig +++ b/cli.zig @@ -4,19 +4,20 @@ const mem = std.mem; const process = std.process; const ArrayList = std.ArrayList; const StringHashMap = std.StringHashMap; +const out = io.getStdOut().writer(); const util = @import("./util.zig"); pub const Option = struct { short: []const u8, long: []const u8, - optional: bool, - global: bool, + optional: bool = true, + global: bool = false, description: []const u8, value_string: []const u8 = "", value_int: i32 = 0, value_float: f32 = 0.0, value_bool: bool = false, - value_type: ValueType, + value_type: ValueType = .BOOL, const ValueType = enum { STRING, @@ -26,6 +27,20 @@ pub const Option = struct { }; }; +const help_option = Option{ + .short = "h", + .long = "help", + .global = true, + .description = "Print help", +}; + +const version_option = Option{ + .short = "v", + .long = "version", + .global = true, + .description = "Print version", +}; + pub const Command = struct { const Commands = StringHashMap(Command); const Options = StringHashMap(Option); @@ -36,7 +51,7 @@ pub const Command = struct { long_description: []const u8, usage: []const u8, version: []const u8 = "0.0.1", - run: *const fn (cmd: *Command) void = undefined, + run: ?*const fn (cmd: *Command) anyerror!void, }; subcommands: Commands, @@ -46,10 +61,13 @@ pub const Command = struct { allocator: mem.Allocator, config: CommandConfig, - pub fn init(allocator: mem.Allocator, args: CommandConfig) Command { + pub fn init(allocator: mem.Allocator, args: CommandConfig) !Command { + var options = Options.init(allocator); + try options.put(help_option.long, help_option); + try options.put(version_option.long, version_option); return Command{ .subcommands = Commands.init(allocator), - .options = Options.init(allocator), + .options = options, .arguments = Args.init(allocator), .cmd_args = Args.init(allocator), .allocator = allocator, @@ -69,6 +87,13 @@ pub const Command = struct { } pub fn addCommand(self: *Command, command: Command) !void { + var option_iterator = self.options.iterator(); + while (option_iterator.next()) |entry| { + const option = entry.value_ptr.*; + if (option.global) { + try @constCast(&command).addOption(option); + } + } try self.subcommands.put(command.config.name, command); } @@ -80,68 +105,70 @@ pub const Command = struct { else self.config.name; - var builder = ArrayList(u8).init(self.allocator); - defer builder.deinit(); - try builder.appendSlice(description); - try builder.appendSlice("\n\n"); - try builder.appendSlice("Usage: "); - try builder.appendSlice(self.config.usage); - try builder.appendSlice("\n\n"); + try out.print("{s}\n\nUsage: {s}", .{ description, self.config.usage }); + if (self.subcommands.count() > 0) { + try out.print(" [COMMAND]", .{}); + } + try out.print(" [OPTIONS] [ARGUMENTS]\n\n", .{}); if (self.subcommands.count() > 0) { - try builder.appendSlice("Subcommands: \n"); + try out.print("Commands: \n\n", .{}); var entry_iterator = self.subcommands.iterator(); while (entry_iterator.next()) |subcommand| { - try builder.appendSlice(" "); - try builder.appendSlice(subcommand.key_ptr.*); - try builder.appendSlice("\t\t\t"); - try builder.appendSlice(subcommand.value_ptr.*.config.short_description); - try builder.appendSlice("\n"); + try out.print(" {s: <20}{s}\n", .{ subcommand.key_ptr.*, subcommand.value_ptr.*.config.short_description }); } - try builder.appendSlice("\n"); + try out.print("\n", .{}); } + try out.print("Options: \n\n", .{}); + if (self.options.count() > 0) { - try builder.appendSlice("Options: \n"); var entry_iterator = self.options.iterator(); while (entry_iterator.next()) |entry| { const option = entry.value_ptr.*; - try builder.appendSlice(" -"); - try builder.appendSlice(option.short); - try builder.appendSlice(", --"); - try builder.appendSlice(option.long); - try builder.appendSlice("\t\t"); - try builder.appendSlice(option.description); + const flags = try mem.concat(self.allocator, u8, &[_][]const u8{ "-", option.short, ", --", option.long }); + try out.print(" {s: <20}{s}", .{ flags, option.description }); if (!option.optional) { - try builder.appendSlice(" (required)"); + try out.print(" (required)\n", .{}); + } else { + try out.print("\n", .{}); } - try builder.appendSlice("\n"); } } - _ = try io.getStdOut().write(builder.items); } pub fn printVersion(self: *Command) !void { _ = try io.getStdOut().write(try mem.concat(self.allocator, u8, &[_][]const u8{ self.config.version, "\n" })); } + fn defaultRun(self: *Command) !void { + if (self.options.get("help").?.value_bool) { + try self.help(); + } else if (self.options.get("version").?.value_bool) { + try self.printVersion(); + } else if (self.config.run) |run| { + try run(self); + } + } + fn parse(self: *Command) !void { - var i: usize = 0; + var i: usize = 1; var current_command = self.*; var current_option: Option = undefined; var parsing_option = false; + var subcommand_parse_over = false; + var option_parse_over = false; parse_while_blk: while (i < self.cmd_args.items.len) { const current_arg = self.cmd_args.items[i]; - if (!parsing_option) { + if (!subcommand_parse_over) { if (current_command.subcommands.get(current_arg)) |subcommand| { current_command = subcommand; i += 1; continue :parse_while_blk; } + subcommand_parse_over = true; } - var entry_iterator = current_command.options.iterator(); - while (entry_iterator.next()) |entry| { - const option = entry.value_ptr.*; + if (!option_parse_over) { if (parsing_option) { switch (current_option.value_type) { .STRING => current_option.value_string = current_arg, @@ -149,28 +176,34 @@ pub const Command = struct { .FLOAT => current_option.value_float = util.strToFloat(current_arg.ptr), else => {}, } - try self.options.put(entry.key_ptr.*, current_option); + try current_command.options.put(current_option.long, current_option); parsing_option = false; i += 1; continue :parse_while_blk; - } else if (mem.eql(u8, current_arg, try mem.concat(self.allocator, u8, &[_][]const u8{ "-", option.short })) or - mem.eql(u8, current_arg, try mem.concat(self.allocator, u8, &[_][]const u8{ "--", option.long }))) - { - current_option = option; - parsing_option = true; - if (current_option.value_type == .BOOL) { - current_option.value_bool = true; - try self.options.put(entry.key_ptr.*, current_option); - parsing_option = false; - } - i += 1; - continue :parse_while_blk; } + var entry_iterator = current_command.options.iterator(); + while (entry_iterator.next()) |entry| { + const option = entry.value_ptr.*; + if (mem.eql(u8, current_arg, try mem.concat(current_command.allocator, u8, &[_][]const u8{ "-", option.short })) or + mem.eql(u8, current_arg, try mem.concat(current_command.allocator, u8, &[_][]const u8{ "--", option.long }))) + { + current_option = option; + parsing_option = true; + if (current_option.value_type == .BOOL) { + current_option.value_bool = true; + try current_command.options.put(entry.key_ptr.*, current_option); + parsing_option = false; + } + i += 1; + continue :parse_while_blk; + } + } + option_parse_over = true; } - try self.arguments.append(current_arg); + try current_command.arguments.append(current_arg); i += 1; } - current_command.config.run(¤t_command); + try current_command.defaultRun(); } pub fn execute(self: *Command) !void { @@ -178,6 +211,7 @@ pub const Command = struct { while (arg_iterator.next()) |arg| { try self.cmd_args.append(arg); } + self.cmd_args.items[0] = self.config.name; try self.parse(); } diff --git a/demo.zig b/demo.zig index 139b171..c35c4e7 100644 --- a/demo.zig +++ b/demo.zig @@ -4,12 +4,12 @@ const cli = @import("cli"); const Command = cli.Command; const Option = cli.Option; -fn rootRun(cmd: *Command) void { +fn rootRun(cmd: *Command) anyerror!void { const e = cmd.getIntOption("example"); std.debug.print("{d}\n", .{e}); } -fn subcommandRun(cmd: *Command) void { +fn subcommandRun(cmd: *Command) anyerror!void { std.debug.print("{s}\n", .{cmd.config.name}); } @@ -23,24 +23,21 @@ pub fn main() !void { .name = "example", .run = rootRun, }; - var root_command = Command.init(allocator, config); - try root_command.addCommand(Command.init(allocator, .{ + var root_command = try Command.init(allocator, config); + const subcommand = try Command.init(allocator, .{ .short_description = "SubExample", .long_description = "Subcommand example", .usage = "subexample", .name = "subexample", .run = subcommandRun, - })); + }); + try root_command.addCommand(subcommand); try root_command.addOption(Option{ .short = "e", .long = "example", .description = "example option", - .optional = false, - .global = false, .value_type = .INT, .value_int = 3, }); try root_command.execute(); - try root_command.help(); - try root_command.printVersion(); }