diff --git a/.github/workflows/simargs.yml b/.github/workflows/simargs.yml index 276ef15..f9d7590 100644 --- a/.github/workflows/simargs.yml +++ b/.github/workflows/simargs.yml @@ -35,4 +35,4 @@ jobs: TEST_BINARY=./zig-out/bin/simargs-demo valgrind --leak-check=full --tool=memcheck \ --show-leak-kinds=all --error-exitcode=1 ${TEST_BINARY} --output a.out \ - hello world + sub1 --a 123 hello world diff --git a/.tool-versions b/.tool-versions index f81adbc..02050d7 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1 +1 @@ -zig master +zig 0.13.0 diff --git a/docs/content/docs/modules/simargs.org b/docs/content/docs/modules/simargs.org index 3a45612..3e51776 100644 --- a/docs/content/docs/modules/simargs.org +++ b/docs/content/docs/modules/simargs.org @@ -1,6 +1,6 @@ #+TITLE: Simargs #+DATE: 2023-10-21T12:04:40+0800 -#+LASTMOD: 2023-10-22T19:30:17+0800 +#+LASTMOD: 2024-08-11T20:30:59+0800 #+WEIGTH: 1 * Simargs @@ -12,26 +12,28 @@ A simple, opinionated, struct-based argument parser in Zig, taking full advantag - =Enum= - Optional fields and fields with default value mean they are optional arguments - Use =comptime= as possible as I can -- Provide =print_help()= out of the box +- Provide =printHelp()= out of the box +- Support sub commands * Usage See [[https://github.com/jiacai2050/zigcli/blob/main/examples/simargs-demo.zig][simargs-demo.zig]]. #+begin_src bash :results verbatim :exports both # Run demo -zig build && ./zig-out/bin/simargs-demo -o /tmp/a.out --user-agent Firefox hello world 2>&1 +zig build run-simargs-demo -- -o /tmp/a.out --user-agent Firefox sub1 --a 123 hello world 2>&1 #+end_src #+RESULTS: #+begin_example ------------------------------Program------------------------------ -./zig-out/bin/simargs-demo +/Users/jiacai/gh/zigcli/.zig-cache/o/bd8a4fb104779110e787d579f1d9c6f0/simargs-demo ------------------------------Arguments------------------------------ verbose: null -user-agent: demo.main__struct_1677.main__struct_1677__enum_1777.Firefox +user-agent: simargs-demo.main__struct_1700.main__struct_1700__enum_1707.Firefox timeout: 30 output: /tmp/a.out help: false +__commands__: simargs-demo.main__struct_1700.main__struct_1700__union_1708{ .sub1 = simargs-demo.main__struct_1700.main__struct_1700__union_1708.main__struct_1700__union_1708__struct_1710{ .a = 123, .help = false } } ------------------------------Positionals------------------------------ 1: hello @@ -39,14 +41,18 @@ user-agent: demo.main__struct_1677.main__struct_1677__enum_1777.Firefox ------------------------------print_help------------------------------ USAGE: - ./zig-out/bin/demo [OPTIONS] [--] [file] + /Users/jiacai/gh/zigcli/.zig-cache/o/bd8a4fb104779110e787d579f1d9c6f0/simargs-demo [OPTIONS] [COMMANDS] + + COMMANDS: + sub1 Subcommand 1 + sub2 Subcommand 2 OPTIONS: - -v, --verbose Make the operation more talkative - -A, --user-agent STRING (valid: Chrome|Firefox|Safari)(default: Firefox) - --timeout INTEGER Max time this request can cost(default: 30) - -o, --output STRING Write to file instead of stdout(required) - -h, --help + -v, --verbose Make the operation more talkative + -A, --user-agent STRING (valid: Chrome|Firefox|Safari)(default: Firefox) + --timeout INTEGER Max time this request can cost(default: 30) + -o, --output STRING Write to file instead of stdout(required) + -h, --help #+end_example * Acknowledgment diff --git a/examples/simargs-demo.zig b/examples/simargs-demo.zig index f38d107..6cefe63 100644 --- a/examples/simargs-demo.zig +++ b/examples/simargs-demo.zig @@ -18,6 +18,23 @@ pub fn main() !void { timeout: ?u16 = 30, // default value output: []const u8, help: bool = false, + version: bool = false, + + // This special field define sub_commands, + // Each union item is a config struct, which is similar with top-level config struct. + __commands__: union(enum) { + sub1: struct { + a: u64, + help: bool = false, + }, + sub2: struct { name: []const u8 }, + + // Define help message for sub commands. + pub const __messages__ = .{ + .sub1 = "Subcommand 1", + .sub2 = "Subcommand 2", + }; + }, // This declares option's short name pub const __shorts__ = .{ @@ -33,7 +50,7 @@ pub fn main() !void { .output = "Write to file instead of stdout", .timeout = "Max time this request can cost", }; - }, "[file]", null); + }, "[file]", "0.1.0"); defer opt.deinit(); const sep = "-" ** 30; @@ -49,12 +66,12 @@ pub fn main() !void { } std.debug.print("\n{s}Positionals{s}\n", .{ sep, sep }); - for (opt.positional_args.items, 0..) |arg, idx| { + for (opt.positional_args, 0..) |arg, idx| { std.debug.print("{d}: {s}\n", .{ idx + 1, arg }); } // Provide a print_help util method std.debug.print("\n{s}print_help{s}\n", .{ sep, sep }); const stdout = std.io.getStdOut(); - try opt.print_help(stdout.writer()); + try opt.printHelp(stdout.writer()); } diff --git a/src/bin/loc.zig b/src/bin/loc.zig index 2861f14..c3619c1 100644 --- a/src/bin/loc.zig +++ b/src/bin/loc.zig @@ -206,10 +206,10 @@ pub fn main() !void { }, "[file or directory]", util.get_build_info()); defer opt.deinit(); - const file_or_dir = if (opt.positional_args.items.len == 0) + const file_or_dir = if (opt.positional_args.len == 0) "." else - opt.positional_args.items[0]; + opt.positional_args[0]; var loc_map = LocMap{}; const dir = fs.cwd().openDir(file_or_dir, .{ .iterate = true }) catch |err| switch (err) { diff --git a/src/bin/repeat.zig b/src/bin/repeat.zig index bc143cc..c70f34e 100644 --- a/src/bin/repeat.zig +++ b/src/bin/repeat.zig @@ -35,9 +35,9 @@ pub fn main() !void { }, "command", util.get_build_info()); defer opt.deinit(); - const argv = if (opt.positional_args.items.len == 0) { + const argv = if (opt.positional_args.len == 0) { return error.NoCommand; - } else opt.positional_args.items; + } else opt.positional_args; var keep_running = true; var i: usize = 0; diff --git a/src/bin/tree.zig b/src/bin/tree.zig index d773919..bd687ee 100644 --- a/src/bin/tree.zig +++ b/src/bin/tree.zig @@ -88,10 +88,10 @@ pub fn main() anyerror!void { ); defer opt.deinit(); - const root_dir = if (opt.positional_args.items.len == 0) + const root_dir = if (opt.positional_args.len == 0) "." else - opt.positional_args.items[0]; + opt.positional_args[0]; var writer = std.io.bufferedWriter(std.io.getStdOut().writer()); _ = try writer.write(root_dir); diff --git a/src/mod/simargs.zig b/src/mod/simargs.zig index b3a6dfc..be74651 100644 --- a/src/mod/simargs.zig +++ b/src/mod/simargs.zig @@ -4,7 +4,16 @@ const std = @import("std"); const testing = std.testing; const is_test = @import("builtin").is_test; -const ParseError = error{ NoProgram, NoOption, MissingRequiredOption, MissingOptionValue, InvalidEnumValue }; +const ParseError = error{ + NoProgram, + NoOption, + MissingRequiredOption, + MissingOptionValue, + InvalidEnumValue, + MissingSubCommand, +}; + +const COMMAND_FIELD_NAME = "__commands__"; const OptionError = ParseError || std.mem.Allocator.Error || std.fmt.ParseIntError || std.fmt.ParseFloatError || std.process.ArgIterator.InitError; @@ -14,11 +23,11 @@ pub fn parse( allocator: std.mem.Allocator, comptime T: type, comptime arg_prompt: ?[]const u8, - version: ?[]const u8, -) OptionError!StructArguments(T, arg_prompt) { + comptime version: ?[]const u8, +) OptionError!StructArguments(T, version, arg_prompt) { const args = try std.process.argsAlloc(allocator); - var parser = OptionParser(T).init(allocator, args); - return parser.parse(arg_prompt, version); + var parser = OptionParser(T).init(allocator); + return parser.parse(arg_prompt, version, args); } const OptionField = struct { @@ -30,23 +39,38 @@ const OptionField = struct { is_set: bool = false, }; -fn parseOptionFields(comptime T: type) [std.meta.fields(T).len]OptionField { +fn getOptionLength(comptime T: type) usize { + const option_type_info = @typeInfo(T); + if (option_type_info != .Struct) { + @compileError("option should be defined using struct, found " ++ @typeName(T)); + } + inline for (std.meta.fields(T)) |fld| { + if (std.mem.eql(u8, fld.name, COMMAND_FIELD_NAME)) { + return std.meta.fields(T).len - 1; + } + } + + return std.meta.fields(T).len; +} + +fn buildOptionFields(comptime T: type) [getOptionLength(T)]OptionField { const option_type_info = @typeInfo(T); if (option_type_info != .Struct) { @compileError("option should be defined using struct, found " ++ @typeName(T)); } - var opt_fields: [std.meta.fields(T).len]OptionField = undefined; + var opt_fields: [getOptionLength(T)]OptionField = undefined; inline for (option_type_info.Struct.fields, 0..) |fld, idx| { const long_name = fld.name; - const opt_type = OptionType.from_zig_type( - fld.type, - ); + if (std.mem.eql(u8, fld.name, COMMAND_FIELD_NAME)) { + continue; + } + const opt_type = OptionType.from_zig_type(fld.type); opt_fields[idx] = .{ .long_name = long_name, .opt_type = opt_type, // option with default value is set automatically - .is_set = !(fld.default_value == null), + .is_set = fld.default_value != null, }; } @@ -98,8 +122,8 @@ fn parseOptionFields(comptime T: type) [std.meta.fields(T).len]OptionField { return opt_fields; } -test "parse option fields" { - const fields = comptime parseOptionFields(struct { +test "build option fields" { + const fields = comptime buildOptionFields(struct { verbose: bool, help: ?bool, timeout: u16, @@ -129,15 +153,191 @@ fn NonOptionType(comptime opt_type: type) type { }; } +const MessageHelper = struct { + allocator: std.mem.Allocator, + program: []const u8, + arg_prompt: ?[]const u8, + version: ?[]const u8, + + fn init( + allocator: std.mem.Allocator, + program: []const u8, + version: ?[]const u8, + arg_prompt: ?[]const u8, + ) MessageHelper { + return .{ + .allocator = allocator, + .program = program, + .version = version, + .arg_prompt = arg_prompt, + }; + } + + fn printDefault(comptime f: std.builtin.Type.StructField, writer: anytype) !void { + if (f.default_value == null) { + if (@typeInfo(f.type) != .Optional) { + try writer.writeAll("(required)"); + } + return; + } + + // Don't print default for false (?)bool + const default = @as(*align(1) const f.type, @ptrCast(f.default_value.?)).*; + switch (@typeInfo(f.type)) { + .Bool => if (!default) return, + .Optional => |opt| if (@typeInfo(opt.child) == .Bool) + if (!(default orelse false)) return, + else => {}, + } + + const format = "(default: " ++ switch (f.type) { + []const u8 => "{s}", + ?[]const u8 => "{?s}", + else => if (@typeInfo(NonOptionType(f.type)) == .Enum) + "{s}" + else + "{any}", + } ++ ")"; + + try std.fmt.format(writer, format, .{switch (@typeInfo(f.type)) { + .Enum => @tagName(default), + .Optional => |opt| if (@typeInfo(opt.child) == .Enum) + @tagName(default.?) + else + default, + else => default, + }}); + } + + pub fn printVersion( + self: MessageHelper, + ) !void { + const stdout = std.io.getStdOut(); + if (self.version) |v| { + try stdout.writer().writeAll(v); + } else { + try stdout.writer().writeAll("Unknown"); + } + } + pub fn printHelp( + self: MessageHelper, + comptime T: type, + sub_cmd_name: ?[]const u8, + writer: anytype, + ) !void { + const fields = comptime buildOptionFields(T); + const sub_cmds = if (@hasField(T, COMMAND_FIELD_NAME)) blk: { + inline for (std.meta.fields(T)) |fld| { + if (comptime std.mem.eql(u8, fld.name, COMMAND_FIELD_NAME)) { + break :blk subCommandsHelpMsg( + fld.type, + std.meta.fields(fld.type).len, + ); + } + } + } else null; + + const header_tmpl = + \\ USAGE: + \\ {s} [OPTIONS] {s} + \\ + \\ OPTIONS: + \\ + ; + + var arena = std.heap.ArenaAllocator.init(self.allocator); + defer arena.deinit(); + const aa = arena.allocator(); + const header = try std.fmt.allocPrint(aa, header_tmpl, .{ + if (sub_cmd_name) |cmd| blk: { + break :blk try std.fmt.allocPrint(aa, "{s} {s}", .{ self.program, cmd }); + } else self.program, + + if (sub_cmds) |cmds| + blk: { + var lst = std.ArrayList([]const u8).init(aa); + try lst.append("[COMMANDS]\n\n COMMANDS:"); + for (cmds) |cmd| { + try lst.append(try std.fmt.allocPrint(aa, " {s:<10} {s}", .{ cmd.name, cmd.message })); + } + break :blk try std.mem.join(aa, "\n", lst.items); + } else if (self.arg_prompt) |p| + blk: { + if (sub_cmd_name == null) { + break :blk try std.fmt.allocPrint(aa, "[--] {s}", .{p}); + } else { + break :blk ""; + } + } else "", + }); + + try writer.writeAll(header); + // TODO: Maybe be too small(or big)? + const msg_offset = 35; + for (fields) |opt_fld| { + var curr_opt = std.ArrayList([]const u8).init(aa); + defer curr_opt.deinit(); + + try curr_opt.append(" "); + if (opt_fld.short_name) |sn| { + try curr_opt.append("-"); + try curr_opt.append(&[_]u8{sn}); + try curr_opt.append(", "); + } else { + try curr_opt.append(" "); + } + try curr_opt.append("--"); + try curr_opt.append(opt_fld.long_name); + try curr_opt.append(opt_fld.opt_type.as_string()); + + var blanks: usize = msg_offset; + for (curr_opt.items) |v| { + blanks -= v.len; + } + while (blanks > 0) { + try curr_opt.append(" "); + blanks -= 1; + } + + if (opt_fld.message) |msg| { + try curr_opt.append(msg); + } + const first_part = try std.mem.join(aa, "", curr_opt.items); + try writer.writeAll(first_part); + + inline for (std.meta.fields(T)) |f| { + if (std.mem.eql(u8, f.name, opt_fld.long_name)) { + const real_type = NonOptionType(f.type); + if (@typeInfo(real_type) == .Enum) { + const enum_opts = try std.mem.join(aa, "|", std.meta.fieldNames(real_type)); + try writer.writeAll(" (valid: "); + try writer.writeAll(enum_opts); + try writer.writeAll(")"); + } + + try MessageHelper.printDefault( + f, + writer, + ); + } + } + + try writer.writeAll("\n"); + } + } +}; + fn StructArguments( comptime T: type, + comptime version: ?[]const u8, comptime arg_prompt: ?[]const u8, ) type { return struct { program: []const u8, // Parsed arguments args: T, - positional_args: std.ArrayList([]const u8), + positional_args: [][:0]u8, + // Unparsed arguments raw_args: [][:0]u8, allocator: std.mem.Allocator, @@ -145,124 +345,18 @@ fn StructArguments( const Self = @This(); pub fn deinit(self: Self) void { - self.positional_args.deinit(); if (!is_test) { std.process.argsFree(self.allocator, self.raw_args); } } - fn print_default(comptime f: std.builtin.Type.StructField, writer: anytype) !void { - if (f.default_value == null) { - if (@typeInfo(f.type) != .Optional) { - try writer.writeAll("(required)"); - } - return; - } - - // Don't print default for false (?)bool - const default = @as(*align(1) const f.type, @ptrCast(f.default_value.?)).*; - switch (@typeInfo(f.type)) { - .Bool => if (!default) return, - .Optional => |opt| if (@typeInfo(opt.child) == .Bool) - if (!(default orelse false)) return, - else => {}, - } - - const format = "(default: " ++ switch (f.type) { - []const u8 => "{s}", - ?[]const u8 => "{?s}", - else => if (@typeInfo(NonOptionType(f.type)) == .Enum) - "{s}" - else - "{any}", - } ++ ")"; - - try std.fmt.format(writer, format, .{switch (@typeInfo(f.type)) { - .Enum => @tagName(default), - .Optional => |opt| if (@typeInfo(opt.child) == .Enum) - @tagName(default.?) - else - default, - else => default, - }}); - } - - pub fn print_help( - self: Self, - writer: anytype, - ) !void { - const fields = comptime parseOptionFields(T); - const header_tmpl = - \\ USAGE: - \\ {s} [OPTIONS] {s} - \\ - \\ OPTIONS: - \\ - ; - const header = try std.fmt.allocPrint(self.allocator, header_tmpl, .{ + pub fn printHelp(self: Self, writer: anytype) !void { + try MessageHelper.init( + self.allocator, self.program, - if (arg_prompt) |p| - "[--] " ++ p - else - "", - }); - defer self.allocator.free(header); - - try writer.writeAll(header); - // TODO: Maybe be too small(or big)? - const msg_offset = 35; - for (fields) |opt_fld| { - var curr_opt = std.ArrayList([]const u8).init(self.allocator); - defer curr_opt.deinit(); - - try curr_opt.append(" "); - if (opt_fld.short_name) |sn| { - try curr_opt.append("-"); - try curr_opt.append(&[_]u8{sn}); - try curr_opt.append(", "); - } else { - try curr_opt.append(" "); - } - try curr_opt.append("--"); - try curr_opt.append(opt_fld.long_name); - try curr_opt.append(opt_fld.opt_type.as_string()); - - var blanks: usize = msg_offset; - for (curr_opt.items) |v| { - blanks -= v.len; - } - while (blanks > 0) { - try curr_opt.append(" "); - blanks -= 1; - } - - if (opt_fld.message) |msg| { - try curr_opt.append(msg); - } - const first_part = try std.mem.join(self.allocator, "", curr_opt.items); - defer self.allocator.free(first_part); - try writer.writeAll(first_part); - - inline for (std.meta.fields(T)) |f| { - if (std.mem.eql(u8, f.name, opt_fld.long_name)) { - const real_type = NonOptionType(f.type); - if (@typeInfo(real_type) == .Enum) { - const enum_opts = try std.mem.join(self.allocator, "|", std.meta.fieldNames(real_type)); - defer self.allocator.free(enum_opts); - try writer.writeAll(" (valid: "); - try writer.writeAll(enum_opts); - try writer.writeAll(")"); - } - - try Self.print_default( - f, - writer, - ); - } - } - - try writer.writeAll("\n"); - } + version, + arg_prompt, + ).printHelp(T, null, writer); } }; } @@ -342,22 +436,100 @@ test "parse OptionType" { } } +const MessageWrapper = struct { + name: []const u8, + message: []const u8, +}; + +fn subCommandsHelpMsg(comptime T: type, comptime len: usize) ?[len]MessageWrapper { + const union_type_info = @typeInfo(T); + if (union_type_info != .Union) { + @compileError("sub commands should be defined using Union(enum), found " ++ @typeName(T)); + } + + if (@hasDecl(T, "__messages__")) { + const messages_type = @TypeOf(T.__messages__); + if (@typeInfo(messages_type) != .Struct) { + @compileError("__messages__ should be defined using struct, found " ++ @typeName(@typeInfo(messages_type))); + } + + var fields: [std.meta.fields(messages_type).len]MessageWrapper = undefined; + inline for (std.meta.fields(messages_type), 0..) |msg_fld, idx| { + inline for (std.meta.fields(T)) |union_fld| { + if (comptime std.mem.eql(u8, msg_fld.name, union_fld.name)) { + fields[idx] = MessageWrapper{ + .name = msg_fld.name, + .message = @field(T.__messages__, msg_fld.name), + }; + break; + } + } else { + @compileError("no such sub_cmd exists, name: " ++ msg_fld.name); + } + } + + return fields; + } + + return null; +} + +fn SubCommandsType(comptime T: type) type { + const union_type_info = @typeInfo(T); + if (union_type_info != .Union) { + @compileError("sub commands should be defined using Union(enum), found " ++ @typeName(T)); + } + + var fields: [std.meta.fields(T).len]std.builtin.Type.StructField = undefined; + inline for (union_type_info.Union.fields, 0..) |fld, idx| { + comptime if (@typeInfo(fld.type) != .Struct) { + @compileError("sub command should be defined using struct, found " ++ @typeName(@typeInfo(fld.type))); + }; + const FieldType = CommandParser(fld.type); + const default_value = FieldType{}; + fields[idx] = .{ + .name = fld.name, + .type = CommandParser(fld.type), + .default_value = @ptrCast(&default_value), + .is_comptime = false, + .alignment = @alignOf(FieldType), + }; + } + return @Type(.{ .Struct = .{ + .layout = .auto, + .fields = &fields, + .decls = &.{}, + .is_tuple = false, + } }); +} + +fn CommandParser(comptime T: type) type { + return struct { + opt_fields: [getOptionLength(T)]OptionField = buildOptionFields(T), + opt_cmds: if (@hasField(T, COMMAND_FIELD_NAME)) blk: { + for (std.meta.fields(T)) |fld| { + if (std.mem.eql(u8, fld.name, COMMAND_FIELD_NAME)) { + break :blk SubCommandsType(fld.type); + } + } else { + unreachable; + } + } else void = if (@hasField(T, COMMAND_FIELD_NAME)) .{} else {}, + }; +} + /// `T` is a struct, which define options fn OptionParser( comptime T: type, ) type { return struct { allocator: std.mem.Allocator, - args: [][:0]u8, - opt_fields: [std.meta.fields(T).len]OptionField, const Self = @This(); - fn init(allocator: std.mem.Allocator, args: [][:0]u8) Self { + fn init(allocator: std.mem.Allocator) Self { return .{ .allocator = allocator, - .args = args, - .opt_fields = comptime parseOptionFields(T), }; } @@ -372,17 +544,20 @@ fn OptionParser( args, }; - fn parse( - self: *Self, - comptime arg_prompt: ?[]const u8, - version: ?[]const u8, - ) OptionError!StructArguments(T, arg_prompt) { - if (self.args.len == 0) { - return error.NoProgram; - } + fn parseCommand( + comptime Args: type, + input_args: [][:0]u8, + arg_idx: *usize, + msg_helper: MessageHelper, + sub_cmd_name: ?[]const u8, + ) !Args { + var args: Args = undefined; + var parser = CommandParser(Args){}; + inline for (std.meta.fields(Args)) |fld| { + if (comptime std.mem.eql(u8, fld.name, COMMAND_FIELD_NAME)) { + continue; + } - var args: T = undefined; - inline for (std.meta.fields(T)) |fld| { if (fld.default_value) |v| { // https://github.com/ziglang/zig/blob/d69e97ae1677ca487833caf6937fa428563ed0ae/lib/std/json.zig#L1590 // why align(1) is used here? @@ -394,22 +569,14 @@ fn OptionParser( } } } - var result = StructArguments(T, arg_prompt){ - .program = self.args[0], - .allocator = self.allocator, - .args = args, - .positional_args = std.ArrayList([]const u8).init(self.allocator), - .raw_args = self.args, - }; - errdefer result.deinit(); var state = ParseState.start; var current_opt: ?*OptionField = null; - - var arg_idx: usize = 1; - while (arg_idx < self.args.len) { - const arg = self.args[arg_idx]; - arg_idx += 1; + var sub_cmd_set = false; + outer: while (arg_idx.* < input_args.len) { + const arg = input_args[arg_idx.*]; + // Point to the next argument + arg_idx.* += 1; switch (state) { .start => { @@ -421,14 +588,15 @@ fn OptionParser( if (!std.mem.startsWith(u8, arg, "-")) { // no option any more, the rest are positional args state = .args; - arg_idx -= 1; + // step back one arg to parse it as positional args + arg_idx.* -= 1; continue; } if (std.mem.startsWith(u8, arg[1..], "-")) { // long option const long_name = arg[2..]; - for (&self.opt_fields) |*opt_fld| { + for (&parser.opt_fields) |*opt_fld| { if (std.mem.eql(u8, opt_fld.long_name, long_name)) { current_opt = opt_fld; break; @@ -443,7 +611,7 @@ fn OptionParser( } return error.NoOption; } - for (&self.opt_fields) |*opt| { + for (&parser.opt_fields) |*opt| { if (opt.short_name) |name| { if (name == short_name[0]) { current_opt = opt; @@ -461,7 +629,7 @@ fn OptionParser( }; if (opt.opt_type == .Bool or opt.opt_type == .RequiredBool) { - opt.is_set = try Self.setOptionValue(&result.args, opt.long_name, "true"); + opt.is_set = try Self.setOptionValue(Args, &args, opt.long_name, "true"); // reset to initial status state = .start; current_opt = null; @@ -470,14 +638,11 @@ fn OptionParser( if (!is_test) { if (std.mem.eql(u8, opt.long_name, "help")) { const stdout = std.io.getStdOut(); - result.print_help(stdout.writer()) catch @panic("OOM"); + msg_helper.printHelp(Args, sub_cmd_name, stdout.writer()) catch @panic("OOM"); std.process.exit(0); } else if (std.mem.eql(u8, opt.long_name, "version")) { - if (version) |v| { - const stdout = std.io.getStdOut(); - stdout.writer().writeAll(v) catch @panic("OOM"); - std.process.exit(0); - } + msg_helper.printVersion() catch @panic("OOM"); + std.process.exit(0); } } } else { @@ -485,11 +650,35 @@ fn OptionParser( } }, .args => { - try result.positional_args.append(arg); + if (@TypeOf(parser.opt_cmds) != void) { + // parse sub command + inline for (std.meta.fields(@TypeOf(parser.opt_cmds))) |fld| { + if (std.mem.eql(u8, fld.name, arg)) { + const CmdType = @TypeOf(@field(args, COMMAND_FIELD_NAME)); + inline for (std.meta.fields(CmdType)) |union_fld| { + if (comptime std.mem.eql(u8, union_fld.name, fld.name)) { + const value = try Self.parseCommand( + union_fld.type, + input_args, + arg_idx, + msg_helper, + fld.name, + ); + @field(args, COMMAND_FIELD_NAME) = @unionInit(CmdType, fld.name, value); + sub_cmd_set = true; + break :outer; + } + } + } + } + } + // From now on, the rest are all positional arguments. + arg_idx.* -= 1; + break :outer; }, .waitValue => { var opt = current_opt.?; - opt.is_set = try Self.setOptionValue(&result.args, opt.long_name, arg); + opt.is_set = try Self.setOptionValue(Args, &args, opt.long_name, arg); // reset to initial status state = .start; current_opt = null; @@ -503,7 +692,10 @@ fn OptionParser( .waitValue => return error.MissingOptionValue, } - inline for (self.opt_fields) |opt| { + if (@TypeOf(parser.opt_cmds) != void and !sub_cmd_set) { + return error.MissingSubCommand; + } + inline for (parser.opt_fields) |opt| { if (opt.opt_type.is_required()) { if (!opt.is_set) { if (!is_test) { @@ -513,6 +705,44 @@ fn OptionParser( } } } + + return args; + } + + fn parse( + self: *Self, + comptime arg_prompt: ?[]const u8, + comptime version: ?[]const u8, + input_args: [][:0]u8, + ) OptionError!StructArguments(T, version, arg_prompt) { + if (input_args.len == 0) { + return error.NoProgram; + } + + const parse_args = input_args[1..]; + var arg_idx: usize = 0; + const msg_helper = MessageHelper.init( + self.allocator, + input_args[0], + version, + arg_prompt, + ); + const parsed = try Self.parseCommand( + T, + parse_args, + &arg_idx, + msg_helper, + null, + ); + var result = StructArguments(T, version, arg_prompt){ + .program = input_args[0], + .allocator = self.allocator, + .args = parsed, + .positional_args = parse_args[arg_idx..], + .raw_args = input_args, + }; + errdefer result.deinit(); + return result; } @@ -525,8 +755,12 @@ fn OptionParser( } // return true when set successfully - fn setOptionValue(opt: *T, long_name: []const u8, raw_value: []const u8) !bool { - inline for (std.meta.fields(T)) |field| { + fn setOptionValue(comptime Args: type, opt: *Args, long_name: []const u8, raw_value: []const u8) !bool { + inline for (std.meta.fields(Args)) |field| { + if (comptime std.mem.eql(u8, field.name, COMMAND_FIELD_NAME)) { + continue; + } + if (std.mem.eql(u8, field.name, long_name)) { @field(opt, field.name) = switch (comptime OptionType.from_zig_type(field.type)) { @@ -591,8 +825,8 @@ test "parse/valid option values" { allocator.free(arg); }; - var parser = OptionParser(TestArguments).init(allocator, &args); - const opt = try parser.parse("...", null); + var parser = OptionParser(TestArguments).init(allocator); + const opt = try parser.parse("...", null, &args); defer opt.deinit(); try std.testing.expectEqualDeep(TestArguments{ @@ -604,14 +838,14 @@ test "parse/valid option values" { var expected = [_][]const u8{ "hello", "world" }; try std.testing.expectEqualDeep( - opt.positional_args.items, + opt.positional_args, &expected, ); var help_msg = std.ArrayList(u8).init(allocator); defer help_msg.deinit(); - try opt.print_help(help_msg.writer()); + try opt.printHelp(help_msg.writer()); try std.testing.expectEqualStrings( \\ USAGE: \\ awesome-cli [OPTIONS] [--] ... @@ -635,12 +869,12 @@ test "parse/bool value" { defer for (args) |arg| { allocator.free(arg); }; - var parser = OptionParser(struct { help: bool }).init(allocator, &args); - const opt = try parser.parse(null, null); + var parser = OptionParser(struct { help: bool }).init(allocator); + const opt = try parser.parse(null, null, &args); defer opt.deinit(); try std.testing.expect(opt.args.help); - try std.testing.expectEqual(opt.positional_args.items, &[_][]const u8{}); + try std.testing.expectEqual(opt.positional_args.len, 0); } { var args = [_][:0]u8{ @@ -651,8 +885,8 @@ test "parse/bool value" { defer for (args) |arg| { allocator.free(arg); }; - var parser = OptionParser(struct { help: bool }).init(allocator, &args); - const opt = try parser.parse(null, null); + var parser = OptionParser(struct { help: bool }).init(allocator); + const opt = try parser.parse(null, null, &args); defer opt.deinit(); try std.testing.expect(opt.args.help); @@ -660,7 +894,7 @@ test "parse/bool value" { "true", }; try std.testing.expectEqualDeep( - opt.positional_args.items, + opt.positional_args, &expected, ); } @@ -675,9 +909,9 @@ test "parse/missing required arguments" { defer for (args) |arg| { allocator.free(arg); }; - var parser = OptionParser(TestArguments).init(allocator, &args); + var parser = OptionParser(TestArguments).init(allocator); - try std.testing.expectError(error.MissingRequiredOption, parser.parse(null, null)); + try std.testing.expectError(error.MissingRequiredOption, parser.parse(null, null, &args)); } test "parse/invalid u16 values" { @@ -691,9 +925,9 @@ test "parse/invalid u16 values" { defer for (args) |arg| { allocator.free(arg); }; - var parser = OptionParser(TestArguments).init(allocator, &args); + var parser = OptionParser(TestArguments).init(allocator); - try std.testing.expectError(error.InvalidCharacter, parser.parse(null, null)); + try std.testing.expectError(error.InvalidCharacter, parser.parse(null, null, &args)); } test "parse/invalid f32 values" { @@ -707,9 +941,9 @@ test "parse/invalid f32 values" { defer for (args) |arg| { allocator.free(arg); }; - var parser = OptionParser(TestArguments).init(allocator, &args); + var parser = OptionParser(TestArguments).init(allocator); - try std.testing.expectError(error.InvalidCharacter, parser.parse(null, null)); + try std.testing.expectError(error.InvalidCharacter, parser.parse(null, null, &args)); } test "parse/unknown option" { @@ -724,9 +958,9 @@ test "parse/unknown option" { defer for (args) |arg| { allocator.free(arg); }; - var parser = OptionParser(TestArguments).init(allocator, &args); + var parser = OptionParser(TestArguments).init(allocator); - try std.testing.expectError(error.NoOption, parser.parse(null, null)); + try std.testing.expectError(error.NoOption, parser.parse(null, null, &args)); } test "parse/missing option value" { @@ -739,9 +973,9 @@ test "parse/missing option value" { defer for (args) |arg| { allocator.free(arg); }; - var parser = OptionParser(TestArguments).init(allocator, &args); + var parser = OptionParser(TestArguments).init(allocator); - try std.testing.expectError(error.MissingOptionValue, parser.parse(null, null)); + try std.testing.expectError(error.MissingOptionValue, parser.parse(null, null, &args)); } test "parse/default value" { @@ -763,13 +997,13 @@ test "parse/default value" { d2: ?bool = false, const __messages__ = .{ .d2 = "padding message" }; - }).init(allocator, &args); - const opt = try parser.parse("...", null); + }).init(allocator); + const opt = try parser.parse("...", null, &args); try std.testing.expectEqualStrings("A1", opt.args.a1); - try std.testing.expectEqual(opt.positional_args.items.len, 0); + try std.testing.expectEqual(opt.positional_args.len, 0); var help_msg = std.ArrayList(u8).init(allocator); defer help_msg.deinit(); - try opt.print_help(help_msg.writer()); + try opt.printHelp(help_msg.writer()); try std.testing.expectEqualStrings( \\ USAGE: \\ awesome-cli [OPTIONS] [--] ... @@ -801,14 +1035,14 @@ test "parse/enum option" { a1: ?enum { A, B } = .A, a2: enum { C, D } = .D, a3: enum { X, Y }, - }).init(allocator, &args); - const opt = try parser.parse("...", null); + }).init(allocator); + const opt = try parser.parse("...", null, &args); defer opt.deinit(); try std.testing.expectEqual(opt.args.a1, .A); var help_msg = std.ArrayList(u8).init(allocator); defer help_msg.deinit(); - try opt.print_help(help_msg.writer()); + try opt.printHelp(help_msg.writer()); try std.testing.expectEqualStrings( \\ USAGE: \\ awesome-cli [OPTIONS] [--] ... @@ -834,17 +1068,17 @@ test "parse/positional arguments" { }; var parser = OptionParser(struct { a: u8 = 1, - }).init(allocator, &args); - const opt = try parser.parse("...", null); + }).init(allocator); + const opt = try parser.parse("...", null, &args); defer opt.deinit(); try std.testing.expectEqualDeep(opt.args.a, 1); var expected = [_][]const u8{ "-a", "2" }; - try std.testing.expectEqualDeep(opt.positional_args.items, &expected); + try std.testing.expectEqualDeep(opt.positional_args, &expected); var help_msg = std.ArrayList(u8).init(allocator); defer help_msg.deinit(); - try opt.print_help(help_msg.writer()); + try opt.printHelp(help_msg.writer()); try std.testing.expectEqualStrings( \\ USAGE: \\ awesome-cli [OPTIONS] [--] ... @@ -854,3 +1088,55 @@ test "parse/positional arguments" { \\ , help_msg.items); } + +test "parse/sub commands" { + const allocator = std.testing.allocator; + var args = [_][:0]u8{ + try allocator.dupeZ(u8, "awesome-cli"), + try allocator.dupeZ(u8, "--a"), + try allocator.dupeZ(u8, "2"), + try allocator.dupeZ(u8, "cmd1"), + try allocator.dupeZ(u8, "--aa"), + try allocator.dupeZ(u8, "22"), + }; + defer for (args) |arg| { + allocator.free(arg); + }; + var parser = OptionParser(struct { + a: u8 = 1, + __commands__: union(enum) { + cmd1: struct { + aa: u8, + }, + cmd2: struct { + bb: u8 = 2, + }, + + pub const __messages__ = .{ + .cmd1 = "This is command 1", + .cmd2 = "This is command 2", + }; + }, + }).init(allocator); + const opt = try parser.parse("...", null, &args); + defer opt.deinit(); + + try std.testing.expectEqualDeep(opt.args.a, 2); + try std.testing.expectEqual(opt.positional_args.len, 0); + + var help_msg = std.ArrayList(u8).init(allocator); + defer help_msg.deinit(); + try opt.printHelp(help_msg.writer()); + try std.testing.expectEqualStrings( + \\ USAGE: + \\ awesome-cli [OPTIONS] [COMMANDS] + \\ + \\ COMMANDS: + \\ cmd1 This is command 1 + \\ cmd2 This is command 2 + \\ + \\ OPTIONS: + \\ --a INTEGER (default: 1) + \\ + , help_msg.items); +}