diff --git a/README.md b/README.md index 8e51aea..fe487a6 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ # Zig CLI -A command argument parser library for Zig. +A command argument parser library for Zig. Now it's in a heavy development. + +See `demo.zig` for the usage. diff --git a/build.zig b/build.zig index 184ebb1..8e1e579 100644 --- a/build.zig +++ b/build.zig @@ -1,18 +1,39 @@ const std = @import("std"); pub fn build(b: *std.Build) void { - // b.addModule("cli", .{ .source_file = .{ .path = "cli.zig" } }); - const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); + const cli_mod = b.addModule("cli", .{ .root_source_file = .{ .path = "cli.zig" } }); + const demo_exe = b.addExecutable(.{ + .name = "cli-demo", + .root_source_file = .{ .path = "demo.zig" }, + .optimize = optimize, + .target = target, + .link_libc = true, + }); + demo_exe.root_module.addImport("cli", cli_mod); + + // compile + const compile_step = b.step("compile", "Compile demo executable"); + compile_step.dependOn(&demo_exe.step); + + // install + b.installArtifact(demo_exe); + + // run test + const run_demo_exe1 = b.addRunArtifact(demo_exe); + run_demo_exe1.addArgs(&[_][]const u8{ "-e", "4" }); + const run_demo_exe2 = b.addRunArtifact(demo_exe); + run_demo_exe2.addArgs(&[_][]const u8{"subexample"}); const unit_tests = b.addTest(.{ .root_source_file = .{ .path = "cli.zig" }, .target = target, .optimize = optimize, }); - const run_unit_tests = b.addRunArtifact(unit_tests); const test_step = b.step("test", "Run unit tests"); test_step.dependOn(&run_unit_tests.step); + test_step.dependOn(&run_demo_exe1.step); + test_step.dependOn(&run_demo_exe2.step); } diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..41f2968 --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,12 @@ +.{ + .name = "zig-cli", + .version = "0.0.1", + .paths = .{ + "README.md", + "build.zig", + "build.zig.zon", + "util.zig", + "cli.zig", + "demo.zig", + }, +} diff --git a/cli.zig b/cli.zig index 6912efc..6ee7fde 100644 --- a/cli.zig +++ b/cli.zig @@ -1,77 +1,199 @@ const std = @import("std"); -const heap = std.heap; +const io = std.io; const mem = std.mem; const process = std.process; -const testing = std.testing; -const Allocator = std.mem.Allocator; -const Arena = std.heap.ArenaAllocator; +const ArrayList = std.ArrayList; +const StringHashMap = std.StringHashMap; +const util = @import("./util.zig"); -pub fn Option(T: type) Option { - return struct { - const Self = @This(); - const Action = fn (e: T) void; +pub const Option = struct { + short: []const u8, + long: []const u8, + optional: bool, + global: bool, + description: []const u8, + value_string: []const u8 = "", + value_int: i32 = 0, + value_float: f32 = 0.0, + value_bool: bool = false, + value_type: ValueType, - short: []const u8, - long: []const u8, - description: []const u8, - value: T, - action: Action, + const ValueType = enum { + STRING, + BOOL, + INT, + FLOAT, }; -} +}; pub const Command = struct { - name: []const u8, - commands: std.ArrayList(*Command), - options: std.ArrayList(*Option), - allocator: *Allocator, + const Commands = StringHashMap(Command); + const Options = StringHashMap(Option); + const Args = ArrayList([]const u8); + pub const CommandConfig = struct { + name: []const u8, + short_description: []const u8, + long_description: []const u8, + usage: []const u8, + version: []const u8 = "0.0.1", + run: *const fn (cmd: *Command) void = undefined, + }; - pub fn init(name: []const u8, allocator: *Allocator) Command { - const command = Command{ - .name = name, + subcommands: Commands, + options: Options, + arguments: Args, + cmd_args: Args, + allocator: mem.Allocator, + config: CommandConfig, + + pub fn init(allocator: mem.Allocator, args: CommandConfig) Command { + return Command{ + .subcommands = Commands.init(allocator), + .options = Options.init(allocator), + .arguments = Args.init(allocator), + .cmd_args = Args.init(allocator), .allocator = allocator, + .config = args, }; - command.commands = std.ArrayList(*Command).init(command.allocator); - command.options = std.ArrayList(*Option).init(command.allocator); - return command; } pub fn deinit(self: *Command) void { - self.commands.deinit(); + self.subcommands.deinit(); self.options.deinit(); + self.arguments.deinit(); + self.cmd_args.deinit(); } - pub fn addCommand(self: *Command, command: *Command) void { - _ = command; - _ = self; + pub fn addOption(self: *Command, option: Option) !void { + try self.options.put(option.long, option); } - pub fn addOption(self: *Command, option: *Option) void { - _ = option; - _ = self; + pub fn addCommand(self: *Command, command: Command) !void { + try self.subcommands.put(command.config.name, command); } -}; -pub const Parser = struct { - allocator: *Allocator, - command: Command, + pub fn help(self: *Command) !void { + const description = if (self.config.long_description.len > 0) + self.config.long_description + else if (self.config.short_description.len > 0) + self.config.short_description + else + self.config.name; - pub fn parse(self: *Parser, argsIterator: process.ArgIterator) void { - _ = self; - _ = argsIterator.skip(); - for (argsIterator.next()) |arg| { - _ = arg; + var builder = ArrayList(u8).init(self.allocator); + defer builder.deinit(); + try builder.appendSlice(description); + try builder.appendSlice("\n\n"); + try builder.appendSlice("Usage: "); + try builder.appendSlice(self.config.usage); + try builder.appendSlice("\n\n"); + + if (self.subcommands.count() > 0) { + try builder.appendSlice("Subcommands: \n"); + var entry_iterator = self.subcommands.iterator(); + while (entry_iterator.next()) |subcommand| { + try builder.appendSlice(" "); + try builder.appendSlice(subcommand.key_ptr.*); + try builder.appendSlice("\t\t\t"); + try builder.appendSlice(subcommand.value_ptr.*.config.short_description); + try builder.appendSlice("\n"); + } + try builder.appendSlice("\n"); } + + if (self.options.count() > 0) { + try builder.appendSlice("Options: \n"); + var entry_iterator = self.options.iterator(); + while (entry_iterator.next()) |entry| { + const option = entry.value_ptr.*; + try builder.appendSlice(" -"); + try builder.appendSlice(option.short); + try builder.appendSlice(", --"); + try builder.appendSlice(option.long); + try builder.appendSlice("\t\t"); + try builder.appendSlice(option.description); + if (!option.optional) { + try builder.appendSlice(" (required)"); + } + try builder.appendSlice("\n"); + } + } + _ = try io.getStdOut().write(builder.items); + } + + pub fn printVersion(self: *Command) !void { + _ = try io.getStdOut().write(try mem.concat(self.allocator, u8, &[_][]const u8{ self.config.version, "\n" })); + } + + fn parse(self: *Command) !void { + var i: usize = 0; + var current_command = self.*; + var current_option: Option = undefined; + var parsing_option = false; + parse_while_blk: while (i < self.cmd_args.items.len) { + const current_arg = self.cmd_args.items[i]; + if (!parsing_option) { + if (current_command.subcommands.get(current_arg)) |subcommand| { + current_command = subcommand; + i += 1; + continue :parse_while_blk; + } + } + var entry_iterator = current_command.options.iterator(); + while (entry_iterator.next()) |entry| { + const option = entry.value_ptr.*; + if (parsing_option) { + switch (current_option.value_type) { + .STRING => current_option.value_string = current_arg, + .INT => current_option.value_int = util.strToInt(current_arg.ptr), + .FLOAT => current_option.value_float = util.strToFloat(current_arg.ptr), + else => {}, + } + try self.options.put(entry.key_ptr.*, current_option); + parsing_option = false; + i += 1; + continue :parse_while_blk; + } else if (mem.eql(u8, current_arg, try mem.concat(self.allocator, u8, &[_][]const u8{ "-", option.short })) or + mem.eql(u8, current_arg, try mem.concat(self.allocator, u8, &[_][]const u8{ "--", option.long }))) + { + current_option = option; + parsing_option = true; + if (current_option.value_type == .BOOL) { + current_option.value_bool = true; + try self.options.put(entry.key_ptr.*, current_option); + parsing_option = false; + } + i += 1; + continue :parse_while_blk; + } + } + try self.arguments.append(current_arg); + i += 1; + } + current_command.config.run(¤t_command); + } + + pub fn execute(self: *Command) !void { + var arg_iterator = process.args(); + while (arg_iterator.next()) |arg| { + try self.cmd_args.append(arg); + } + try self.parse(); + } + + pub fn getBoolOption(self: *Command, option_name: []const u8) bool { + return if (self.options.get(option_name)) |option| option.value_bool else false; + } + + pub fn getStringOption(self: *Command, option_name: []const u8) []const u8 { + return if (self.options.get(option_name)) |option| option.value_string else ""; + } + + pub fn getIntOption(self: *Command, option_name: []const u8) i32 { + return if (self.options.get(option_name)) |option| option.value_int else 0; + } + + pub fn getFloatOption(self: *Command, option_name: []const u8) f32 { + return if (self.options.get(option_name)) |option| option.value_float else 0.0; } }; - -test { - const argsIterator = process.args(); - const arena = Arena.init(heap.page_allocator); - const allocator = arena.child_allocator; - defer arena.deinit(); - - const command = Command{ .name = "test" }; - - const parser = Parser{ .allocator = allocator, .command = command }; - parser.parse(argsIterator); -} diff --git a/demo.zig b/demo.zig new file mode 100644 index 0000000..139b171 --- /dev/null +++ b/demo.zig @@ -0,0 +1,46 @@ +const std = @import("std"); +const heap = std.heap; +const cli = @import("cli"); +const Command = cli.Command; +const Option = cli.Option; + +fn rootRun(cmd: *Command) void { + const e = cmd.getIntOption("example"); + std.debug.print("{d}\n", .{e}); +} + +fn subcommandRun(cmd: *Command) void { + std.debug.print("{s}\n", .{cmd.config.name}); +} + +pub fn main() !void { + var gpa = heap.GeneralPurposeAllocator(.{}){}; + const allocator = gpa.allocator(); + const config = Command.CommandConfig{ + .short_description = "Example", + .long_description = "Example command", + .usage = "example", + .name = "example", + .run = rootRun, + }; + var root_command = Command.init(allocator, config); + try root_command.addCommand(Command.init(allocator, .{ + .short_description = "SubExample", + .long_description = "Subcommand example", + .usage = "subexample", + .name = "subexample", + .run = subcommandRun, + })); + try root_command.addOption(Option{ + .short = "e", + .long = "example", + .description = "example option", + .optional = false, + .global = false, + .value_type = .INT, + .value_int = 3, + }); + try root_command.execute(); + try root_command.help(); + try root_command.printVersion(); +} diff --git a/util.zig b/util.zig new file mode 100644 index 0000000..cec9d3a --- /dev/null +++ b/util.zig @@ -0,0 +1,18 @@ +const std = @import("std"); +const testing = std.testing; +const stdlib = @cImport({ + @cInclude("stdlib.h"); +}); + +pub fn strToFloat(str: [*c]const u8) f32 { + return @floatCast(stdlib.atof(str)); +} + +pub fn strToInt(str: [*c]const u8) i32 { + return @intCast(stdlib.atoi(str)); +} + +test "string cast to number" { + try testing.expect(3 == strToInt("3")); + try testing.expect(3.2 == strToFloat("3.2")); +}