From 1970535fbcfccfd26ea671d7705ce222cca72f34 Mon Sep 17 00:00:00 2001 From: Alexei Samokvalov Date: Sun, 15 Oct 2023 18:44:20 +0200 Subject: [PATCH 01/20] Enable parsing positional arguments into zig values --- example/short.zig | 2 +- example/simple.zig | 4 +-- src/command.zig | 12 ++++++--- src/parser.zig | 65 +++++++++++++++++++++++++--------------------- src/tests.zig | 36 +++++++++++++++---------- src/value_ref.zig | 2 +- 6 files changed, 70 insertions(+), 51 deletions(-) diff --git a/example/short.zig b/example/short.zig index 04034a2..0b29ff3 100644 --- a/example/short.zig +++ b/example/short.zig @@ -29,6 +29,6 @@ pub fn main() !void { return cli.run(app, allocator); } -fn run_server(_: []const []const u8) !void { +fn run_server() !void { std.log.debug("server is listening on {s}:{}", .{ config.host, config.port }); } diff --git a/example/simple.zig b/example/simple.zig index 266095b..5ea8771 100644 --- a/example/simple.zig +++ b/example/simple.zig @@ -69,7 +69,7 @@ pub fn main() anyerror!void { return cli.run(app, allocator); } -fn run_sub2(args: []const []const u8) anyerror!void { +fn run_sub2() anyerror!void { const c = &config; - std.log.debug("running sub2: ip={s}, bool={any}, float={any} arg_count={any}", .{ c.ip, c.bool, c.float, args.len }); + std.log.debug("running sub2: ip={s}, bool={any}, float={any}", .{ c.ip, c.bool, c.float }); } diff --git a/src/command.zig b/src/command.zig index b405657..0c30ac5 100644 --- a/src/command.zig +++ b/src/command.zig @@ -1,12 +1,12 @@ const std = @import("std"); -const vref = @import("./value_ref.zig"); -pub const ValueRef = vref.ValueRef; +const ValueRef = @import("./value_ref.zig").ValueRef; pub const App = struct { name: []const u8, description: ?[]const u8 = null, version: ?[]const u8 = null, author: ?[]const u8 = null, + positional_args: ?[]const *PositionalArg = null, options: ?[]const *Option = null, subcommands: ?[]const *const Command = null, action: ?Action = null, @@ -43,7 +43,7 @@ pub const Command = struct { action: ?Action = null, }; -pub const Action = *const fn (args: []const []const u8) anyerror!void; +pub const Action = *const fn () anyerror!void; pub const Option = struct { long_name: []const u8, @@ -54,3 +54,9 @@ pub const Option = struct { value_name: []const u8 = "VALUE", envvar: ?[]const u8 = null, }; + +pub const PositionalArg = struct { + name: []const u8, + help: []const u8, + value_ref: ValueRef, +}; diff --git a/src/parser.zig b/src/parser.zig index 4c045da..4c10be7 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -5,12 +5,10 @@ const command = @import("command.zig"); const help = @import("./help.zig"); const argp = @import("./arg.zig"); const Printer = @import("./Printer.zig"); -const mkRef = @import("./value_ref.zig").mkRef; +const vref = @import("./value_ref.zig"); +const mkRef = vref.mkRef; -pub const ParseResult = struct { - action: command.Action, - args: []const []const u8, -}; +pub const ParseResult = command.Action; pub fn run(app: *const command.App, alloc: Allocator) anyerror!void { var iter = try std.process.argsWithAllocator(alloc); @@ -19,8 +17,8 @@ pub fn run(app: *const command.App, alloc: Allocator) anyerror!void { var cr = try Parser(std.process.ArgIterator).init(app, iter, alloc); defer cr.deinit(); - var result = try cr.parse(); - return result.action(result.args); + var action = try cr.parse(); + return action(); } var help_option_set: bool = false; @@ -40,7 +38,7 @@ pub fn Parser(comptime Iterator: type) type { arg_iterator: Iterator, app: *const command.App, command_path: std.ArrayList(*const command.Command), - captured_arguments: std.ArrayList([]const u8), + position_argument_ix: usize = 0, pub fn init(app: *const command.App, it: Iterator, alloc: Allocator) !Self { return Self{ @@ -48,12 +46,10 @@ pub fn Parser(comptime Iterator: type) type { .arg_iterator = it, .app = app, .command_path = try std.ArrayList(*const command.Command).initCapacity(alloc, 16), - .captured_arguments = try std.ArrayList([]const u8).initCapacity(alloc, 16), }; } pub fn deinit(self: *Self) void { - self.captured_arguments.deinit(); self.command_path.deinit(); } @@ -77,9 +73,9 @@ pub fn Parser(comptime Iterator: type) type { var args_only = false; while (self.next_arg()) |arg| { if (args_only) { - try self.captured_arguments.append(arg); - } else if (argp.interpret(arg)) |int| { - args_only = try self.process_interpretation(&int); + try self.handlePositionalArgument(arg); + } else if (argp.interpret(arg)) |interpretation| { + args_only = try self.process_interpretation(&interpretation); } else |err| { switch (err) { error.MissingOptionArgument => self.fail("missing argument: '{s}'", .{arg}), @@ -95,21 +91,41 @@ pub fn Parser(comptime Iterator: type) type { for (options) |opt| { try self.set_option_value_from_envvar(opt); try opt.value_ref.finalize(self.alloc); + + if (opt.required and opt.value_ref.element_count == 0) { + self.fail("missing required option '{s}'", .{opt.long_name}); + } } } } - - self.ensure_all_required_set(self.current_command()); - var args = try self.captured_arguments.toOwnedSlice(); + if (self.app.positional_args) |pargs| { + for (pargs) |parg| { + try parg.value_ref.finalize(self.alloc); + } + } if (self.current_command().action) |action| { - return ParseResult{ .action = action, .args = args }; + return action; } else { self.fail("command '{s}': no subcommand provided", .{self.current_command().name}); unreachable; } } + fn handlePositionalArgument(self: *Self, arg: []const u8) !void { + if (self.app.positional_args) |posArgs| { + if (self.position_argument_ix >= posArgs.len) { + self.fail("unexpected positional argument: {s}", .{arg}); + } + + const posArgRef = &posArgs[self.position_argument_ix].value_ref; + try posArgRef.put(arg, self.alloc); + if (posArgRef.value_type == vref.ValueType.single) { + self.position_argument_ix += 1; + } + } + } + fn set_option_value_from_envvar(self: *const Self, opt: *command.Option) !void { if (opt.value_ref.element_count > 0) return; @@ -151,11 +167,10 @@ pub fn Parser(comptime Iterator: type) type { }, .other => |some_name| { if (find_subcommand(self.current_command(), some_name)) |cmd| { - self.ensure_all_required_set(self.current_command()); - self.validate_command(cmd); + self.validate_command(cmd); // TODO: validation can happen at comptime for all commands try self.command_path.append(cmd); } else { - try self.captured_arguments.append(some_name); + try self.handlePositionalArgument(some_name); } }, }; @@ -260,16 +275,6 @@ pub fn Parser(comptime Iterator: type) type { } } } - - fn ensure_all_required_set(self: *const Self, cmd: *const command.Command) void { - if (cmd.options) |list| { - for (list) |option| { - if (option.required and option.value_ref.element_count == 0) { - self.fail("missing required option '{s}'", .{option.long_name}); - } - } - } - } }; } diff --git a/src/tests.zig b/src/tests.zig index 202986e..8790fdd 100644 --- a/src/tests.zig +++ b/src/tests.zig @@ -25,18 +25,17 @@ const StringSliceIterator = struct { } }; -fn run(app: *command.App, items: []const []const u8) !ParseResult { +fn run(app: *command.App, items: []const []const u8) !void { var it = StringSliceIterator{ .items = items, }; var parser = try Parser(StringSliceIterator).init(app, it, alloc); - var result = try parser.parse(); + _ = try parser.parse(); parser.deinit(); - return result; } -fn dummy_action(_: []const []const u8) !void {} +fn dummy_action() !void {} test "long option" { var aa: []const u8 = "test"; @@ -211,6 +210,8 @@ test "string list" { } test "mix positional arguments and options" { + var arg1: u32 = 0; + var args: []const []const u8 = undefined; var aav: []const u8 = undefined; var bbv: []const u8 = undefined; var aa = command.Option{ @@ -224,21 +225,28 @@ test "mix positional arguments and options" { .help = "option bb", .value_ref = mkRef(&bbv), }; - var app = command.App{ + var parg1 = command.PositionalArg{ + .name = "abc1", + .help = "help", + .value_ref = mkRef(&arg1), + }; + var parg2 = command.PositionalArg{ .name = "abc", - .options = &.{ &aa, &bb }, - .action = dummy_action, + .help = "help", + .value_ref = mkRef(&args), }; + var app = command.App{ .name = "abc", .options = &.{ &aa, &bb }, .action = dummy_action, .positional_args = &.{ &parg1, &parg2 } }; + + try run(&app, &.{ "cmd", "--bb", "tt", "178", "-a", "val", "arg2", "--", "--arg3", "-arg4" }); + defer std.testing.allocator.free(args); - var result = try run(&app, &.{ "cmd", "--bb", "tt", "arg1", "-a", "val", "arg2", "--", "--arg3", "-arg4" }); - defer std.testing.allocator.free(result.args); try std.testing.expectEqualStrings("val", aav); try std.testing.expectEqualStrings("tt", bbv); - try expect(result.args.len == 4); - try std.testing.expectEqualStrings("arg1", result.args[0]); - try std.testing.expectEqualStrings("arg2", result.args[1]); - try std.testing.expectEqualStrings("--arg3", result.args[2]); - try std.testing.expectEqualStrings("-arg4", result.args[3]); + try std.testing.expect(arg1 == 178); + try expect(args.len == 3); + try std.testing.expectEqualStrings("arg2", args[0]); + try std.testing.expectEqualStrings("--arg3", args[1]); + try std.testing.expectEqualStrings("-arg4", args[2]); } test "parse enums" { diff --git a/src/value_ref.zig b/src/value_ref.zig index 3ca6237..eb08730 100644 --- a/src/value_ref.zig +++ b/src/value_ref.zig @@ -40,7 +40,7 @@ pub const ValueRef = struct { } }; -const ValueType = union(enum) { +pub const ValueType = union(enum) { single, multi: ValueList, }; From 78069711e501ed3676920664982276cc1143bb5b Mon Sep 17 00:00:00 2001 From: Alexei Samokvalov Date: Mon, 16 Oct 2023 17:16:02 +0200 Subject: [PATCH 02/20] Improve positional argument handling --- src/Printer.zig | 4 ++++ src/command.zig | 2 ++ src/parser.zig | 33 ++++++++++++++++++++++++--------- src/tests.zig | 2 +- 4 files changed, 31 insertions(+), 10 deletions(-) diff --git a/src/Printer.zig b/src/Printer.zig index 58696c7..4c9e3fd 100644 --- a/src/Printer.zig +++ b/src/Printer.zig @@ -23,6 +23,10 @@ pub inline fn write(self: *Self, text: []const u8) void { _ = self.out.write(text) catch unreachable; } +pub inline fn printNewLine(self: *Self) void { + self.write("\n"); +} + pub inline fn format(self: *Self, comptime text: []const u8, args: anytype) void { std.fmt.format(self.out, text, args) catch unreachable; } diff --git a/src/command.zig b/src/command.zig index 0c30ac5..8a5cbef 100644 --- a/src/command.zig +++ b/src/command.zig @@ -39,6 +39,7 @@ pub const Command = struct { /// One liner for subcommands help: []const u8, options: ?[]const *Option = null, + positional_args: ?[]const *PositionalArg = null, subcommands: ?[]const *const Command = null, action: ?Action = null, }; @@ -59,4 +60,5 @@ pub const PositionalArg = struct { name: []const u8, help: []const u8, value_ref: ValueRef, + required: bool = false, }; diff --git a/src/parser.zig b/src/parser.zig index 4c10be7..9a6bcc0 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -65,6 +65,7 @@ pub fn Parser(comptime Iterator: type) type { .action = self.app.action, .subcommands = self.app.subcommands, .options = self.app.options, + .positional_args = self.app.positional_args, }; try self.command_path.append(&app_command); @@ -97,10 +98,14 @@ pub fn Parser(comptime Iterator: type) type { } } } - } - if (self.app.positional_args) |pargs| { - for (pargs) |parg| { - try parg.value_ref.finalize(self.alloc); + if (cmd.positional_args) |pargs| { + for (pargs) |parg| { + try parg.value_ref.finalize(self.alloc); + + if (parg.required and parg.value_ref.element_count == 0) { + self.fail("missing required positional argument '{s}'", .{parg.name}); + } + } } } @@ -113,13 +118,17 @@ pub fn Parser(comptime Iterator: type) type { } fn handlePositionalArgument(self: *Self, arg: []const u8) !void { - if (self.app.positional_args) |posArgs| { + if (self.current_command().positional_args) |posArgs| { if (self.position_argument_ix >= posArgs.len) { - self.fail("unexpected positional argument: {s}", .{arg}); + self.fail("unexpected positional argument '{s}'", .{arg}); } - const posArgRef = &posArgs[self.position_argument_ix].value_ref; - try posArgRef.put(arg, self.alloc); + var posArg = posArgs[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) }); + unreachable; + }; if (posArgRef.value_type == vref.ValueType.single) { self.position_argument_ix += 1; } @@ -166,7 +175,13 @@ pub fn Parser(comptime Iterator: type) type { args_only = true; }, .other => |some_name| { - if (find_subcommand(self.current_command(), some_name)) |cmd| { + const has_positional_arguments = if (self.current_command().positional_args) |pargs| + self.position_argument_ix > 0 or pargs[0].value_ref.element_count > 0 + else + false; + if (has_positional_arguments) { + try self.handlePositionalArgument(some_name); + } else if (find_subcommand(self.current_command(), some_name)) |cmd| { self.validate_command(cmd); // TODO: validation can happen at comptime for all commands try self.command_path.append(cmd); } else { diff --git a/src/tests.zig b/src/tests.zig index 8790fdd..1114304 100644 --- a/src/tests.zig +++ b/src/tests.zig @@ -243,7 +243,7 @@ test "mix positional arguments and options" { try std.testing.expectEqualStrings("val", aav); try std.testing.expectEqualStrings("tt", bbv); try std.testing.expect(arg1 == 178); - try expect(args.len == 3); + try std.testing.expectEqual(@as(usize, 3), args.len); try std.testing.expectEqualStrings("arg2", args[0]); try std.testing.expectEqualStrings("--arg3", args[1]); try std.testing.expectEqualStrings("-arg4", args[2]); From dcf6130d6a3c2056f0898a8c7515704656c95b0e Mon Sep 17 00:00:00 2001 From: Alexei Samokvalov Date: Wed, 18 Oct 2023 17:52:59 +0200 Subject: [PATCH 03/20] Print positional arguments --- example/simple.zig | 39 +++++++++++++++++++++++++++++++++------ src/help.zig | 27 ++++++++++++++++++++++++++- 2 files changed, 59 insertions(+), 7 deletions(-) diff --git a/example/simple.zig b/example/simple.zig index 5ea8771..753e813 100644 --- a/example/simple.zig +++ b/example/simple.zig @@ -9,6 +9,8 @@ var config = struct { int: i32 = undefined, bool: bool = false, float: f64 = 0.34, + arg1: u64 = 0, + arg2: []const []const u8 = undefined, }{}; var ip_option = cli.Option{ @@ -36,6 +38,17 @@ var float_option = cli.Option{ .value_ref = cli.mkRef(&config.float), }; +var arg1 = cli.PositionalArg{ + .name = "ARG1", + .help = "arg1 help", + .value_ref = cli.mkRef(&config.arg1), +}; + +var arg2 = cli.PositionalArg{ + .name = "ARG2", + .help = "multiple arg2 help", + .value_ref = cli.mkRef(&config.arg2), +}; var app = &cli.App{ .name = "simple", .description = "This a simple CLI app\nEnjoy!", @@ -55,13 +68,19 @@ var app = &cli.App{ &bool_option, &float_option, }, - .subcommands = &.{ - &cli.Command{ - .name = "sub2", - .help = "sub2 help", - .action = run_sub2, + .subcommands = &.{ &cli.Command{ + .name = "sub2", + .help = "sub2 help", + .action = run_sub2, + }, &cli.Command{ + .name = "sub3", + .help = "sub3 command with that takes positional arguments", + .action = run_sub3, + .positional_args = &.{ + &arg1, + &arg2, }, - }, + } }, }}, }; @@ -69,6 +88,14 @@ pub fn main() anyerror!void { return cli.run(app, allocator); } +fn run_sub3() anyerror!void { + const c = &config; + std.log.debug("sub3: arg1: {}", .{c.arg1}); + for (c.arg2) |arg| { + std.log.debug("sub3: arg2: {s}", .{arg}); + } +} + fn run_sub2() anyerror!void { const c = &config; std.log.debug("running sub2: ip={s}, bool={any}, float={any}", .{ c.ip, c.bool, c.float }); diff --git a/src/help.zig b/src/help.zig index f3a22d6..f2ec81b 100644 --- a/src/help.zig +++ b/src/help.zig @@ -1,6 +1,7 @@ const std = @import("std"); const command = @import("command.zig"); const Printer = @import("Printer.zig"); +const value_ref = @import("value_ref.zig"); const color_clear = "0"; @@ -44,7 +45,16 @@ const HelpPrinter = struct { self.printer.format("{s} ", .{cmd.name}); } var current_command = command_path[command_path.len - 1]; - self.printer.format("[OPTIONS]\n", .{}); + self.printer.format("[OPTIONS]", .{}); + if (current_command.positional_args) |pargs| { + for (pargs) |parg| { + self.printer.format(" <{s}>", .{parg.name}); + if (parg.value_ref.value_type == value_ref.ValueType.multi) { + self.printer.write("..."); + } + } + } + self.printer.printNewLine(); self.printer.printColor(color_clear); if (command_path.len > 1) { @@ -54,6 +64,21 @@ const HelpPrinter = struct { self.printer.format("\n{s}\n", .{desc}); } + if (current_command.positional_args) |pargs| { + self.printer.printInColor(self.help_config.color_section, "\nARGUMENTS:\n"); + var max_arg_width: usize = 0; + for (pargs) |parg| { + max_arg_width = @max(max_arg_width, parg.name.len); + } + for (pargs) |parg| { + self.printer.write(" "); + self.printer.printInColor(self.help_config.color_option, parg.name); + self.printer.printSpaces(max_arg_width - parg.name.len + 3); + self.printer.write(parg.help); + self.printer.printNewLine(); + } + } + if (current_command.subcommands) |sc_list| { self.printer.printInColor(self.help_config.color_section, "\nCOMMANDS:\n"); From 586efcfc03d888f295dcee56da954fcf28f4a7a6 Mon Sep 17 00:00:00 2001 From: Alexei Samokvalov Date: Fri, 20 Oct 2023 22:49:50 +0200 Subject: [PATCH 04/20] Add build.zig.zon --- .github/workflows/build-master.yaml | 1 + build.zig.zon | 4 ++++ 2 files changed, 5 insertions(+) create mode 100644 build.zig.zon diff --git a/.github/workflows/build-master.yaml b/.github/workflows/build-master.yaml index d7a2eb9..8debb24 100644 --- a/.github/workflows/build-master.yaml +++ b/.github/workflows/build-master.yaml @@ -16,4 +16,5 @@ jobs: with: version: master - run: zig fmt --check *.zig src/*.zig + - run: zig build - run: zig build test diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..c01d2d9 --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,4 @@ +.{ + .name = "zig-cli", + .version = "0.8.0", +} From bfe1c687883af7c4c49d7e6a0c75e9bbc5be77ab Mon Sep 17 00:00:00 2001 From: Alexei Samokvalov Date: Fri, 20 Oct 2023 22:56:56 +0200 Subject: [PATCH 05/20] Add github action for zig 0.11 --- .github/workflows/build-master.yaml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-master.yaml b/.github/workflows/build-master.yaml index 8debb24..b187e16 100644 --- a/.github/workflows/build-master.yaml +++ b/.github/workflows/build-master.yaml @@ -12,9 +12,19 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - uses: goto-bus-stop/setup-zig@v1 + - uses: goto-bus-stop/setup-zig@v2 with: version: master - run: zig fmt --check *.zig src/*.zig - run: zig build - run: zig build test + validate_and_test_011: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: goto-bus-stop/setup-zig@v2 + with: + version: 0.11.0 + - run: zig fmt --check *.zig src/*.zig + - run: zig build + - run: zig build test From 62e20a2d44752d7e57e189e29c22cc995fe4b9fd Mon Sep 17 00:00:00 2001 From: Alexei Samokvalov Date: Sat, 21 Oct 2023 10:00:50 +0200 Subject: [PATCH 06/20] Update github acitons --- .github/workflows/build-master.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-master.yaml b/.github/workflows/build-master.yaml index b187e16..754df2b 100644 --- a/.github/workflows/build-master.yaml +++ b/.github/workflows/build-master.yaml @@ -11,7 +11,7 @@ jobs: validate_and_test_master: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - uses: goto-bus-stop/setup-zig@v2 with: version: master @@ -21,7 +21,7 @@ jobs: validate_and_test_011: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - uses: goto-bus-stop/setup-zig@v2 with: version: 0.11.0 From b1f885e2d8c9a21177ee5d6d3c54e7ba0ebf6a5a Mon Sep 17 00:00:00 2001 From: Alexei Samokvalov Date: Sat, 21 Oct 2023 21:02:15 +0200 Subject: [PATCH 07/20] Compile with zig 0.12-dev --- build.zig.zon | 1 + 1 file changed, 1 insertion(+) diff --git a/build.zig.zon b/build.zig.zon index c01d2d9..7cd6413 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,4 +1,5 @@ .{ .name = "zig-cli", .version = "0.8.0", + .paths = .{"./src"}, } From 472188de28ea1b11bec4b176d7ee2b4ea2540bba Mon Sep 17 00:00:00 2001 From: Alexei Samokvalov Date: Sat, 21 Oct 2023 21:17:28 +0200 Subject: [PATCH 08/20] test --- build.zig.zon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.zig.zon b/build.zig.zon index 7cd6413..10236ab 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,5 +1,5 @@ .{ .name = "zig-cli", .version = "0.8.0", - .paths = .{"./src"}, + .paths = .{""}, } From c89d36726844e527e8f98c42ce94d0a7c2bbbd35 Mon Sep 17 00:00:00 2001 From: Alexei Samokvalov Date: Sat, 21 Oct 2023 21:22:24 +0200 Subject: [PATCH 09/20] test2 --- build.zig.zon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.zig.zon b/build.zig.zon index 10236ab..5687ec2 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,5 +1,5 @@ .{ .name = "zig-cli", .version = "0.8.0", - .paths = .{""}, + .paths = .{"./src", "./build.zig.zon"}, } From 581ab3410397b05062e0a68cd054521b20515d03 Mon Sep 17 00:00:00 2001 From: Alexei Samokvalov Date: Sun, 22 Oct 2023 00:03:35 +0200 Subject: [PATCH 10/20] Restructure Command --- example/short.zig | 13 +++-- example/simple.zig | 82 +++++++++++++++++++---------- src/command.zig | 31 ++++++----- src/help.zig | 98 ++++++++++++++++++---------------- src/parser.zig | 128 ++++++++++++++++++++------------------------- 5 files changed, 191 insertions(+), 161 deletions(-) diff --git a/example/short.zig b/example/short.zig index 0b29ff3..dfa51b1 100644 --- a/example/short.zig +++ b/example/short.zig @@ -20,9 +20,16 @@ var port = cli.Option{ .value_ref = cli.mkRef(&config.port), }; var app = &cli.App{ - .name = "short", - .options = &.{ &host, &port }, - .action = run_server, + .command = cli.Command{ + .name = "short", + .options = &.{ &host, &port }, + .description = cli.Description{ .one_line = "a short example" }, + .target = cli.CommandTarget{ + .action = cli.CommandAction{ + .exec = run_server, + }, + }, + }, }; pub fn main() !void { diff --git a/example/simple.zig b/example/simple.zig index 753e813..d054e51 100644 --- a/example/simple.zig +++ b/example/simple.zig @@ -49,39 +49,65 @@ var arg2 = cli.PositionalArg{ .help = "multiple arg2 help", .value_ref = cli.mkRef(&config.arg2), }; -var app = &cli.App{ - .name = "simple", - .description = "This a simple CLI app\nEnjoy!", - .version = "0.10.3", - .author = "sam701 & contributors", - .subcommands = &.{&cli.Command{ - .name = "sub1", - .help = "another awesome command", - .description = + +var sub1 = cli.Command{ + .name = "sub1", + .description = cli.Description{ + .one_line = "another awesome command", + .detailed = \\this is my awesome multiline description. \\This is already line 2. \\And this is line 3. , - .options = &.{ - &ip_option, - &int_option, - &bool_option, - &float_option, + }, + .options = &.{ + &ip_option, + &int_option, + &bool_option, + &float_option, + }, + .target = cli.CommandTarget{ + .subcommands = &.{ &sub2, &sub3 }, + }, +}; + +var sub2 = cli.Command{ + .name = "sub2", + .description = cli.Description{ + .one_line = "sub2 help", + }, + .target = cli.CommandTarget{ + .action = cli.CommandAction{ + .exec = run_sub2, + }, + }, +}; + +var sub3 = cli.Command{ + .name = "sub3", + .description = cli.Description{ + .one_line = "sub3 with positional arguments", + }, + .target = cli.CommandTarget{ + .action = cli.CommandAction{ + .positional_args = &.{ &arg1, &arg2 }, + .exec = run_sub3, + }, + }, +}; + +var app = &cli.App{ + .command = cli.Command{ + .name = "simple", + .description = cli.Description{ + .one_line = "This a simple CLI app. Enjoy!", + }, + .target = cli.CommandTarget{ + .subcommands = &.{&sub1}, }, - .subcommands = &.{ &cli.Command{ - .name = "sub2", - .help = "sub2 help", - .action = run_sub2, - }, &cli.Command{ - .name = "sub3", - .help = "sub3 command with that takes positional arguments", - .action = run_sub3, - .positional_args = &.{ - &arg1, - &arg2, - }, - } }, - }}, + }, + .version = "0.10.3", + .author = "sam701 & contributors", }; pub fn main() anyerror!void { diff --git a/src/command.zig b/src/command.zig index 8a5cbef..0e5502d 100644 --- a/src/command.zig +++ b/src/command.zig @@ -2,14 +2,9 @@ const std = @import("std"); const ValueRef = @import("./value_ref.zig").ValueRef; pub const App = struct { - name: []const u8, - description: ?[]const u8 = null, + command: Command, version: ?[]const u8 = null, author: ?[]const u8 = null, - positional_args: ?[]const *PositionalArg = null, - options: ?[]const *Option = null, - subcommands: ?[]const *const Command = null, - action: ?Action = null, /// If set all options can be set by providing an environment variable. /// For example an option with a long name `hello_world` can be set by setting `_HELLO_WORLD` environment variable. @@ -34,17 +29,27 @@ pub const HelpConfig = struct { pub const Command = struct { name: []const u8, - /// Detailed multiline command description - description: ?[]const u8 = null, - /// One liner for subcommands - help: []const u8, + description: Description, options: ?[]const *Option = null, + target: CommandTarget, +}; + +pub const Description = struct { + one_line: []const u8, + detailed: ?[]const u8 = null, +}; + +pub const CommandTarget = union(enum) { + subcommands: []const *const Command, + action: CommandAction, +}; + +pub const CommandAction = struct { positional_args: ?[]const *PositionalArg = null, - subcommands: ?[]const *const Command = null, - action: ?Action = null, + exec: ExecFn, }; -pub const Action = *const fn () anyerror!void; +pub const ExecFn = *const fn () anyerror!void; pub const Option = struct { long_name: []const u8, diff --git a/src/help.zig b/src/help.zig index f2ec81b..514fd46 100644 --- a/src/help.zig +++ b/src/help.zig @@ -24,7 +24,7 @@ const HelpPrinter = struct { fn printAppHelp(self: *HelpPrinter, app: *const command.App, command_path: []const *const command.Command) void { self.printer.printColor(self.help_config.color_app_name); - self.printer.format("{s}\n", .{app.name}); + self.printer.format("{s}\n", .{app.command.name}); self.printer.printColor(color_clear); if (app.version) |v| { self.printer.format("Version: {s}\n", .{v}); @@ -44,66 +44,72 @@ const HelpPrinter = struct { for (command_path) |cmd| { self.printer.format("{s} ", .{cmd.name}); } - var current_command = command_path[command_path.len - 1]; + var cmd = command_path[command_path.len - 1]; self.printer.format("[OPTIONS]", .{}); - if (current_command.positional_args) |pargs| { - for (pargs) |parg| { - self.printer.format(" <{s}>", .{parg.name}); - if (parg.value_ref.value_type == value_ref.ValueType.multi) { - self.printer.write("..."); + switch (cmd.target) { + .action => |act| { + if (act.positional_args) |pargs| { + for (pargs) |parg| { + self.printer.format(" <{s}>", .{parg.name}); + if (parg.value_ref.value_type == value_ref.ValueType.multi) { + self.printer.write("..."); + } + } } - } + }, + .subcommands => {}, } self.printer.printNewLine(); self.printer.printColor(color_clear); - if (command_path.len > 1) { - self.printer.format("\n{s}\n", .{current_command.help}); - } - if (current_command.description) |desc| { - self.printer.format("\n{s}\n", .{desc}); + self.printer.format("\n{s}\n", .{cmd.description.one_line}); + if (cmd.description.detailed) |det| { + self.printer.format("\n{s}\n", .{det}); } - if (current_command.positional_args) |pargs| { - self.printer.printInColor(self.help_config.color_section, "\nARGUMENTS:\n"); - var max_arg_width: usize = 0; - for (pargs) |parg| { - max_arg_width = @max(max_arg_width, parg.name.len); - } - for (pargs) |parg| { - self.printer.write(" "); - self.printer.printInColor(self.help_config.color_option, parg.name); - self.printer.printSpaces(max_arg_width - parg.name.len + 3); - self.printer.write(parg.help); - self.printer.printNewLine(); - } - } - - if (current_command.subcommands) |sc_list| { - self.printer.printInColor(self.help_config.color_section, "\nCOMMANDS:\n"); + switch (cmd.target) { + .action => |act| { + if (act.positional_args) |pargs| { + self.printer.printInColor(self.help_config.color_section, "\nARGUMENTS:\n"); + var max_arg_width: usize = 0; + for (pargs) |parg| { + max_arg_width = @max(max_arg_width, parg.name.len); + } + for (pargs) |parg| { + self.printer.write(" "); + self.printer.printInColor(self.help_config.color_option, parg.name); + self.printer.printSpaces(max_arg_width - parg.name.len + 3); + self.printer.write(parg.help); + self.printer.printNewLine(); + } + } + }, + .subcommands => |sc_list| { + self.printer.printInColor(self.help_config.color_section, "\nCOMMANDS:\n"); - var max_cmd_width: usize = 0; - for (sc_list) |sc| { - max_cmd_width = @max(max_cmd_width, sc.name.len); - } - const cmd_column_width = max_cmd_width + 3; - for (sc_list) |sc| { - self.printer.printColor(self.help_config.color_option); - self.printer.format(" {s}", .{sc.name}); - self.printer.printColor(color_clear); - var i: usize = 0; - while (i < cmd_column_width - sc.name.len) { - self.printer.write(" "); - i += 1; + var max_cmd_width: usize = 0; + for (sc_list) |sc| { + max_cmd_width = @max(max_cmd_width, sc.name.len); } + const cmd_column_width = max_cmd_width + 3; + for (sc_list) |sc| { + self.printer.printColor(self.help_config.color_option); + self.printer.format(" {s}", .{sc.name}); + self.printer.printColor(color_clear); + var i: usize = 0; + while (i < cmd_column_width - sc.name.len) { + self.printer.write(" "); + i += 1; + } - self.printer.format("{s}\n", .{sc.help}); - } + self.printer.format("{s}\n", .{sc.description.one_line}); + } + }, } self.printer.printInColor(self.help_config.color_section, "\nOPTIONS:\n"); var option_column_width: usize = 7; - if (current_command.options) |option_list| { + if (cmd.options) |option_list| { var max_option_width: usize = 0; for (option_list) |option| { var w = option.long_name.len + option.value_name.len + 3; diff --git a/src/parser.zig b/src/parser.zig index 9a6bcc0..6d65be6 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -8,7 +8,7 @@ const Printer = @import("./Printer.zig"); const vref = @import("./value_ref.zig"); const mkRef = vref.mkRef; -pub const ParseResult = command.Action; +pub const ParseResult = command.ExecFn; pub fn run(app: *const command.App, alloc: Allocator) anyerror!void { var iter = try std.process.argsWithAllocator(alloc); @@ -58,18 +58,8 @@ pub fn Parser(comptime Iterator: type) type { } pub fn parse(self: *Self) anyerror!ParseResult { - const app_command = command.Command{ - .name = self.app.name, - .description = self.app.description, - .help = "", - .action = self.app.action, - .subcommands = self.app.subcommands, - .options = self.app.options, - .positional_args = self.app.positional_args, - }; - try self.command_path.append(&app_command); + try self.command_path.append(&self.app.command); - self.validate_command(&app_command); _ = self.next_arg(); var args_only = false; while (self.next_arg()) |arg| { @@ -98,40 +88,56 @@ pub fn Parser(comptime Iterator: type) type { } } } - if (cmd.positional_args) |pargs| { - for (pargs) |parg| { - try parg.value_ref.finalize(self.alloc); - - if (parg.required and parg.value_ref.element_count == 0) { - self.fail("missing required positional argument '{s}'", .{parg.name}); + switch (cmd.target) { + .action => |act| { + if (act.positional_args) |pargs| { + for (pargs) |parg| { + try parg.value_ref.finalize(self.alloc); + + if (parg.required and parg.value_ref.element_count == 0) { + self.fail("missing required positional argument '{s}'", .{parg.name}); + } + } } - } + }, + .subcommands => {}, } } - if (self.current_command().action) |action| { - return action; - } else { - self.fail("command '{s}': no subcommand provided", .{self.current_command().name}); - unreachable; + switch (self.current_command().target) { + .action => |act| { + return act.exec; + }, + .subcommands => { + self.fail("command '{s}': no subcommand provided", .{self.current_command().name}); + unreachable; + }, } } fn handlePositionalArgument(self: *Self, arg: []const u8) !void { - if (self.current_command().positional_args) |posArgs| { - if (self.position_argument_ix >= posArgs.len) { - self.fail("unexpected positional argument '{s}'", .{arg}); - } + const cmd = self.current_command(); + switch (cmd.target) { + .subcommands => { + self.fail("command '{s}' cannot have positional arguments", .{cmd.name}); + }, + .action => |act| { + if (act.positional_args) |posArgs| { + if (self.position_argument_ix >= posArgs.len) { + self.fail("unexpected positional argument '{s}'", .{arg}); + } - var posArg = posArgs[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) }); - unreachable; - }; - if (posArgRef.value_type == vref.ValueType.single) { - self.position_argument_ix += 1; - } + var posArg = posArgs[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) }); + unreachable; + }; + if (posArgRef.value_type == vref.ValueType.single) { + self.position_argument_ix += 1; + } + } + }, } } @@ -175,17 +181,20 @@ pub fn Parser(comptime Iterator: type) type { args_only = true; }, .other => |some_name| { - const has_positional_arguments = if (self.current_command().positional_args) |pargs| - self.position_argument_ix > 0 or pargs[0].value_ref.element_count > 0 - else - false; - if (has_positional_arguments) { - try self.handlePositionalArgument(some_name); - } else if (find_subcommand(self.current_command(), some_name)) |cmd| { - self.validate_command(cmd); // TODO: validation can happen at comptime for all commands - try self.command_path.append(cmd); - } else { - try self.handlePositionalArgument(some_name); + const cmd = self.current_command(); + switch (cmd.target) { + .subcommands => |cmds| { + for (cmds) |sc| { + if (std.mem.eql(u8, sc.name, some_name)) { + try self.command_path.append(sc); + return false; + } + } + self.fail("no such subcommand '{s}'", .{some_name}); + }, + .action => { + try self.handlePositionalArgument(some_name); + }, } }, }; @@ -267,18 +276,6 @@ pub fn Parser(comptime Iterator: type) type { unreachable; } - fn validate_command(self: *const Self, cmd: *const command.Command) void { - if (cmd.subcommands == null) { - if (cmd.action == null) { - self.fail("command '{s}' has neither subcommands no an aciton assigned", .{cmd.name}); - } - } else { - if (cmd.action != null) { - self.fail("command '{s}' has subcommands and an action assigned. Commands with subcommands are not allowed to have action.", .{cmd.name}); - } - } - } - /// Set boolean options provided like `-acde` fn set_concatenated_boolean_options(self: *const Self, cmd: *const command.Command, options: []const u8) void { for (options) |alias| { @@ -292,14 +289,3 @@ pub fn Parser(comptime Iterator: type) type { } }; } - -fn find_subcommand(cmd: *const command.Command, subcommand_name: []const u8) ?*const command.Command { - if (cmd.subcommands) |sc_list| { - for (sc_list) |sc| { - if (std.mem.eql(u8, sc.name, subcommand_name)) { - return sc; - } - } - } - return null; -} From 3fd78c05f8591c5f80f91c266aa3da21e795b723 Mon Sep 17 00:00:00 2001 From: Alexei Samokvalov Date: Mon, 23 Oct 2023 08:27:05 +0200 Subject: [PATCH 11/20] Fix tests --- src/tests.zig | 87 +++++++++++++++++++-------------------------------- 1 file changed, 33 insertions(+), 54 deletions(-) diff --git a/src/tests.zig b/src/tests.zig index 1114304..e238e65 100644 --- a/src/tests.zig +++ b/src/tests.zig @@ -25,7 +25,7 @@ const StringSliceIterator = struct { } }; -fn run(app: *command.App, items: []const []const u8) !void { +fn run(app: *const command.App, items: []const []const u8) !void { var it = StringSliceIterator{ .items = items, }; @@ -37,6 +37,27 @@ fn run(app: *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 app = command.App{ + .command = command.Command{ + .name = "cmd", + .description = command.Description{ .one_line = "short help" }, + .options = options, + .target = command.CommandTarget{ + .action = command.CommandAction{ + .positional_args = pargs, + .exec = dummy_action, + }, + }, + }, + }; + try run(&app, input); +} + +fn runOptions(input: []const []const u8, options: []const *command.Option) !void { + try runOptionsPArgs(input, options, null); +} + test "long option" { var aa: []const u8 = "test"; var opt = command.Option{ @@ -44,16 +65,11 @@ test "long option" { .help = "option aa", .value_ref = mkRef(&aa), }; - var cmd = command.App{ - .name = "abc", - .options = &.{&opt}, - .action = dummy_action, - }; - _ = try run(&cmd, &.{ "cmd", "--aa", "val" }); + try runOptions(&.{ "cmd", "--aa", "val" }, &.{&opt}); try std.testing.expectEqualStrings("val", aa); - _ = try run(&cmd, &.{ "cmd", "--aa=bb" }); + try runOptions(&.{ "cmd", "--aa=bb" }, &.{&opt}); try std.testing.expectEqualStrings("bb", aa); } @@ -65,16 +81,11 @@ test "short option" { .help = "option aa", .value_ref = mkRef(&aa), }; - var app = command.App{ - .name = "abc", - .options = &.{&opt}, - .action = dummy_action, - }; - _ = try run(&app, &.{ "abc", "-a", "val" }); + try runOptions(&.{ "abc", "-a", "val" }, &.{&opt}); try std.testing.expectEqualStrings("val", aa); - _ = try run(&app, &.{ "abc", "-a=bb" }); + try runOptions(&.{ "abc", "-a=bb" }, &.{&opt}); try std.testing.expectEqualStrings("bb", aa); } @@ -93,13 +104,8 @@ test "concatenated aliases" { .help = "option aa", .value_ref = mkRef(&aa), }; - var app = command.App{ - .name = "abc", - .options = &.{ &bbopt, &opt }, - .action = dummy_action, - }; - _ = try run(&app, &.{ "abc", "-ba", "val" }); + try runOptions(&.{ "abc", "-ba", "val" }, &.{ &opt, &bbopt }); try std.testing.expectEqualStrings("val", aa); try expect(bb); } @@ -117,13 +123,8 @@ test "int and float" { .help = "option bb", .value_ref = mkRef(&bb), }; - var app = command.App{ - .name = "abc", - .options = &.{ &aa_opt, &bb_opt }, - .action = dummy_action, - }; - _ = try run(&app, &.{ "abc", "--aa=34", "--bb", "15.25" }); + try runOptions(&.{ "abc", "--aa=34", "--bb", "15.25" }, &.{ &aa_opt, &bb_opt }); try expect(34 == aa); try expect(15.25 == bb); } @@ -148,13 +149,8 @@ test "optional values" { .help = "option cc", .value_ref = mkRef(&cc), }; - var app = command.App{ - .name = "abc", - .options = &.{ &aa_opt, &bb_opt, &cc_opt }, - .action = dummy_action, - }; - _ = try run(&app, &.{ "abc", "--aa=34", "--bb", "15.25" }); + 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); @@ -168,20 +164,14 @@ test "int list" { .help = "option aa", .value_ref = mkRef(&aa), }; - var app = command.App{ - .name = "abc", - .options = &.{&aa_opt}, - .action = dummy_action, - }; - _ = try run(&app, &.{ "abc", "--aa=100", "--aa", "200", "-a", "300", "-a=400" }); + 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); try expect(aa[2] == 300); try expect(aa[3] == 400); - // FIXME: it tries to deallocated u64 while the memory was allocated using u8 alignment alloc.free(aa); } @@ -193,13 +183,8 @@ test "string list" { .help = "option aa", .value_ref = mkRef(&aa), }; - var app = command.App{ - .name = "abc", - .options = &.{&aa_opt}, - .action = dummy_action, - }; - _ = try run(&app, &.{ "abc", "--aa=a1", "--aa", "a2", "-a", "a3", "-a=a4" }); + 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]); @@ -235,9 +220,8 @@ test "mix positional arguments and options" { .help = "help", .value_ref = mkRef(&args), }; - var app = command.App{ .name = "abc", .options = &.{ &aa, &bb }, .action = dummy_action, .positional_args = &.{ &parg1, &parg2 } }; - try run(&app, &.{ "cmd", "--bb", "tt", "178", "-a", "val", "arg2", "--", "--arg3", "-arg4" }); + 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); @@ -261,13 +245,8 @@ test "parse enums" { .help = "option aa", .value_ref = mkRef(&aa), }; - var app = command.App{ - .name = "abc", - .options = &.{&aa_opt}, - .action = dummy_action, - }; - _ = try run(&app, &.{ "abc", "--aa=cc", "--aa", "dd" }); + 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); From 1ee37012cfeeae8e9b6c15b98b713cef49e6a43e Mon Sep 17 00:00:00 2001 From: Alexei Samokvalov Date: Wed, 22 Nov 2023 09:27:58 +0100 Subject: [PATCH 12/20] Introduce PositionalArgs struct --- example/simple.zig | 5 ++++- src/command.zig | 10 ++++++++-- src/help.zig | 19 +++++++++++++++---- src/parser.zig | 14 ++++++++++---- src/tests.zig | 3 ++- 5 files changed, 39 insertions(+), 12 deletions(-) diff --git a/example/simple.zig b/example/simple.zig index d054e51..8e7661c 100644 --- a/example/simple.zig +++ b/example/simple.zig @@ -90,7 +90,10 @@ var sub3 = cli.Command{ }, .target = cli.CommandTarget{ .action = cli.CommandAction{ - .positional_args = &.{ &arg1, &arg2 }, + .positional_args = cli.PositionalArgs{ + .args = &.{ &arg1, &arg2 }, + .first_optional_arg = &arg2, + }, .exec = run_sub3, }, }, diff --git a/src/command.zig b/src/command.zig index 0e5502d..c4e117e 100644 --- a/src/command.zig +++ b/src/command.zig @@ -45,7 +45,7 @@ pub const CommandTarget = union(enum) { }; pub const CommandAction = struct { - positional_args: ?[]const *PositionalArg = null, + positional_args: ?PositionalArgs = null, exec: ExecFn, }; @@ -61,9 +61,15 @@ pub const Option = struct { envvar: ?[]const u8 = null, }; +pub const PositionalArgs = struct { + args: []const *PositionalArg, + + /// If not set, all positional arguments are considered as required. + first_optional_arg: ?*const PositionalArg = null, +}; + pub const PositionalArg = struct { name: []const u8, help: []const u8, value_ref: ValueRef, - required: bool = false, }; diff --git a/src/help.zig b/src/help.zig index 514fd46..81ccda2 100644 --- a/src/help.zig +++ b/src/help.zig @@ -49,12 +49,23 @@ const HelpPrinter = struct { switch (cmd.target) { .action => |act| { if (act.positional_args) |pargs| { - for (pargs) |parg| { - self.printer.format(" <{s}>", .{parg.name}); + var closeOpt = false; + for (pargs.args) |parg| { + self.printer.write(" "); + if (pargs.first_optional_arg) |opt| { + if (opt == parg) { + self.printer.write("["); + closeOpt = true; + } + } + self.printer.format("<{s}>", .{parg.name}); if (parg.value_ref.value_type == value_ref.ValueType.multi) { self.printer.write("..."); } } + if (closeOpt) { + self.printer.write("]"); + } } }, .subcommands => {}, @@ -72,10 +83,10 @@ const HelpPrinter = struct { if (act.positional_args) |pargs| { self.printer.printInColor(self.help_config.color_section, "\nARGUMENTS:\n"); var max_arg_width: usize = 0; - for (pargs) |parg| { + for (pargs.args) |parg| { max_arg_width = @max(max_arg_width, parg.name.len); } - for (pargs) |parg| { + for (pargs.args) |parg| { self.printer.write(" "); self.printer.printInColor(self.help_config.color_option, parg.name); self.printer.printSpaces(max_arg_width - parg.name.len + 3); diff --git a/src/parser.zig b/src/parser.zig index 6d65be6..35a642e 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -91,10 +91,16 @@ pub fn Parser(comptime Iterator: type) type { switch (cmd.target) { .action => |act| { if (act.positional_args) |pargs| { - for (pargs) |parg| { + var optional = false; + for (pargs.args) |parg| { try parg.value_ref.finalize(self.alloc); - if (parg.required and parg.value_ref.element_count == 0) { + if (pargs.first_optional_arg) |first_opt| { + if (parg == first_opt) { + optional = true; + } + } + if (!optional and parg.value_ref.element_count == 0) { self.fail("missing required positional argument '{s}'", .{parg.name}); } } @@ -123,11 +129,11 @@ pub fn Parser(comptime Iterator: type) type { }, .action => |act| { if (act.positional_args) |posArgs| { - if (self.position_argument_ix >= posArgs.len) { + if (self.position_argument_ix >= posArgs.args.len) { self.fail("unexpected positional argument '{s}'", .{arg}); } - var posArg = posArgs[self.position_argument_ix]; + var posArg = posArgs.args[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) }); diff --git a/src/tests.zig b/src/tests.zig index e238e65..085b9e2 100644 --- a/src/tests.zig +++ b/src/tests.zig @@ -38,6 +38,7 @@ 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; const app = command.App{ .command = command.Command{ .name = "cmd", @@ -45,7 +46,7 @@ fn runOptionsPArgs(input: []const []const u8, options: []const *command.Option, .options = options, .target = command.CommandTarget{ .action = command.CommandAction{ - .positional_args = pargs, + .positional_args = pa, .exec = dummy_action, }, }, From efb74c9250526d4429c3a43ab454128cb7428848 Mon Sep 17 00:00:00 2001 From: Alexei Samokvalov Date: Wed, 22 Nov 2023 10:08:13 +0100 Subject: [PATCH 13/20] Fix code for zig 0.12 --- src/help.zig | 2 +- src/parser.zig | 2 +- src/tests.zig | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/help.zig b/src/help.zig index 30c853c..be522a7 100644 --- a/src/help.zig +++ b/src/help.zig @@ -44,7 +44,7 @@ const HelpPrinter = struct { for (command_path) |cmd| { self.printer.format("{s} ", .{cmd.name}); } - var cmd = command_path[command_path.len - 1]; + const cmd = command_path[command_path.len - 1]; self.printer.format("[OPTIONS]", .{}); switch (cmd.target) { .action => |act| { diff --git a/src/parser.zig b/src/parser.zig index 35a642e..456da58 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -17,7 +17,7 @@ pub fn run(app: *const command.App, alloc: Allocator) anyerror!void { var cr = try Parser(std.process.ArgIterator).init(app, iter, alloc); defer cr.deinit(); - var action = try cr.parse(); + const action = try cr.parse(); return action(); } diff --git a/src/tests.zig b/src/tests.zig index 085b9e2..5eafe95 100644 --- a/src/tests.zig +++ b/src/tests.zig @@ -26,7 +26,7 @@ const StringSliceIterator = struct { }; fn run(app: *const command.App, items: []const []const u8) !void { - var it = StringSliceIterator{ + const it = StringSliceIterator{ .items = items, }; From 4914dd972c4b4579f3fded307c7e704fcb2001bf Mon Sep 17 00:00:00 2001 From: Alexei Samokvalov Date: Wed, 22 Nov 2023 11:04:29 +0100 Subject: [PATCH 14/20] Search for options in all provided commands --- src/parser.zig | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/parser.zig b/src/parser.zig index 456da58..f41c860 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -213,7 +213,7 @@ pub fn Parser(comptime Iterator: type) type { fn process_option(self: *Self, option_interpretation: *const argp.OptionInterpretation) !void { var opt: *command.Option = switch (option_interpretation.option_type) { - .long => self.find_option_by_name(self.current_command(), option_interpretation.name), + .long => self.find_option_by_name(option_interpretation.name), .short => a: { self.set_concatenated_boolean_options(self.current_command(), option_interpretation.name[0 .. option_interpretation.name.len - 1]); break :a self.find_option_by_alias(self.current_command(), option_interpretation.name[option_interpretation.name.len - 1]); @@ -250,14 +250,18 @@ pub fn Parser(comptime Iterator: type) type { std.os.exit(1); } - fn find_option_by_name(self: *const Self, cmd: *const command.Command, option_name: []const u8) *command.Option { + fn find_option_by_name(self: *const Self, option_name: []const u8) *command.Option { if (std.mem.eql(u8, "help", option_name)) { return &help_option; } - if (cmd.options) |option_list| { - for (option_list) |option| { - if (std.mem.eql(u8, option.long_name, option_name)) { - return option; + var ix = self.command_path.items.len - 1; + while (ix >= 0) : (ix -= 1) { + const cmd = self.command_path.items[ix]; + if (cmd.options) |option_list| { + for (option_list) |option| { + if (std.mem.eql(u8, option.long_name, option_name)) { + return option; + } } } } From 5c6de1399110f314065eeb0d1e4c0b94029eea5a Mon Sep 17 00:00:00 2001 From: Alexei Samokvalov Date: Wed, 22 Nov 2023 11:17:29 +0100 Subject: [PATCH 15/20] Add standalone example --- ...{build-master.yaml => build-and-test.yaml} | 4 +- build.zig | 4 +- {example => examples}/short.zig | 0 {example => examples}/simple.zig | 0 examples/standalone/build.zig | 74 +++++++++++++++++++ examples/standalone/build.zig.zon | 11 +++ examples/standalone/src/main.zig | 41 ++++++++++ 7 files changed, 131 insertions(+), 3 deletions(-) rename .github/workflows/{build-master.yaml => build-and-test.yaml} (84%) rename {example => examples}/short.zig (100%) rename {example => examples}/simple.zig (100%) create mode 100644 examples/standalone/build.zig create mode 100644 examples/standalone/build.zig.zon create mode 100644 examples/standalone/src/main.zig diff --git a/.github/workflows/build-master.yaml b/.github/workflows/build-and-test.yaml similarity index 84% rename from .github/workflows/build-master.yaml rename to .github/workflows/build-and-test.yaml index 754df2b..b3a0b60 100644 --- a/.github/workflows/build-master.yaml +++ b/.github/workflows/build-and-test.yaml @@ -1,4 +1,4 @@ -name: build-master +name: build-and-test on: push: branches: @@ -18,6 +18,7 @@ jobs: - run: zig fmt --check *.zig src/*.zig - run: zig build - run: zig build test + - run: cd examples/standalone && zig build validate_and_test_011: runs-on: ubuntu-latest steps: @@ -28,3 +29,4 @@ jobs: - run: zig fmt --check *.zig src/*.zig - run: zig build - run: zig build test + - run: cd examples/standalone && zig build diff --git a/build.zig b/build.zig index e2618c7..f947b7c 100644 --- a/build.zig +++ b/build.zig @@ -27,7 +27,7 @@ pub fn build(b: *std.Build) void { const simple = b.addExecutable(.{ .name = "simple", - .root_source_file = .{ .path = "example/simple.zig" }, + .root_source_file = .{ .path = "examples/simple.zig" }, .optimize = optimize, }); simple.addModule("zig-cli", module); @@ -35,7 +35,7 @@ pub fn build(b: *std.Build) void { const short = b.addExecutable(.{ .name = "short", - .root_source_file = .{ .path = "example/short.zig" }, + .root_source_file = .{ .path = "examples/short.zig" }, .optimize = optimize, }); short.addModule("zig-cli", module); diff --git a/example/short.zig b/examples/short.zig similarity index 100% rename from example/short.zig rename to examples/short.zig diff --git a/example/simple.zig b/examples/simple.zig similarity index 100% rename from example/simple.zig rename to examples/simple.zig diff --git a/examples/standalone/build.zig b/examples/standalone/build.zig new file mode 100644 index 0000000..9150213 --- /dev/null +++ b/examples/standalone/build.zig @@ -0,0 +1,74 @@ +const std = @import("std"); + +// Although this function looks imperative, note that its job is to +// declaratively construct a build graph that will be executed by an external +// runner. +pub fn build(b: *std.Build) void { + // Standard target options allows the person running `zig build` to choose + // what target to build for. Here we do not override the defaults, which + // means any target is allowed, and the default is native. Other options + // for restricting supported target set are available. + const target = b.standardTargetOptions(.{}); + + // Standard optimization options allow the person running `zig build` to select + // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not + // set a preferred release mode, allowing the user to decide how to optimize. + const optimize = b.standardOptimizeOption(.{}); + + const zigcli_dep = b.dependency("zig-cli", .{ .target = target }); + const zigcli_mod = zigcli_dep.module("zig-cli"); + + const exe = b.addExecutable(.{ + .name = "standalone", + // In this case the main source file is merely a path, however, in more + // complicated build scripts, this could be a generated file. + .root_source_file = .{ .path = "src/main.zig" }, + .target = target, + .optimize = optimize, + }); + exe.addModule("zig-cli", zigcli_mod); + + // This declares intent for the executable to be installed into the + // standard location when the user invokes the "install" step (the default + // step when running `zig build`). + b.installArtifact(exe); + + // This *creates* a Run step in the build graph, to be executed when another + // step is evaluated that depends on it. The next line below will establish + // such a dependency. + const run_cmd = b.addRunArtifact(exe); + + // By making the run step depend on the install step, it will be run from the + // installation directory rather than directly from within the cache directory. + // This is not necessary, however, if the application depends on other installed + // files, this ensures they will be present and in the expected location. + run_cmd.step.dependOn(b.getInstallStep()); + + // This allows the user to pass arguments to the application in the build + // command itself, like this: `zig build run -- arg1 arg2 etc` + if (b.args) |args| { + run_cmd.addArgs(args); + } + + // This creates a build step. It will be visible in the `zig build --help` menu, + // and can be selected like this: `zig build run` + // This will evaluate the `run` step rather than the default, which is "install". + const run_step = b.step("run", "Run the app"); + run_step.dependOn(&run_cmd.step); + + // Creates a step for unit testing. This only builds the test executable + // but does not run it. + const unit_tests = b.addTest(.{ + .root_source_file = .{ .path = "src/main.zig" }, + .target = target, + .optimize = optimize, + }); + + const run_unit_tests = b.addRunArtifact(unit_tests); + + // Similar to creating the run step earlier, this exposes a `test` step to + // the `zig build --help` menu, providing a way for the user to request + // running the unit tests. + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&run_unit_tests.step); +} diff --git a/examples/standalone/build.zig.zon b/examples/standalone/build.zig.zon new file mode 100644 index 0000000..cf7adff --- /dev/null +++ b/examples/standalone/build.zig.zon @@ -0,0 +1,11 @@ +.{ + .name = "standalone", + .version = "0.1.0", + .paths = .{"./src"}, + .dependencies = .{ + .@"zig-cli" = .{ + .url = "https://github.com/sam701/zig-cli/archive/4914dd972c4b4579f3fded307c7e704fcb2001bf.tar.gz", + .hash = "1220ba256158526e02fca0bb823efd244b3a73256e9d9fc7ba155d774dd6788e3185", + }, + }, +} diff --git a/examples/standalone/src/main.zig b/examples/standalone/src/main.zig new file mode 100644 index 0000000..dfa51b1 --- /dev/null +++ b/examples/standalone/src/main.zig @@ -0,0 +1,41 @@ +const std = @import("std"); +const cli = @import("zig-cli"); + +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; +const allocator = gpa.allocator(); + +var config = struct { + host: []const u8 = "localhost", + port: u16 = undefined, +}{}; +var host = cli.Option{ + .long_name = "host", + .help = "host to listen on", + .value_ref = cli.mkRef(&config.host), +}; +var port = cli.Option{ + .long_name = "port", + .help = "port to bind to", + .required = true, + .value_ref = cli.mkRef(&config.port), +}; +var app = &cli.App{ + .command = cli.Command{ + .name = "short", + .options = &.{ &host, &port }, + .description = cli.Description{ .one_line = "a short example" }, + .target = cli.CommandTarget{ + .action = cli.CommandAction{ + .exec = run_server, + }, + }, + }, +}; + +pub fn main() !void { + return cli.run(app, allocator); +} + +fn run_server() !void { + std.log.debug("server is listening on {s}:{}", .{ config.host, config.port }); +} From ca7493bcb8eba79222a84f97fda146116a19853e Mon Sep 17 00:00:00 2001 From: Alexei Samokvalov Date: Wed, 22 Nov 2023 11:29:27 +0100 Subject: [PATCH 16/20] Do not run standalone test with zig master --- .github/workflows/build-and-test.yaml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-and-test.yaml b/.github/workflows/build-and-test.yaml index b3a0b60..eb879e6 100644 --- a/.github/workflows/build-and-test.yaml +++ b/.github/workflows/build-and-test.yaml @@ -8,7 +8,7 @@ on: - cron: 0 4 * * * jobs: - validate_and_test_master: + validate_and_test_with_zig_master: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -18,8 +18,7 @@ jobs: - run: zig fmt --check *.zig src/*.zig - run: zig build - run: zig build test - - run: cd examples/standalone && zig build - validate_and_test_011: + validate_and_test_with_zig_011: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 From 365050a37207455ea5b5f9f0d4db2a0eb0387a6d Mon Sep 17 00:00:00 2001 From: Alexei Samokvalov Date: Wed, 22 Nov 2023 11:41:39 +0100 Subject: [PATCH 17/20] Update README --- README.md | 15 ++++++++++----- examples/short.zig | 5 ++--- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 4ba8b7b..ff24697 100644 --- a/README.md +++ b/README.md @@ -44,16 +44,21 @@ var port = cli.Option{ .value_ref = cli.mkRef(&config.port), }; var app = &cli.App{ - .name = "short", - .options = &.{ &host, &port }, - .action = run_server, + .command = cli.Command{ + .name = "short", + .options = &.{ &host, &port }, + .description = cli.Description{ .one_line = "a short example" }, + .target = cli.CommandTarget{ + .action = cli.CommandAction{ .exec = run_server }, + }, + }, }; pub fn main() !void { return cli.run(app, allocator); } -fn run_server(_: []const []const u8) !void { +fn run_server() !void { std.log.debug("server is listening on {s}:{}", .{ config.host, config.port }); } ``` @@ -84,4 +89,4 @@ OPTIONS: ``` ## License -MIT \ No newline at end of file +MIT diff --git a/examples/short.zig b/examples/short.zig index dfa51b1..3a393ce 100644 --- a/examples/short.zig +++ b/examples/short.zig @@ -8,6 +8,7 @@ var config = struct { host: []const u8 = "localhost", port: u16 = undefined, }{}; + var host = cli.Option{ .long_name = "host", .help = "host to listen on", @@ -25,9 +26,7 @@ var app = &cli.App{ .options = &.{ &host, &port }, .description = cli.Description{ .one_line = "a short example" }, .target = cli.CommandTarget{ - .action = cli.CommandAction{ - .exec = run_server, - }, + .action = cli.CommandAction{ .exec = run_server }, }, }, }; From e32f7454d690761ec2b4eab8600e0610d0d233bd Mon Sep 17 00:00:00 2001 From: Alexei Samokvalov Date: Wed, 22 Nov 2023 11:55:26 +0100 Subject: [PATCH 18/20] Make command description optional --- README.md | 1 - examples/short.zig | 1 - examples/simple.zig | 3 --- examples/standalone/src/main.zig | 6 ++---- src/command.zig | 2 +- src/help.zig | 23 ++++++++++++++--------- 6 files changed, 17 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index ff24697..4e9d325 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,6 @@ var app = &cli.App{ .command = cli.Command{ .name = "short", .options = &.{ &host, &port }, - .description = cli.Description{ .one_line = "a short example" }, .target = cli.CommandTarget{ .action = cli.CommandAction{ .exec = run_server }, }, diff --git a/examples/short.zig b/examples/short.zig index 3a393ce..095408d 100644 --- a/examples/short.zig +++ b/examples/short.zig @@ -24,7 +24,6 @@ var app = &cli.App{ .command = cli.Command{ .name = "short", .options = &.{ &host, &port }, - .description = cli.Description{ .one_line = "a short example" }, .target = cli.CommandTarget{ .action = cli.CommandAction{ .exec = run_server }, }, diff --git a/examples/simple.zig b/examples/simple.zig index 8e7661c..7c852ed 100644 --- a/examples/simple.zig +++ b/examples/simple.zig @@ -73,9 +73,6 @@ var sub1 = cli.Command{ var sub2 = cli.Command{ .name = "sub2", - .description = cli.Description{ - .one_line = "sub2 help", - }, .target = cli.CommandTarget{ .action = cli.CommandAction{ .exec = run_sub2, diff --git a/examples/standalone/src/main.zig b/examples/standalone/src/main.zig index dfa51b1..095408d 100644 --- a/examples/standalone/src/main.zig +++ b/examples/standalone/src/main.zig @@ -8,6 +8,7 @@ var config = struct { host: []const u8 = "localhost", port: u16 = undefined, }{}; + var host = cli.Option{ .long_name = "host", .help = "host to listen on", @@ -23,11 +24,8 @@ var app = &cli.App{ .command = cli.Command{ .name = "short", .options = &.{ &host, &port }, - .description = cli.Description{ .one_line = "a short example" }, .target = cli.CommandTarget{ - .action = cli.CommandAction{ - .exec = run_server, - }, + .action = cli.CommandAction{ .exec = run_server }, }, }, }; diff --git a/src/command.zig b/src/command.zig index c4e117e..b15635d 100644 --- a/src/command.zig +++ b/src/command.zig @@ -29,7 +29,7 @@ pub const HelpConfig = struct { pub const Command = struct { name: []const u8, - description: Description, + description: ?Description = null, options: ?[]const *Option = null, target: CommandTarget, }; diff --git a/src/help.zig b/src/help.zig index be522a7..b1c0aaa 100644 --- a/src/help.zig +++ b/src/help.zig @@ -73,9 +73,11 @@ const HelpPrinter = struct { self.printer.printNewLine(); self.printer.printColor(color_clear); - self.printer.format("\n{s}\n", .{cmd.description.one_line}); - if (cmd.description.detailed) |det| { - self.printer.format("\n{s}\n", .{det}); + if (cmd.description) |desc| { + self.printer.format("\n{s}\n", .{desc.one_line}); + if (desc.detailed) |det| { + self.printer.format("\n{s}\n", .{det}); + } } switch (cmd.target) { @@ -107,13 +109,16 @@ const HelpPrinter = struct { self.printer.printColor(self.help_config.color_option); self.printer.format(" {s}", .{sc.name}); self.printer.printColor(color_clear); - var i: usize = 0; - while (i < cmd_column_width - sc.name.len) { - self.printer.write(" "); - i += 1; - } + if (sc.description) |desc| { + var i: usize = 0; + while (i < cmd_column_width - sc.name.len) { + self.printer.write(" "); + i += 1; + } - self.printer.format("{s}\n", .{sc.description.one_line}); + self.printer.format("{s}", .{desc.one_line}); + } + self.printer.printNewLine(); } }, } From fcc3ba8dcdd10800d1acf724d6261107da4283d9 Mon Sep 17 00:00:00 2001 From: Alexei Samokvalov Date: Wed, 22 Nov 2023 11:59:08 +0100 Subject: [PATCH 19/20] Fix zon hash --- examples/standalone/build.zig.zon | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/standalone/build.zig.zon b/examples/standalone/build.zig.zon index cf7adff..7518790 100644 --- a/examples/standalone/build.zig.zon +++ b/examples/standalone/build.zig.zon @@ -4,8 +4,8 @@ .paths = .{"./src"}, .dependencies = .{ .@"zig-cli" = .{ - .url = "https://github.com/sam701/zig-cli/archive/4914dd972c4b4579f3fded307c7e704fcb2001bf.tar.gz", - .hash = "1220ba256158526e02fca0bb823efd244b3a73256e9d9fc7ba155d774dd6788e3185", + .url = "https://github.com/sam701/zig-cli/archive/e32f7454d690761ec2b4eab8600e0610d0d233bd.tar.gz", + .hash = "1220bf895404724653daf8e0e054e6c943b8dc13c312c3d32d9e0eaa4448f261fd98", }, }, } From f33ac75bb032a61b51e0bc31bf9d628a6db9a618 Mon Sep 17 00:00:00 2001 From: Alexei Samokvalov Date: Sun, 3 Dec 2023 21:50:41 +0100 Subject: [PATCH 20/20] Document package manager usage --- README.md | 3 +++ examples/standalone/build.zig.zon | 1 + 2 files changed, 4 insertions(+) diff --git a/README.md b/README.md index 4e9d325..66efd1e 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,9 @@ fn run_server() !void { } ``` +### Using with the Zig package manager +See the [`standalone`](./examples/standalone) example in the `examples` folder. + ## Printing help See [`simple.zig`](./example/simple.zig) diff --git a/examples/standalone/build.zig.zon b/examples/standalone/build.zig.zon index 7518790..d617ddb 100644 --- a/examples/standalone/build.zig.zon +++ b/examples/standalone/build.zig.zon @@ -4,6 +4,7 @@ .paths = .{"./src"}, .dependencies = .{ .@"zig-cli" = .{ + // URL pattern: https://github.com/sam701/zig-cli/archive/.tar.gz .url = "https://github.com/sam701/zig-cli/archive/e32f7454d690761ec2b4eab8600e0610d0d233bd.tar.gz", .hash = "1220bf895404724653daf8e0e054e6c943b8dc13c312c3d32d9e0eaa4448f261fd98", },