From 59e9c24a9ad19d09a77f539cadf4774df25ddbe9 Mon Sep 17 00:00:00 2001 From: PrajwalCH Date: Tue, 25 Oct 2022 23:16:58 +0545 Subject: [PATCH] feat: Implement new settings generator --- src/Arg.zig | 61 ++++++++++++++++++------------------------- src/Command.zig | 43 ++++++++++++++---------------- src/Help.zig | 27 ++++++++++--------- src/main.zig | 11 ++++---- src/parser/Parser.zig | 20 +++++++------- src/settings.zig | 54 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 129 insertions(+), 87 deletions(-) create mode 100644 src/settings.zig diff --git a/src/Arg.zig b/src/Arg.zig index 48c1faf..33a84b8 100644 --- a/src/Arg.zig +++ b/src/Arg.zig @@ -3,12 +3,13 @@ const Arg = @This(); const std = @import("std"); const ArgsContext = @import("parser/ArgsContext.zig"); +const MakeSettings = @import("settings.zig").MakeSettings; -const Settings = struct { - takes_value: bool = false, - takes_multiple_values: bool = false, - allow_empty_value: bool = false, -}; +const Settings = MakeSettings(&[_][]const u8{ + "takes_value", + "takes_multiple_values", + "allow_empty_value", +}); name: []const u8, short_name: ?u8, @@ -57,56 +58,34 @@ pub fn setDescription(self: *Arg, description: []const u8) void { } /// Sets the minimum number of values required to provide for an argument. -/// Implicitly sets the `Arg.takesValue(true)` +/// Implicitly applies the `.takes_value` setting pub fn minValues(self: *Arg, num: usize) void { if (num >= 1) { self.min_values = num; - self.takesValue(true); + self.applySetting(.takes_value); } } /// Sets the maximum number of values an argument can take. -/// Implicitly sets the `Arg.takesValue(true)` +/// Implicitly applies the `.takes_value` setting pub fn maxValues(self: *Arg, num: usize) void { self.max_values = num; - self.takesValue(true); + self.applySetting(.takes_value); } /// Sets the allowed values for an argument. /// Value outside of allowed values will be consider as error. -/// Implicitly sets the `Arg.takesValue(true)` +/// Implicitly applies the `.takes_value` setting pub fn allowedValues(self: *Arg, values: []const []const u8) void { self.allowed_values = values; - self.takesValue(true); + self.applySetting(.takes_value); } /// Sets separator between the values of an argument. -/// Implicitly sets the `Arg.takesValue(true)` +/// Implicitly applies the `.takes_value` setting pub fn valuesDelimiter(self: *Arg, delimiter: []const u8) void { self.values_delimiter = delimiter; - self.takesValue(true); -} - -/// Specifies that an argument will takes a value -pub fn takesValue(self: *Arg, b: bool) void { - self.settings.takes_value = b; -} - -/// Specifies that the argument will takes an unknown number of values. -/// You can use `Arg.maxValues(n)` to limit the number of values. -/// -/// Note: -/// Values will continue to be consume until one of the following condition wiil satisfies: -/// 1. If another flag is found -/// 2. If parser reaches the end of raw argument -/// 3. If parser reaches the maximum number of values. Requires to explicitly set `Arg.maxValues(n)` -pub fn takesMultipleValues(self: *Arg, b: bool) void { - self.settings.takes_multiple_values = b; -} - -/// Specifies wether an argument can take a empty value or not -pub fn allowedEmptyValue(self: *Arg, b: bool) void { - self.settings.allow_empty_value = b; + self.applySetting(.takes_value); } pub fn verifyValueInAllowedValues(self: *const Arg, value_to_check: []const u8) bool { @@ -120,6 +99,18 @@ pub fn verifyValueInAllowedValues(self: *const Arg, value_to_check: []const u8) } } +pub fn applySetting(self: *Arg, option: Settings.Options) void { + return self.settings.apply(option); +} + +pub fn removeSetting(self: *Arg, option: Settings.Options) void { + return self.settings.remove(option); +} + +pub fn isSettingApplied(self: *const Arg, option: Settings.Options) bool { + return self.settings.isApplied(option); +} + test "emit methods docs" { std.testing.refAllDecls(@This()); } diff --git a/src/Command.zig b/src/Command.zig index 2b037c7..0f394b6 100644 --- a/src/Command.zig +++ b/src/Command.zig @@ -3,16 +3,16 @@ const Command = @This(); const std = @import("std"); const Arg = @import("Arg.zig"); const Help = @import("Help.zig"); +const MakeSettings = @import("settings.zig").MakeSettings; const mem = std.mem; const ArrayList = std.ArrayListUnmanaged; const Allocator = mem.Allocator; - -const Setting = struct { - takes_value: bool = false, - arg_required: bool = false, - subcommand_required: bool = false, -}; +const Settings = MakeSettings(&[_][]const u8{ + "takes_value", + "arg_required", + "subcommand_required", +}); allocator: Allocator, name: []const u8, @@ -20,7 +20,7 @@ description: ?[]const u8 = null, args: ArrayList(Arg) = .{}, options: ArrayList(Arg) = .{}, subcommands: ArrayList(Command) = .{}, -setting: Setting = .{}, +settings: Settings = .{}, /// Creates a new instance of it pub fn new(allocator: Allocator, name: []const u8) Command { @@ -81,22 +81,7 @@ pub fn takesNValues(self: *Command, arg_name: []const u8, n: usize) !void { if (n > 1) arg.valuesDelimiter(","); try self.addArg(arg); - self.takesValue(true); -} - -/// Specifies that the command takes value. Default to 'false` -pub fn takesValue(self: *Command, b: bool) void { - self.setting.takes_value = b; -} - -/// Specifies that argument is required to provide. Default to `false` -pub fn argRequired(self: *Command, boolean: bool) void { - self.setting.arg_required = boolean; -} - -/// Specifies that sub-command is required to provide. Default to `false` -pub fn subcommandRequired(self: *Command, boolean: bool) void { - self.setting.subcommand_required = boolean; + self.applySetting(.takes_value); } pub fn countArgs(self: *const Command) usize { @@ -145,6 +130,18 @@ pub fn findSubcommand(self: *const Command, provided_subcmd: []const u8) ?*const return null; } +pub fn applySetting(self: *Command, option: Settings.Options) void { + return self.settings.apply(option); +} + +pub fn removeSetting(self: *Command, option: Settings.Options) void { + return self.settings.remove(option); +} + +pub fn isSettingApplied(self: *const Command, option: Settings.Options) bool { + return self.settings.isApplied(option); +} + pub fn help(self: *const Command) Help { return Help.init(self); } diff --git a/src/Help.zig b/src/Help.zig index 3e5ff3d..ba2e9d2 100644 --- a/src/Help.zig +++ b/src/Help.zig @@ -3,13 +3,14 @@ const Help = @This(); const std = @import("std"); const Command = @import("Command.zig"); +const MakeSettings = @import("settings.zig").MakeSettings; const Braces = std.meta.Tuple(&[2]type{ u8, u8 }); -pub const Options = struct { - include_args: bool = false, - include_subcmds: bool = false, - include_flags: bool = false, -}; +pub const Options = MakeSettings(&[_][]const u8{ + "include_args", + "include_subcmds", + "include_flags", +}); cmd: *const Command, options: Options = .{}, @@ -45,7 +46,7 @@ pub fn writeAll(self: *Help) !void { try self.writeCommands(writer); try self.writeOptions(writer); - if (self.options.include_subcmds) { + if (self.options.isApplied(.include_subcmds)) { try writeNewLine(writer); try writer.print( "Run '{s} -h' or '{s} --help' to get help for specific command", @@ -65,8 +66,8 @@ fn writeHeader(self: *Help, writer: anytype) !void { try writer.print("Usage: {s} ", .{self.cmd.name}); if (self.cmd.countArgs() >= 1) { - self.options.include_flags = true; - const braces = getBraces(self.cmd.setting.arg_required); + self.options.apply(.include_args); + const braces = getBraces(self.cmd.isSettingApplied(.arg_required)); for (self.cmd.args.items) |arg| { try writer.print("{c}{s}{c} ", .{ braces[0], arg.name, braces[1] }); @@ -76,8 +77,8 @@ fn writeHeader(self: *Help, writer: anytype) !void { if (self.cmd.countOptions() >= 1) try writer.writeAll("[OPTIONS] "); if (self.cmd.countSubcommands() >= 1) { - self.options.include_subcmds = true; - const braces = getBraces(self.cmd.setting.subcommand_required); + self.options.apply(.include_subcmds); + const braces = getBraces(self.cmd.isSettingApplied(.subcommand_required)); try writer.print("{c}COMMAND{c}", .{ braces[0], braces[1] }); try writeNewLine(writer); @@ -90,7 +91,7 @@ fn getBraces(required: bool) Braces { } fn writeCommands(self: *Help, writer: anytype) !void { - if (!(self.options.include_subcmds)) return; + if (!(self.options.isApplied(.include_subcmds))) return; try writer.writeAll("Commands:"); try writeNewLine(writer); @@ -104,7 +105,7 @@ fn writeCommands(self: *Help, writer: anytype) !void { } fn writeOptions(self: *Help, writer: anytype) !void { - if (self.options.include_flags) { + if (self.options.isApplied(.include_flags)) { try writer.writeAll("Options:"); try writeNewLine(writer); @@ -116,7 +117,7 @@ fn writeOptions(self: *Help, writer: anytype) !void { if (arg.long_name) |long_name| try writer.print(" --{s} ", .{long_name}); - if (arg.settings.takes_value) { + if (arg.isSettingApplied(.takes_value)) { // TODO: Add new `Arg.placeholderName()` to display proper placeholder // Required options: diff --git a/src/main.zig b/src/main.zig index 60c5eb5..03bf63e 100644 --- a/src/main.zig +++ b/src/main.zig @@ -39,7 +39,7 @@ test "arg required error" { }; var app = try initAppArgs(allocator); - app.rootCommand().argRequired(true); + app.rootCommand().applySetting(.arg_required); defer app.deinit(); try testing.expectError(error.CommandArgumentNotProvided, app.parseFrom(argv)); @@ -51,7 +51,7 @@ test "subcommand required error" { }; var app = try initAppArgs(allocator); - app.rootCommand().subcommandRequired(true); + app.rootCommand().applySetting(.subcommand_required); defer app.deinit(); try testing.expectError(error.CommandSubcommandNotProvided, app.parseFrom(argv)); @@ -112,11 +112,10 @@ test "arg.takes_multiple_values" { var app = try initAppArgs(allocator); defer app.deinit(); - app.rootCommand().takesValue(true); + app.rootCommand().applySetting(.takes_value); var files = Arg.new("files"); - //files.takesValue(true); - files.takesMultipleValues(true); + files.applySetting(.takes_multiple_values); try app.rootCommand().addArg(files); var args = try app.parseFrom(argv); @@ -131,7 +130,7 @@ test "using displayHelp and displaySubcommandHelp help api" { var app = try initAppArgs(allocator); defer app.deinit(); - app.rootCommand().takesValue(false); + app.rootCommand().removeSetting(.takes_value); var subcmd = app.createCommand("subcmd", null); try subcmd.addArg(flag.boolean("bool", null, null)); diff --git a/src/parser/Parser.zig b/src/parser/Parser.zig index 6ad0533..9d1b630 100644 --- a/src/parser/Parser.zig +++ b/src/parser/Parser.zig @@ -108,7 +108,7 @@ pub fn init( .err_builder = ErrorBuilder.init(), .cmd = command, .cmd_args_idx = 0, - .consume_cmd_args = (command.setting.takes_value and command.countArgs() >= 1), + .consume_cmd_args = (command.isSettingApplied(.takes_value) and command.countArgs() >= 1), }; } @@ -162,7 +162,7 @@ pub fn parse(self: *Parser) Error!ArgsContext { } if (!(self.args_ctx.isPresent("help"))) { - if (self.cmd.setting.subcommand_required and self.args_ctx.subcommand == null) { + if (self.cmd.isSettingApplied(.subcommand_required) and self.args_ctx.subcommand == null) { self.err_builder.setErr(Error.CommandSubcommandNotProvided); return self.err_builder.err; } @@ -179,7 +179,7 @@ fn consumeCommandArg(self: *Parser, token: *const Token) Error!void { // Looks like we found a option if (token.tag != .some_argument) { - if (self.cmd.setting.arg_required and (self.args_ctx.args.count() == 0)) { + if (self.cmd.isSettingApplied(.arg_required) and (self.args_ctx.args.count() == 0)) { self.err_builder.setErr(Error.CommandArgumentNotProvided); return self.err_builder.err; } else { @@ -221,7 +221,7 @@ fn parseShortOption(self: *Parser, token: *const Token) InternalError!void { }; self.err_builder.setArg(arg); - if (!(arg.settings.takes_value)) { + if (!(arg.isSettingApplied(.takes_value))) { if (short_option.hasValue()) { return Error.UnneededAttachedValue; } else if (short_option.hasEmptyValue()) { @@ -256,7 +256,7 @@ fn parseLongOption(self: *Parser, token: *const Token) InternalError!void { }; self.err_builder.setArg(arg); - if (!(arg.settings.takes_value)) { + if (!(arg.isSettingApplied(.takes_value))) { if (option_tuple[1] != null) { return Error.UnneededAttachedValue; } else { @@ -356,7 +356,7 @@ fn processValue( const max_eqls_one = (has_max_num and (arg.max_values.? == 1)); // If maximum number and takes_multiple_values is not set we are not looking for more values - if ((!has_max_num or max_eqls_one) and !(arg.settings.takes_multiple_values)) { + if ((!has_max_num or max_eqls_one) and !(arg.isSettingApplied(.takes_multiple_values))) { // If values contains only one value, we can be sure that the minimum number of values is set to 1 // therefore return it as a single value instead if (values.items.len == 1) { @@ -366,7 +366,7 @@ fn processValue( return self.args_ctx.putMatchedArg(arg, .{ .many = values }); } } - if (arg.settings.takes_multiple_values) { + if (arg.isSettingApplied(.takes_multiple_values)) { if (!has_max_num) { try self.consumeValuesTillNextOption(arg, &values); return self.args_ctx.putMatchedArg(arg, .{ .many = values }); @@ -410,7 +410,7 @@ fn verifyAndAppendValue( ) InternalError!void { self.err_builder.setProvidedArg(value); - if ((value.len == 0) and !(arg.settings.allow_empty_value)) + if ((value.len == 0) and !(arg.isSettingApplied(.allow_empty_value))) return InternalError.EmptyArgValueNotAllowed; if (!(arg.verifyValueInAllowedValues(value))) return InternalError.ProvidedValueIsNotValidOption; @@ -425,14 +425,14 @@ fn parseSubCommand(self: *Parser, provided_subcmd: []const u8) Error!MatchedSubC }; // zig fmt: off - if (valid_subcmd.setting.takes_value + if (valid_subcmd.isSettingApplied(.takes_value) or valid_subcmd.countArgs() >= 1 or valid_subcmd.countOptions() >= 1 or valid_subcmd.countSubcommands() >= 1) { // zig fmt: on const help = valid_subcmd.help(); const subcmd_argv = self.tokenizer.restArg() orelse { - if (!(valid_subcmd.setting.arg_required)) { + if (!(valid_subcmd.isSettingApplied(.arg_required))) { return MatchedSubCommand.initWithArg( valid_subcmd.name, ArgsContext.init(self.allocator), diff --git a/src/settings.zig b/src/settings.zig new file mode 100644 index 0000000..a9dd727 --- /dev/null +++ b/src/settings.zig @@ -0,0 +1,54 @@ +const std = @import("std"); +const EnumField = std.builtin.TypeInfo.EnumField; + +pub fn MakeSettings(comptime options_name: []const []const u8) type { + return struct { + const Self = @This(); + pub const Options = MakeOptions(options_name); + + options: std.EnumMap(Options, bool) = .{}, + + pub fn apply(self: *Self, opt: Options) void { + return self.options.put(opt, true); + } + + pub fn remove(self: *Self, opt: Options) void { + return self.options.remove(opt); + } + + pub fn isApplied(self: *const Self, opt: Options) bool { + return self.options.contains(opt); + } + }; +} + +fn MakeOptions(comptime options_name: []const []const u8) type { + var fields: []const EnumField = &[_]EnumField{}; + for (options_name) |option_name, idx| { + fields = fields ++ &[_]EnumField{ + .{ .name = option_name, .value = idx }, + }; + } + return @Type(.{ + .Enum = .{ + .layout = .Auto, + .tag_type = u8, + .fields = fields, + .decls = &.{}, + .is_exhaustive = true, + }, + }); +} + +test "settings generator" { + const CmdSettings = MakeSettings(&[_][]const u8{ + "takes_value", + "subcommand_required", + }); + + var settings = CmdSettings{}; + + try std.testing.expectEqual(false, settings.isApplied(.takes_value)); + settings.apply(.takes_value); + try std.testing.expectEqual(true, settings.isApplied(.takes_value)); +}