From 07cd3fa55bf431f370fad8f120590b681f78a6ca Mon Sep 17 00:00:00 2001 From: Alexei Samokvalov Date: Sat, 30 Dec 2023 14:38:38 +0200 Subject: [PATCH] Pass all config structs by value Fix for zig 0.12 --- build.zig | 3 +- examples/short.zig | 4 +-- examples/simple.zig | 40 ++++++++++----------- src/PositionalArgsHelper.zig | 58 ++++++++++++++++++++++++++++++ src/command.zig | 10 +++--- src/help.zig | 41 +++++++++++++--------- src/parser.zig | 31 ++++++++-------- src/tests.zig | 68 ++++++++++++++++++------------------ 8 files changed, 158 insertions(+), 97 deletions(-) create mode 100644 src/PositionalArgsHelper.zig diff --git a/build.zig b/build.zig index 9d6bbde..4890cd2 100644 --- a/build.zig +++ b/build.zig @@ -32,6 +32,7 @@ pub fn build(b: *std.Build) void { }); simple.addModule("zig-cli", module); b.installArtifact(simple); + b.default_step.dependOn(&simple.step); const short = b.addExecutable(.{ .name = "short", @@ -40,7 +41,5 @@ pub fn build(b: *std.Build) void { }); short.addModule("zig-cli", module); b.installArtifact(short); - - // b.default_step.dependOn(&simple.step); b.default_step.dependOn(&short.step); } diff --git a/examples/short.zig b/examples/short.zig index 97782c4..cfd7b94 100644 --- a/examples/short.zig +++ b/examples/short.zig @@ -13,13 +13,13 @@ pub fn main() !void { .command = cli.Command{ .name = "short", .options = &.{ - &cli.Option{ + .{ .long_name = "port", .help = "port to bind to", .required = true, .value_ref = r.mkRef(&config.port), }, - &cli.Option{ + .{ .long_name = "host", .help = "host to listen on", .value_ref = r.mkRef(&config.host), diff --git a/examples/simple.zig b/examples/simple.zig index 74e56a8..78bdd0a 100644 --- a/examples/simple.zig +++ b/examples/simple.zig @@ -16,18 +16,6 @@ var config = struct { pub fn main() anyerror!void { var r = try cli.AppRunner.init(std.heap.page_allocator); - const arg1 = cli.PositionalArg{ - .name = "ARG1", - .help = "arg1 help", - .value_ref = r.mkRef(&config.arg1), - }; - - const arg2 = cli.PositionalArg{ - .name = "ARG2", - .help = "multiple arg2 help", - .value_ref = r.mkRef(&config.arg2), - }; - const sub2 = cli.Command{ .name = "sub2", .target = cli.CommandTarget{ @@ -45,8 +33,20 @@ pub fn main() anyerror!void { .target = cli.CommandTarget{ .action = cli.CommandAction{ .positional_args = cli.PositionalArgs{ - .args = &.{ &arg1, &arg2 }, - .first_optional_arg = &arg2, + .required = &.{ + cli.PositionalArg{ + .name = "ARG1", + .help = "arg1 help", + .value_ref = r.mkRef(&config.arg1), + }, + }, + .optional = &.{ + cli.PositionalArg{ + .name = "ARG2", + .help = "multiple arg2 help", + .value_ref = r.mkRef(&config.arg2), + }, + }, }, .exec = run_sub3, }, @@ -60,7 +60,7 @@ pub fn main() anyerror!void { }, .target = cli.CommandTarget{ .subcommands = &.{ - &cli.Command{ + cli.Command{ .name = "sub1", .description = cli.Description{ .one_line = "another awesome command", @@ -71,7 +71,7 @@ pub fn main() anyerror!void { , }, .options = &.{ - &cli.Option{ + .{ .long_name = "ip", .help = "this is the IP address", .short_alias = 'i', @@ -79,25 +79,25 @@ pub fn main() anyerror!void { .required = true, .value_name = "IP", }, - &cli.Option{ + .{ .long_name = "int", .help = "this is an int", .value_ref = r.mkRef(&config.int), }, - &cli.Option{ + .{ .long_name = "bool", .short_alias = 'b', .help = "this is a bool", .value_ref = r.mkRef(&config.bool), }, - &cli.Option{ + .{ .long_name = "float", .help = "this is a float", .value_ref = r.mkRef(&config.float), }, }, .target = cli.CommandTarget{ - .subcommands = &.{ &sub2, &sub3 }, + .subcommands = &.{ sub2, sub3 }, }, }, }, diff --git a/src/PositionalArgsHelper.zig b/src/PositionalArgsHelper.zig new file mode 100644 index 0000000..6f3e8e9 --- /dev/null +++ b/src/PositionalArgsHelper.zig @@ -0,0 +1,58 @@ +const command = @import("command.zig"); + +inner: *const command.PositionalArgs, + +const Self = @This(); + +pub fn len(self: *const Self) usize { + var cnt: usize = 0; + if (self.inner.required) |x| { + cnt += x.len; + } + if (self.inner.optional) |x| { + cnt += x.len; + } + return cnt; +} + +pub fn at(self: *const Self, ix: usize) *const command.PositionalArg { + var ix2 = ix; + if (self.inner.required) |x| { + if (ix < x.len) { + return &x[ix]; + } else { + ix2 -= x.len; + } + } + if (self.inner.optional) |x| { + return &x[ix2]; + } + + unreachable; +} + +pub fn iterator(self: *const Self) Iterator { + return Iterator.init(self); +} + +const Iterator = struct { + helper: *const Self, + len: usize, + index: usize, + + fn init(helper: *const Self) Iterator { + return .{ + .helper = helper, + .len = helper.len(), + .index = 0, + }; + } + + pub fn next(self: *Iterator) ?*const command.PositionalArg { + if (self.index >= self.len) return null; + + const x = self.helper.at(self.index); + self.index += 1; + return x; + } +}; diff --git a/src/command.zig b/src/command.zig index 2a8f8e0..5b1f4ea 100644 --- a/src/command.zig +++ b/src/command.zig @@ -30,7 +30,7 @@ pub const HelpConfig = struct { pub const Command = struct { name: []const u8, description: ?Description = null, - options: ?[]const *const Option = null, + options: ?[]const Option = null, target: CommandTarget, }; @@ -40,7 +40,7 @@ pub const Description = struct { }; pub const CommandTarget = union(enum) { - subcommands: []const *const Command, + subcommands: []const Command, action: CommandAction, }; @@ -62,10 +62,8 @@ pub const Option = struct { }; pub const PositionalArgs = struct { - args: []const *const PositionalArg, - - /// If not set, all positional arguments are considered as required. - first_optional_arg: ?*const PositionalArg = null, + required: ?[]const PositionalArg = null, + optional: ?[]const PositionalArg = null, }; pub const PositionalArg = struct { diff --git a/src/help.zig b/src/help.zig index 25bdf2c..c785da6 100644 --- a/src/help.zig +++ b/src/help.zig @@ -3,6 +3,7 @@ const command = @import("command.zig"); const Printer = @import("Printer.zig"); const value_ref = @import("value_ref.zig"); const GlobalOptions = @import("GlobalOptions.zig"); +const PositionalArgsHelper = @import("PositionalArgsHelper.zig"); const color_clear = "0"; @@ -56,22 +57,25 @@ const HelpPrinter = struct { switch (cmd.target) { .action => |act| { if (act.positional_args) |pargs| { - var closeOpt = false; - for (pargs.args) |parg| { - self.printer.write(" "); - if (pargs.first_optional_arg) |opt| { - if (opt == parg) { - self.printer.write("["); - closeOpt = true; + if (pargs.required) |req| { + for (req) |*parg| { + self.printer.write(" "); + self.printer.format("<{s}>", .{parg.name}); + if (parg.value_ref.value_type == value_ref.ValueType.multi) { + self.printer.write("..."); } } - self.printer.format("<{s}>", .{parg.name}); - if (parg.value_ref.value_type == value_ref.ValueType.multi) { - self.printer.write("..."); - } } - if (closeOpt) { - self.printer.write("]"); + if (pargs.optional) |opt| { + for (opt) |*parg| { + self.printer.write(" "); + self.printer.write("["); + self.printer.format("<{s}>", .{parg.name}); + if (parg.value_ref.value_type == value_ref.ValueType.multi) { + self.printer.write("..."); + } + self.printer.write("]"); + } } } }, @@ -89,13 +93,16 @@ const HelpPrinter = struct { switch (cmd.target) { .action => |act| { - if (act.positional_args) |pargs| { + if (act.positional_args) |*pargs| { self.printer.printInColor(self.help_config.color_section, "\nARGUMENTS:\n"); var max_arg_width: usize = 0; - for (pargs.args) |parg| { + const arg_h = PositionalArgsHelper{ .inner = pargs }; + var it = arg_h.iterator(); + while (it.next()) |parg| { max_arg_width = @max(max_arg_width, parg.name.len); } - for (pargs.args) |parg| { + it.index = 0; + while (it.next()) |parg| { self.printer.write(" "); self.printer.printInColor(self.help_config.color_option, parg.name); self.printer.printSpaces(max_arg_width - parg.name.len + 3); @@ -144,7 +151,7 @@ const HelpPrinter = struct { } option_column_width += 3; if (cmd.options) |option_list| { - for (option_list) |option| { + for (option_list) |*option| { self.printOption(option, option_column_width); } } diff --git a/src/parser.zig b/src/parser.zig index 3633a07..ba36c87 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -11,6 +11,7 @@ const value_parser = @import("value_parser.zig"); const str_true = value_parser.str_true; const str_false = value_parser.str_false; const GlobalOptions = @import("GlobalOptions.zig"); +const PositionalArgsHelper = @import("PositionalArgsHelper.zig"); pub const ParseResult = command.ExecFn; @@ -67,7 +68,7 @@ pub fn Parser(comptime Iterator: type) type { fn finalize(self: *Self) !ParseResult { for (self.command_path.items) |cmd| { if (cmd.options) |options| { - for (options) |opt| { + for (options) |*opt| { try self.set_option_value_from_envvar(opt); try opt.value_ref.finalize(self.alloc); @@ -78,17 +79,14 @@ pub fn Parser(comptime Iterator: type) type { } switch (cmd.target) { .action => |act| { - if (act.positional_args) |pargs| { - var optional = false; - for (pargs.args) |parg| { + if (act.positional_args) |*pargs| { + const argh = PositionalArgsHelper{ .inner = pargs }; + var it = argh.iterator(); + const required_args_no = if (pargs.required) |req| req.len else 0; + while (it.next()) |parg| { try parg.value_ref.finalize(self.alloc); - if (pargs.first_optional_arg) |first_opt| { - if (parg == first_opt) { - optional = true; - } - } - if (!optional and parg.value_ref.element_count == 0) { + if (it.index <= required_args_no and parg.value_ref.element_count == 0) { self.fail("missing required positional argument '{s}'", .{parg.name}); } } @@ -116,12 +114,13 @@ pub fn Parser(comptime Iterator: type) type { self.fail("command '{s}' cannot have positional arguments", .{cmd.name}); }, .action => |act| { - if (act.positional_args) |posArgs| { - if (self.position_argument_ix >= posArgs.args.len) { + if (act.positional_args) |*posArgs| { + var posH = PositionalArgsHelper{ .inner = posArgs }; + if (self.position_argument_ix >= posH.len()) { self.fail("unexpected positional argument '{s}'", .{arg}); } - const posArg = posArgs.args[self.position_argument_ix]; + const posArg = posH.at(self.position_argument_ix); var posArgRef = posArg.value_ref; posArgRef.put(arg, self.alloc) catch |err| { self.fail("positional argument ({s}): cannot parse '{s}' as {s}: {s}", .{ posArg.name, arg, posArgRef.value_data.type_name, @errorName(err) }); @@ -187,7 +186,7 @@ pub fn Parser(comptime Iterator: type) type { const cmd = self.current_command(); switch (cmd.target) { .subcommands => |cmds| { - for (cmds) |sc| { + for (cmds) |*sc| { if (std.mem.eql(u8, sc.name, some_name)) { try self.command_path.append(sc); return false; @@ -282,7 +281,7 @@ pub fn Parser(comptime Iterator: type) type { for (0..self.command_path.items.len) |ix| { const cmd = self.command_path.items[self.command_path.items.len - ix - 1]; if (cmd.options) |option_list| { - for (option_list) |option| { + for (option_list) |*option| { if (std.mem.eql(u8, option.long_name, option_name)) { return option; } @@ -303,7 +302,7 @@ pub fn Parser(comptime Iterator: type) type { return self.global_options.option_show_help; } if (cmd.options) |option_list| { - for (option_list) |option| { + for (option_list) |*option| { if (option.short_alias) |alias| { if (alias == option_alias) { return option; diff --git a/src/tests.zig b/src/tests.zig index 3cdf6f8..688a074 100644 --- a/src/tests.zig +++ b/src/tests.zig @@ -41,8 +41,8 @@ fn run(app: *const command.App, items: []const []const u8) !void { fn dummy_action() !void {} -fn runOptionsPArgs(input: []const []const u8, options: []const *command.Option, pargs: ?[]const *command.PositionalArg) !void { - const pa = if (pargs) |p| command.PositionalArgs{ .args = p } else null; +fn runOptionsPArgs(input: []const []const u8, options: []const command.Option, pargs: ?[]const command.PositionalArg) !void { + const pa = if (pargs) |p| command.PositionalArgs{ .required = p } else null; const app = command.App{ .command = command.Command{ .name = "cmd", @@ -59,7 +59,7 @@ fn runOptionsPArgs(input: []const []const u8, options: []const *command.Option, try run(&app, input); } -fn runOptions(input: []const []const u8, options: []const *command.Option) !void { +fn runOptions(input: []const []const u8, options: []const command.Option) !void { try runOptionsPArgs(input, options, null); } @@ -67,16 +67,16 @@ test "long option" { var r = runner(); defer r.deinit(); var aa: []const u8 = "test"; - var opt = command.Option{ + const opt = command.Option{ .long_name = "aa", .help = "option aa", .value_ref = r.mkRef(&aa), }; - try runOptions(&.{ "cmd", "--aa", "val" }, &.{&opt}); + try runOptions(&.{ "cmd", "--aa", "val" }, &.{opt}); try std.testing.expectEqualStrings("val", aa); - try runOptions(&.{ "cmd", "--aa=bb" }, &.{&opt}); + try runOptions(&.{ "cmd", "--aa=bb" }, &.{opt}); try std.testing.expectEqualStrings("bb", aa); } @@ -84,17 +84,17 @@ test "short option" { var r = runner(); defer r.deinit(); var aa: []const u8 = undefined; - var opt = command.Option{ + const opt = command.Option{ .long_name = "aa", .short_alias = 'a', .help = "option aa", .value_ref = r.mkRef(&aa), }; - try runOptions(&.{ "abc", "-a", "val" }, &.{&opt}); + try runOptions(&.{ "abc", "-a", "val" }, &.{opt}); try std.testing.expectEqualStrings("val", aa); - try runOptions(&.{ "abc", "-a=bb" }, &.{&opt}); + try runOptions(&.{ "abc", "-a=bb" }, &.{opt}); try std.testing.expectEqualStrings("bb", aa); } @@ -103,20 +103,20 @@ test "concatenated aliases" { defer r.deinit(); var aa: []const u8 = undefined; var bb: bool = false; - var bbopt = command.Option{ + const bbopt = command.Option{ .long_name = "bb", .short_alias = 'b', .help = "option bb", .value_ref = r.mkRef(&bb), }; - var opt = command.Option{ + const opt = command.Option{ .long_name = "aa", .short_alias = 'a', .help = "option aa", .value_ref = r.mkRef(&aa), }; - try runOptions(&.{ "abc", "-ba", "val" }, &.{ &opt, &bbopt }); + try runOptions(&.{ "abc", "-ba", "val" }, &.{ opt, bbopt }); try std.testing.expectEqualStrings("val", aa); try expect(bb); } @@ -126,18 +126,18 @@ test "int and float" { defer r.deinit(); var aa: i32 = undefined; var bb: f64 = undefined; - var aa_opt = command.Option{ + const aa_opt = command.Option{ .long_name = "aa", .help = "option aa", .value_ref = r.mkRef(&aa), }; - var bb_opt = command.Option{ + const bb_opt = command.Option{ .long_name = "bb", .help = "option bb", .value_ref = r.mkRef(&bb), }; - try runOptions(&.{ "abc", "--aa=34", "--bb", "15.25" }, &.{ &aa_opt, &bb_opt }); + try runOptions(&.{ "abc", "--aa=34", "--bb", "15.25" }, &.{ aa_opt, bb_opt }); try expect(34 == aa); try expect(15.25 == bb); } @@ -148,24 +148,24 @@ test "bools" { var aa: bool = true; var bb: bool = false; var cc: bool = false; - var aa_opt = command.Option{ + const aa_opt = command.Option{ .long_name = "aa", .help = "option aa", .value_ref = r.mkRef(&aa), }; - var bb_opt = command.Option{ + const bb_opt = command.Option{ .long_name = "bb", .help = "option bb", .value_ref = r.mkRef(&bb), }; - var cc_opt = command.Option{ + const cc_opt = command.Option{ .long_name = "cc", .short_alias = 'c', .help = "option cc", .value_ref = r.mkRef(&cc), }; - try runOptions(&.{ "abc", "--aa=faLSE", "-c", "--bb", "trUE" }, &.{ &aa_opt, &bb_opt, &cc_opt }); + try runOptions(&.{ "abc", "--aa=faLSE", "-c", "--bb", "trUE" }, &.{ aa_opt, bb_opt, cc_opt }); try expect(!aa); try expect(bb); try expect(cc); @@ -178,23 +178,23 @@ test "optional values" { var bb: ?f32 = 500; var cc: ?f32 = null; - var aa_opt = command.Option{ + const aa_opt = command.Option{ .long_name = "aa", .help = "option aa", .value_ref = r.mkRef(&aa), }; - var bb_opt = command.Option{ + const bb_opt = command.Option{ .long_name = "bb", .help = "option bb", .value_ref = r.mkRef(&bb), }; - var cc_opt = command.Option{ + const cc_opt = command.Option{ .long_name = "cc", .help = "option cc", .value_ref = r.mkRef(&cc), }; - try runOptions(&.{ "abc", "--aa=34", "--bb", "15.25" }, &.{ &aa_opt, &bb_opt, &cc_opt }); + try runOptions(&.{ "abc", "--aa=34", "--bb", "15.25" }, &.{ aa_opt, bb_opt, cc_opt }); try expect(34 == aa.?); try expect(15.25 == bb.?); try std.testing.expect(cc == null); @@ -204,14 +204,14 @@ test "int list" { var r = runner(); defer r.deinit(); var aa: []u64 = undefined; - var aa_opt = command.Option{ + const aa_opt = command.Option{ .long_name = "aa", .short_alias = 'a', .help = "option aa", .value_ref = r.mkRef(&aa), }; - try runOptions(&.{ "abc", "--aa=100", "--aa", "200", "-a", "300", "-a=400" }, &.{&aa_opt}); + try runOptions(&.{ "abc", "--aa=100", "--aa", "200", "-a", "300", "-a=400" }, &.{aa_opt}); try expect(aa.len == 4); try expect(aa[0] == 100); try expect(aa[1] == 200); @@ -225,14 +225,14 @@ test "string list" { var r = runner(); defer r.deinit(); var aa: [][]const u8 = undefined; - var aa_opt = command.Option{ + const aa_opt = command.Option{ .long_name = "aa", .short_alias = 'a', .help = "option aa", .value_ref = r.mkRef(&aa), }; - try runOptions(&.{ "abc", "--aa=a1", "--aa", "a2", "-a", "a3", "-a=a4" }, &.{&aa_opt}); + try runOptions(&.{ "abc", "--aa=a1", "--aa", "a2", "-a", "a3", "-a=a4" }, &.{aa_opt}); try expect(aa.len == 4); try std.testing.expectEqualStrings("a1", aa[0]); try std.testing.expectEqualStrings("a2", aa[1]); @@ -249,29 +249,29 @@ test "mix positional arguments and options" { var args: []const []const u8 = undefined; var aav: []const u8 = undefined; var bbv: []const u8 = undefined; - var aa = command.Option{ + const aa = command.Option{ .long_name = "aa", .short_alias = 'a', .help = "option aa", .value_ref = r.mkRef(&aav), }; - var bb = command.Option{ + const bb = command.Option{ .long_name = "bb", .help = "option bb", .value_ref = r.mkRef(&bbv), }; - var parg1 = command.PositionalArg{ + const parg1 = command.PositionalArg{ .name = "abc1", .help = "help", .value_ref = r.mkRef(&arg1), }; - var parg2 = command.PositionalArg{ + const parg2 = command.PositionalArg{ .name = "abc", .help = "help", .value_ref = r.mkRef(&args), }; - try runOptionsPArgs(&.{ "cmd", "--bb", "tt", "178", "-a", "val", "arg2", "--", "--arg3", "-arg4" }, &.{ &aa, &bb }, &.{ &parg1, &parg2 }); + try runOptionsPArgs(&.{ "cmd", "--bb", "tt", "178", "-a", "val", "arg2", "--", "--arg3", "-arg4" }, &.{ aa, bb }, &.{ parg1, parg2 }); defer std.testing.allocator.free(args); try std.testing.expectEqualStrings("val", aav); @@ -291,14 +291,14 @@ test "parse enums" { dd, }; var aa: []Aa = undefined; - var aa_opt = command.Option{ + const aa_opt = command.Option{ .long_name = "aa", .short_alias = 'a', .help = "option aa", .value_ref = r.mkRef(&aa), }; - try runOptions(&.{ "abc", "--aa=cc", "--aa", "dd" }, &.{&aa_opt}); + try runOptions(&.{ "abc", "--aa=cc", "--aa", "dd" }, &.{aa_opt}); try std.testing.expect(2 == aa.len); try std.testing.expect(aa[0] == Aa.cc); try std.testing.expect(aa[1] == Aa.dd);