[FEATURE] develop a mvp
This commit is contained in:
parent
94cb44d399
commit
04d51e79eb
|
|
@ -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.
|
||||
|
|
|
|||
27
build.zig
27
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
}
|
||||
226
cli.zig
226
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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"));
|
||||
}
|
||||
Loading…
Reference in New Issue