zig-cli/cli.zig

234 lines
8.1 KiB
Zig
Raw Permalink Normal View History

2023-09-20 10:49:38 +00:00
const std = @import("std");
2024-01-14 10:07:26 +00:00
const io = std.io;
2023-09-20 10:49:38 +00:00
const mem = std.mem;
const process = std.process;
2024-01-14 10:07:26 +00:00
const ArrayList = std.ArrayList;
const StringHashMap = std.StringHashMap;
2024-01-16 05:57:39 +00:00
const out = io.getStdOut().writer();
2024-01-14 10:07:26 +00:00
const util = @import("./util.zig");
pub const Option = struct {
short: []const u8,
long: []const u8,
2024-01-16 05:57:39 +00:00
optional: bool = true,
global: bool = false,
2024-01-14 10:07:26 +00:00
description: []const u8,
value_string: []const u8 = "",
value_int: i32 = 0,
value_float: f32 = 0.0,
value_bool: bool = false,
2024-01-16 05:57:39 +00:00
value_type: ValueType = .BOOL,
2024-01-14 10:07:26 +00:00
const ValueType = enum {
STRING,
BOOL,
INT,
FLOAT,
2023-09-20 10:49:38 +00:00
};
2024-01-14 10:07:26 +00:00
};
2023-09-20 10:49:38 +00:00
2024-01-16 05:57:39 +00:00
const help_option = Option{
.short = "h",
.long = "help",
.global = true,
.description = "Print help",
};
const version_option = Option{
.short = "v",
.long = "version",
.global = true,
.description = "Print version",
};
2023-09-20 10:49:38 +00:00
pub const Command = struct {
2024-01-14 10:07:26 +00:00
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",
2024-01-16 05:57:39 +00:00
run: ?*const fn (cmd: *Command) anyerror!void,
2024-01-14 10:07:26 +00:00
};
subcommands: Commands,
options: Options,
arguments: Args,
cmd_args: Args,
allocator: mem.Allocator,
config: CommandConfig,
2024-01-16 05:57:39 +00:00
pub fn init(allocator: mem.Allocator, args: CommandConfig) !Command {
var options = Options.init(allocator);
try options.put(help_option.long, help_option);
try options.put(version_option.long, version_option);
2024-01-14 10:07:26 +00:00
return Command{
.subcommands = Commands.init(allocator),
2024-01-16 05:57:39 +00:00
.options = options,
2024-01-14 10:07:26 +00:00
.arguments = Args.init(allocator),
.cmd_args = Args.init(allocator),
2023-09-20 10:49:38 +00:00
.allocator = allocator,
2024-01-14 10:07:26 +00:00
.config = args,
2023-09-20 10:49:38 +00:00
};
}
pub fn deinit(self: *Command) void {
2024-01-14 10:07:26 +00:00
self.subcommands.deinit();
2023-09-20 10:49:38 +00:00
self.options.deinit();
2024-01-14 10:07:26 +00:00
self.arguments.deinit();
self.cmd_args.deinit();
2023-09-20 10:49:38 +00:00
}
2024-01-14 10:07:26 +00:00
pub fn addOption(self: *Command, option: Option) !void {
try self.options.put(option.long, option);
2023-09-20 10:49:38 +00:00
}
2024-01-14 10:07:26 +00:00
pub fn addCommand(self: *Command, command: Command) !void {
2024-01-16 05:57:39 +00:00
var option_iterator = self.options.iterator();
while (option_iterator.next()) |entry| {
const option = entry.value_ptr.*;
if (option.global) {
try @constCast(&command).addOption(option);
}
}
2024-01-14 10:07:26 +00:00
try self.subcommands.put(command.config.name, command);
2023-09-20 10:49:38 +00:00
}
2024-01-14 10:07:26 +00:00
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;
2023-09-20 10:49:38 +00:00
2024-01-16 05:57:39 +00:00
try out.print("{s}\n\nUsage: {s}", .{ description, self.config.usage });
if (self.subcommands.count() > 0) {
try out.print(" [COMMAND]", .{});
}
try out.print(" [OPTIONS] [ARGUMENTS]\n\n", .{});
2024-01-14 10:07:26 +00:00
if (self.subcommands.count() > 0) {
2024-01-16 05:57:39 +00:00
try out.print("Commands: \n\n", .{});
2024-01-14 10:07:26 +00:00
var entry_iterator = self.subcommands.iterator();
while (entry_iterator.next()) |subcommand| {
2024-01-16 05:57:39 +00:00
try out.print(" {s: <20}{s}\n", .{ subcommand.key_ptr.*, subcommand.value_ptr.*.config.short_description });
2024-01-14 10:07:26 +00:00
}
2024-01-16 05:57:39 +00:00
try out.print("\n", .{});
2024-01-14 10:07:26 +00:00
}
2024-01-16 05:57:39 +00:00
try out.print("Options: \n\n", .{});
2024-01-14 10:07:26 +00:00
if (self.options.count() > 0) {
var entry_iterator = self.options.iterator();
while (entry_iterator.next()) |entry| {
const option = entry.value_ptr.*;
2024-01-16 05:57:39 +00:00
const flags = try mem.concat(self.allocator, u8, &[_][]const u8{ "-", option.short, ", --", option.long });
try out.print(" {s: <20}{s}", .{ flags, option.description });
2024-01-14 10:07:26 +00:00
if (!option.optional) {
2024-01-16 05:57:39 +00:00
try out.print(" (required)\n", .{});
} else {
try out.print("\n", .{});
2024-01-14 10:07:26 +00:00
}
}
2023-09-20 10:49:38 +00:00
}
}
2024-01-14 10:07:26 +00:00
pub fn printVersion(self: *Command) !void {
_ = try io.getStdOut().write(try mem.concat(self.allocator, u8, &[_][]const u8{ self.config.version, "\n" }));
}
2023-09-20 10:49:38 +00:00
2024-01-16 05:57:39 +00:00
fn defaultRun(self: *Command) !void {
if (self.options.get("help").?.value_bool) {
try self.help();
} else if (self.options.get("version").?.value_bool) {
try self.printVersion();
} else if (self.config.run) |run| {
try run(self);
}
}
2024-01-14 10:07:26 +00:00
fn parse(self: *Command) !void {
2024-01-16 05:57:39 +00:00
var i: usize = 1;
2024-01-14 10:07:26 +00:00
var current_command = self.*;
var current_option: Option = undefined;
var parsing_option = false;
2024-01-16 05:57:39 +00:00
var subcommand_parse_over = false;
var option_parse_over = false;
2024-01-14 10:07:26 +00:00
parse_while_blk: while (i < self.cmd_args.items.len) {
const current_arg = self.cmd_args.items[i];
2024-01-16 05:57:39 +00:00
if (!subcommand_parse_over) {
2024-01-14 10:07:26 +00:00
if (current_command.subcommands.get(current_arg)) |subcommand| {
current_command = subcommand;
i += 1;
continue :parse_while_blk;
}
2024-01-16 05:57:39 +00:00
subcommand_parse_over = true;
2024-01-14 10:07:26 +00:00
}
2024-01-16 05:57:39 +00:00
if (!option_parse_over) {
2024-01-14 10:07:26 +00:00
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 => {},
}
2024-01-16 05:57:39 +00:00
try current_command.options.put(current_option.long, current_option);
2024-01-14 10:07:26 +00:00
parsing_option = false;
i += 1;
continue :parse_while_blk;
2024-01-16 05:57:39 +00:00
}
var entry_iterator = current_command.options.iterator();
while (entry_iterator.next()) |entry| {
const option = entry.value_ptr.*;
if (mem.eql(u8, current_arg, try mem.concat(current_command.allocator, u8, &[_][]const u8{ "-", option.short })) or
mem.eql(u8, current_arg, try mem.concat(current_command.allocator, u8, &[_][]const u8{ "--", option.long })))
{
current_option = option;
parsing_option = true;
if (current_option.value_type == .BOOL) {
current_option.value_bool = true;
try current_command.options.put(entry.key_ptr.*, current_option);
parsing_option = false;
}
i += 1;
continue :parse_while_blk;
2024-01-14 10:07:26 +00:00
}
}
2024-01-16 05:57:39 +00:00
option_parse_over = true;
2024-01-14 10:07:26 +00:00
}
2024-01-16 05:57:39 +00:00
try current_command.arguments.append(current_arg);
2024-01-14 10:07:26 +00:00
i += 1;
}
2024-01-16 05:57:39 +00:00
try current_command.defaultRun();
2024-01-14 10:07:26 +00:00
}
pub fn execute(self: *Command) !void {
var arg_iterator = process.args();
while (arg_iterator.next()) |arg| {
try self.cmd_args.append(arg);
}
2024-01-16 05:57:39 +00:00
self.cmd_args.items[0] = self.config.name;
2024-01-14 10:07:26 +00:00
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;
}
2023-09-20 10:49:38 +00:00
2024-01-14 10:07:26 +00:00
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;
}
};