const std = @import("std"); const io = std.io; const mem = std.mem; const process = std.process; const ArrayList = std.ArrayList; const StringHashMap = std.StringHashMap; const util = @import("./util.zig"); pub const Option = struct { short: []const u8, long: []const u8, optional: bool, global: bool, description: []const u8, value_string: []const u8 = "", value_int: i32 = 0, value_float: f32 = 0.0, value_bool: bool = false, value_type: ValueType, const ValueType = enum { STRING, BOOL, INT, FLOAT, }; }; pub const Command = struct { const Commands = StringHashMap(Command); const Options = StringHashMap(Option); const Args = ArrayList([]const u8); pub const CommandConfig = struct { name: []const u8, short_description: []const u8, long_description: []const u8, usage: []const u8, version: []const u8 = "0.0.1", run: *const fn (cmd: *Command) void = undefined, }; subcommands: Commands, options: Options, arguments: Args, cmd_args: Args, allocator: mem.Allocator, config: CommandConfig, pub fn init(allocator: mem.Allocator, args: CommandConfig) Command { return Command{ .subcommands = Commands.init(allocator), .options = Options.init(allocator), .arguments = Args.init(allocator), .cmd_args = Args.init(allocator), .allocator = allocator, .config = args, }; } pub fn deinit(self: *Command) void { self.subcommands.deinit(); self.options.deinit(); self.arguments.deinit(); self.cmd_args.deinit(); } pub fn addOption(self: *Command, option: Option) !void { try self.options.put(option.long, option); } pub fn addCommand(self: *Command, command: Command) !void { try self.subcommands.put(command.config.name, command); } pub fn help(self: *Command) !void { const description = if (self.config.long_description.len > 0) self.config.long_description else if (self.config.short_description.len > 0) self.config.short_description 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"); if (self.subcommands.count() > 0) { try builder.appendSlice("Subcommands: \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 builder.appendSlice("\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); if (!option.optional) { try builder.appendSlice(" (required)"); } 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 parse(self: *Command) !void { var i: usize = 0; var current_command = self.*; var current_option: Option = undefined; var parsing_option = false; parse_while_blk: while (i < self.cmd_args.items.len) { const current_arg = self.cmd_args.items[i]; if (!parsing_option) { if (current_command.subcommands.get(current_arg)) |subcommand| { current_command = subcommand; i += 1; continue :parse_while_blk; } } var entry_iterator = current_command.options.iterator(); while (entry_iterator.next()) |entry| { const option = entry.value_ptr.*; if (parsing_option) { switch (current_option.value_type) { .STRING => current_option.value_string = current_arg, .INT => current_option.value_int = util.strToInt(current_arg.ptr), .FLOAT => current_option.value_float = util.strToFloat(current_arg.ptr), else => {}, } try self.options.put(entry.key_ptr.*, 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; } } try self.arguments.append(current_arg); i += 1; } current_command.config.run(¤t_command); } pub fn execute(self: *Command) !void { var arg_iterator = process.args(); while (arg_iterator.next()) |arg| { try self.cmd_args.append(arg); } try self.parse(); } pub fn getBoolOption(self: *Command, option_name: []const u8) bool { return if (self.options.get(option_name)) |option| option.value_bool else false; } pub fn getStringOption(self: *Command, option_name: []const u8) []const u8 { return if (self.options.get(option_name)) |option| option.value_string else ""; } pub fn getIntOption(self: *Command, option_name: []const u8) i32 { return if (self.options.get(option_name)) |option| option.value_int else 0; } pub fn getFloatOption(self: *Command, option_name: []const u8) f32 { return if (self.options.get(option_name)) |option| option.value_float else 0.0; } };