Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for arbitrary types #21

Merged
merged 11 commits into from
Oct 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 12 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ A simple package for building command line apps in Zig.
Inspired by [urfave/cli](https://github.com/urfave/cli) Go package.

## Features
* command line arguments are parsed into zig values
* long and short options: `--option1`, `-o`
* optional `=` sign: `--address=127.0.0.1` equals `--address 127.0.0.1`
* concatenated short options: `-a -b -c` equals `-abc`
Expand All @@ -24,19 +25,25 @@ 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 = cli.OptionValue{ .string = null },
.value_ref = cli.mkRef(&config.host),
};
var port = cli.Option{
.long_name = "port",
.help = "port to bind to",
.value = cli.OptionValue{ .int = null },
.required = true,
.value_ref = cli.mkRef(&config.port),
};
var app = &cli.App{
.name = "awesome-app",
.options = &.{&host, &port},
.name = "short",
.options = &.{ &host, &port },
.action = run_server,
};

Expand All @@ -45,9 +52,7 @@ pub fn main() !void {
}

fn run_server(_: []const []const u8) !void {
var h = host.value.string.?;
var p = port.value.int.?;
std.log.debug("server is listening on {s}:{any}", .{ h, p });
std.log.debug("server is listening on {s}:{}", .{ config.host, config.port });
}
```

Expand Down
12 changes: 7 additions & 5 deletions example/short.zig
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,20 @@ 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 = cli.OptionValue{ .string = "localhost" },
.value_ref = cli.mkRef(&config.host),
};
var port = cli.Option{
.long_name = "port",
.help = "port to bind to",
.required = true,
.value = cli.OptionValue{ .int = null },
.value_ref = cli.mkRef(&config.port),
};
var app = &cli.App{
.name = "short",
Expand All @@ -26,7 +30,5 @@ pub fn main() !void {
}

fn run_server(_: []const []const u8) !void {
var h = host.value.string.?;
var p = port.value.int.?;
std.log.debug("server is listening on {s}:{}", .{ h, p });
std.log.debug("server is listening on {s}:{}", .{ config.host, config.port });
}
26 changes: 14 additions & 12 deletions example/simple.zig
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,38 @@ const cli = @import("zig-cli");
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();

var config = struct {
ip: []const u8 = undefined,
int: i32 = undefined,
bool: bool = false,
float: f64 = 0.34,
}{};

var ip_option = cli.Option{
.long_name = "ip",
.help = "this is the IP address",
.short_alias = 'i',
.value = cli.OptionValue{ .string = null },
.value_ref = cli.mkRef(&config.ip),
.required = true,
.value_name = "IP",
};
var int_option = cli.Option{
.long_name = "int",
.help = "this is an int",
.value = cli.OptionValue{ .int = null },
.value_ref = cli.mkRef(&config.int),
};
var bool_option = cli.Option{
.long_name = "bool",
.short_alias = 'b',
.help = "this is a bool",
.value = cli.OptionValue{ .bool = false },
.value_ref = cli.mkRef(&config.bool),
};
var float_option = cli.Option{
.long_name = "float",
.help = "this is a float",
.value = cli.OptionValue{ .float = 0.34 },
.value_ref = cli.mkRef(&config.float),
};

var name_option = cli.Option{
.long_name = "long_name",
.help = "long_name help",
.value = cli.OptionValue{ .string = null },
};
var app = &cli.App{
.name = "simple",
.description = "This a simple CLI app\nEnjoy!",
Expand All @@ -42,7 +44,7 @@ var app = &cli.App{
.subcommands = &.{&cli.Command{
.name = "sub1",
.help = "another awesome command",
.description =
.description =
\\this is my awesome multiline description.
\\This is already line 2.
\\And this is line 3.
Expand All @@ -68,6 +70,6 @@ pub fn main() anyerror!void {
}

fn run_sub2(args: []const []const u8) anyerror!void {
var ip = ip_option.value.string.?;
std.log.debug("running sub2: ip={s}, bool={any}, float={any} arg_count={any}", .{ ip, bool_option.value.bool, float_option.value.float, args.len });
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 });
}
12 changes: 3 additions & 9 deletions src/command.zig
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
const std = @import("std");
const vref = @import("./value_ref.zig");
pub const ValueRef = vref.ValueRef;

pub const App = struct {
name: []const u8,
Expand Down Expand Up @@ -39,19 +41,11 @@ pub const Command = struct {

pub const Action = *const fn (args: []const []const u8) anyerror!void;

pub const OptionValue = union(enum) {
bool: bool,
string: ?[]const u8,
int: ?i64,
float: ?f64,
string_list: ?[]const []const u8,
};

pub const Option = struct {
long_name: []const u8,
short_alias: ?u8 = null,
help: []const u8,
required: bool = false,
value: OptionValue,
value_ref: ValueRef,
value_name: []const u8 = "VALUE",
};
2 changes: 1 addition & 1 deletion src/help.zig
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ const HelpPrinter = struct {
self.printer.format("--{s}", .{option.long_name});
self.printer.printColor(color_clear);
var width = option.long_name.len;
if (option.value != .bool) {
if (!option.value_ref.value_data.is_bool) {
self.printer.printColor(self.help_config.color_option);
self.printer.format(" <{s}>", .{option.value_name});
self.printer.printColor(color_clear);
Expand Down
1 change: 1 addition & 0 deletions src/main.zig
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub usingnamespace @import("./command.zig");
const parser = @import("./parser.zig");

pub const mkRef = @import("./value_ref.zig").mkRef;
pub const run = parser.run;
106 changes: 32 additions & 74 deletions src/parser.zig
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ 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;

pub const ParseResult = struct {
action: command.Action,
Expand All @@ -22,16 +23,15 @@ pub fn run(app: *const command.App, alloc: Allocator) anyerror!void {
return result.action(result.args);
}

var help_option_set: bool = false;

var help_option = command.Option{
.long_name = "help",
.help = "Show this help output.",
.short_alias = 'h',
.value = command.OptionValue{ .bool = false },
.value_ref = mkRef(&help_option_set),
};

const ValueList = std.ArrayList([]const u8);
const ValueListMap = std.AutoHashMap(*command.Option, ValueList);

pub fn Parser(comptime Iterator: type) type {
return struct {
const Self = @This();
Expand All @@ -41,7 +41,6 @@ pub fn Parser(comptime Iterator: type) type {
app: *const command.App,
command_path: std.ArrayList(*const command.Command),
captured_arguments: std.ArrayList([]const u8),
value_lists: ?ValueListMap,

pub fn init(app: *const command.App, it: Iterator, alloc: Allocator) !Self {
return Self{
Expand All @@ -50,7 +49,6 @@ pub fn Parser(comptime Iterator: type) type {
.app = app,
.command_path = try std.ArrayList(*const command.Command).initCapacity(alloc, 16),
.captured_arguments = try std.ArrayList([]const u8).initCapacity(alloc, 16),
.value_lists = null,
};
}

Expand Down Expand Up @@ -95,13 +93,12 @@ pub fn Parser(comptime Iterator: type) type {
self.ensure_all_required_set(self.current_command());
var args = try self.captured_arguments.toOwnedSlice();

if (self.value_lists) |vl| {
var it = vl.iterator();
while (it.next()) |entry| {
var option: *command.Option = entry.key_ptr.*;
option.value.string_list = try entry.value_ptr.toOwnedSlice();
for (self.command_path.items) |cmd| {
if (cmd.options) |options| {
for (options) |opt| {
try opt.value_ref.finalize(self.alloc);
}
}
self.value_lists.?.deinit();
}

if (self.current_command().action) |action| {
Expand Down Expand Up @@ -136,12 +133,12 @@ pub fn Parser(comptime Iterator: type) type {
return self.arg_iterator.next();
}

fn process_option(self: *Self, option: *const argp.OptionInterpretation) !void {
var opt: *command.Option = switch (option.option_type) {
.long => self.find_option_by_name(self.current_command(), option.name),
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),
.short => a: {
self.set_boolean_options(self.current_command(), option.name[0 .. option.name.len - 1]);
break :a self.find_option_by_alias(self.current_command(), option.name[option.name.len - 1]);
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]);
},
};

Expand All @@ -150,49 +147,18 @@ pub fn Parser(comptime Iterator: type) type {
std.os.exit(0);
}

switch (opt.value) {
.bool => opt.value = command.OptionValue{ .bool = true },
else => {
const arg = option.value orelse self.next_arg() orelse {
self.fail("missing argument for {s}", .{opt.long_name});
unreachable;
};
try self.parse_and_set_option_value(arg, opt);
},
}
}

fn parse_and_set_option_value(self: *Self, text: []const u8, option: *command.Option) !void {
switch (option.value) {
.bool => unreachable,
.string => option.value = command.OptionValue{ .string = text },
.int => {
if (std.fmt.parseInt(i64, text, 10)) |iv| {
option.value = command.OptionValue{ .int = iv };
} else |_| {
self.fail("option({s}): cannot parse int value", .{option.long_name});
unreachable;
}
},
.float => {
if (std.fmt.parseFloat(f64, text)) |fv| {
option.value = command.OptionValue{ .float = fv };
} else |_| {
self.fail("option({s}): cannot parse float value", .{option.long_name});
unreachable;
}
},
.string_list => {
if (self.value_lists == null) {
self.value_lists = ValueListMap.init(self.alloc);
}

var res = try self.value_lists.?.getOrPut(option);
if (!res.found_existing) {
res.value_ptr.* = try ValueList.initCapacity(self.alloc, 16);
}
try res.value_ptr.append(text);
},
if (opt.value_ref.value_data.is_bool) {
try opt.value_ref.put("true", self.alloc);
// TODO: bool argument can be explicitly passed as a value
} else {
const arg = option_interpretation.value orelse self.next_arg() orelse {
self.fail("missing argument for {s}", .{opt.long_name});
unreachable;
};
opt.value_ref.put(arg, self.alloc) catch |err| {
self.fail("option({s}): cannot parse {s} value: {s}", .{ opt.long_name, opt.value_ref.value_data.type_name, @errorName(err) });
unreachable;
};
}
}

Expand Down Expand Up @@ -250,11 +216,12 @@ pub fn Parser(comptime Iterator: type) type {
}
}

fn set_boolean_options(self: *const Self, cmd: *const command.Command, options: []const u8) void {
/// 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| {
var opt = self.find_option_by_alias(cmd, alias);
if (opt.value == command.OptionValue.bool) {
opt.value.bool = true;
if (opt.value_ref.value_data.is_bool) {
opt.value_ref.put("true", self.alloc) catch unreachable;
} else {
self.fail("'-{c}' is not a boolean option", .{alias});
}
Expand All @@ -264,17 +231,8 @@ 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) {
var not_set = switch (option.value) {
.bool => false,
.string => |x| x == null,
.int => |x| x == null,
.float => |x| x == null,
.string_list => |x| x == null,
};
if (not_set) {
self.fail("missing required option '{s}'", .{option.long_name});
}
if (option.required and option.value_ref.element_count == 0) {
self.fail("missing required option '{s}'", .{option.long_name});
}
}
}
Expand Down
Loading
Loading