Skip to content

Commit

Permalink
Merge pull request #40 from tcoratger/improvements
Browse files Browse the repository at this point in the history
add various idiomatic and documentation improvements
  • Loading branch information
sam701 authored Jan 12, 2024
2 parents fef13e1 + 7b2ff15 commit 50201f0
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 72 deletions.
13 changes: 12 additions & 1 deletion examples/short.zig
Original file line number Diff line number Diff line change
@@ -1,25 +1,33 @@
const std = @import("std");
const cli = @import("zig-cli");

// Create a GeneralPurposeAllocator for heap allocations.
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
// Obtain the allocator from the GeneralPurposeAllocator.
const allocator = gpa.allocator();

// Define a configuration structure with default values.
var config = struct {
host: []const u8 = "localhost",
port: u16 = undefined,
}{};

// Define an Option for the "host" command-line argument.
var host = cli.Option{
.long_name = "host",
.help = "host to listen on",
.value_ref = cli.mkRef(&config.host),
};

// Define an Option for the "port" command-line argument.
var port = cli.Option{
.long_name = "port",
.help = "port to bind to",
.required = true,
.value_ref = cli.mkRef(&config.port),
};

// Create an App with a command named "short" that takes host and port options.
var app = &cli.App{
.command = cli.Command{
.name = "short",
Expand All @@ -30,10 +38,13 @@ var app = &cli.App{
},
};

// Main function where the CLI is run with the provided app and allocator.
pub fn main() !void {
return cli.run(app, allocator);
}

// Action function to execute when the "short" command is invoked.
fn run_server() !void {
std.log.debug("server is listening on {s}:{}", .{ config.host, config.port });
// Log a debug message indicating the server is listening on the specified host and port.
std.log.debug("server is listening on {s}:{d}", .{ config.host, config.port });
}
32 changes: 18 additions & 14 deletions src/arg.zig
Original file line number Diff line number Diff line change
Expand Up @@ -18,34 +18,38 @@ pub const ArgumentInterpretation = union(enum) {
};

pub fn interpret(arg: []const u8) !ArgumentInterpretation {
if (arg.len == 0) return ArgumentInterpretation{ .other = arg };
if (arg.len == 0) return .{ .other = arg };

if (arg[0] == '-') {
if (arg.len == 1) return ArgumentInterpretation{ .other = arg };
if (arg.len == 1) return .{ .other = arg };

var name = arg[1..];
var option_type = OptionType.short;
var option_type: OptionType = .short;
if (arg[1] == '-') {
if (arg.len == 2) return ArgumentInterpretation.double_dash;
if (arg.len == 2) return .double_dash;
name = arg[2..];
option_type = .long;
}

if (std.mem.indexOfScalar(u8, name, '=')) |ix| {
if (name.len < ix + 2) return error.MissingOptionArgument;
return ArgumentInterpretation{ .option = OptionInterpretation{
.option_type = option_type,
.name = name[0..ix],
.value = name[ix + 1 ..],
} };
return .{
.option = .{
.option_type = option_type,
.name = name[0..ix],
.value = name[ix + 1 ..],
},
};
} else {
return ArgumentInterpretation{ .option = OptionInterpretation{
.option_type = option_type,
.name = name,
} };
return .{
.option = .{
.option_type = option_type,
.name = name,
},
};
}
} else {
return ArgumentInterpretation{ .other = arg };
return .{ .other = arg };
}
}

Expand Down
48 changes: 43 additions & 5 deletions src/command.zig
Original file line number Diff line number Diff line change
@@ -1,75 +1,113 @@
const std = @import("std");
const ValueRef = @import("./value_ref.zig").ValueRef;

/// Main structure for the application.
pub const App = struct {
/// Main command configuration.
command: Command,
/// Optional version information.
version: ?[]const u8 = null,
/// Optional author information.
author: ?[]const u8 = null,

/// If set all options can be set by providing an environment variable.
/// For example an option with a long name `hello_world` can be set by setting `<prefix in upper case>_HELLO_WORLD` environment variable.
/// If set, all options can be set by providing an environment variable.
/// For example, an option with a long name `hello_world` can be set by setting `<prefix in upper case>_HELLO_WORLD` environment variable.
option_envvar_prefix: ?[]const u8 = null,

/// Help display configuration.
help_config: HelpConfig = HelpConfig{},
};

/// Enumeration for color usage in help display.
pub const ColorUsage = enum {
always,
never,
auto,
};

/// Configuration for help display.
pub const HelpConfig = struct {
/// Color usage setting.
color_usage: ColorUsage = .auto,
/// Color for the application name in help.
color_app_name: []const u8 = "33;1",
/// Color for section headers in help.
color_section: []const u8 = "33;1",
/// Color for option names in help.
color_option: []const u8 = "32",
/// Color for error messages in help.
color_error: []const u8 = "31;1",
};

/// Structure representing a command.
pub const Command = struct {
/// Name of the command.
name: []const u8,
/// Description of the command.
description: ?Description = null,
/// List of options for the command.
options: ?[]const *Option = null,
/// Target of the command (subcommands or action).
target: CommandTarget,
};

/// Structure representing a description.
pub const Description = struct {
/// One-line description.
one_line: []const u8,
/// Detailed description (optional).
detailed: ?[]const u8 = null,
};

/// Union for different command targets.
pub const CommandTarget = union(enum) {
/// Subcommands of the command.
subcommands: []const *const Command,
/// Action to execute for the command.
action: CommandAction,
};

/// Structure representing a command action.
pub const CommandAction = struct {
/// Positional arguments for the action.
positional_args: ?PositionalArgs = null,
/// Function to execute for the action.
exec: ExecFn,
};

/// Function pointer type for command execution.
pub const ExecFn = *const fn () anyerror!void;

/// Structure representing an option.
pub const Option = struct {
/// Long name of the option.
long_name: []const u8,
/// Short alias for the option.
short_alias: ?u8 = null,
/// Help description for the option.
help: []const u8,
/// Whether the option is required or not.
required: bool = false,
/// Reference to the value of the option.
value_ref: ValueRef,
/// Name of the value for the option.
value_name: []const u8 = "VALUE",
/// Environment variable name for the option.
envvar: ?[]const u8 = null,
};

/// Structure representing positional arguments for an action.
pub const PositionalArgs = struct {
/// List of positional arguments.
args: []const *PositionalArg,

/// If not set, all positional arguments are considered as required.
first_optional_arg: ?*const PositionalArg = null,
};

/// Structure representing a positional argument.
pub const PositionalArg = struct {
/// Name of the positional argument.
name: []const u8,
/// Help description for the positional argument.
help: []const u8,
/// Reference to the value of the positional argument.
value_ref: ValueRef,
};
49 changes: 28 additions & 21 deletions src/parser.zig
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ pub fn Parser(comptime Iterator: type) type {
next_arg: ?[]const u8 = null,

pub fn init(app: *const command.App, it: Iterator, alloc: Allocator) !Self {
return Self{
return .{
.alloc = alloc,
.arg_iterator = it,
.app = app,
Expand Down Expand Up @@ -143,7 +143,7 @@ pub fn Parser(comptime Iterator: type) type {
self.fail("positional argument ({s}): cannot parse '{s}' as {s}: {s}", .{ posArg.name, arg, posArgRef.value_data.type_name, @errorName(err) });
unreachable;
};
if (posArgRef.value_type == vref.ValueType.single) {
if (posArgRef.value_type == .single) {
self.position_argument_ix += 1;
}
}
Expand Down Expand Up @@ -171,17 +171,16 @@ pub fn Parser(comptime Iterator: type) type {
defer self.alloc.free(envvar_name);
@memcpy(envvar_name[0..prefix.len], prefix);
for (envvar_name[prefix.len..], opt.long_name) |*dest, name_char| {
if (name_char == '-') {
dest.* = '_';
} else {
dest.* = std.ascii.toUpper(name_char);
}
dest.* = if (name_char == '-') '_' else std.ascii.toUpper(name_char);
}

if (std.process.getEnvVarOwned(self.alloc, envvar_name)) |value| {
defer self.alloc.free(value);
opt.value_ref.put(value, self.alloc) catch |err| {
self.fail("envvar({s}): cannot parse {s} value '{s}': {s}", .{ envvar_name, opt.value_ref.value_data.type_name, value, @errorName(err) });
self.fail(
"envvar({s}): cannot parse {s} value '{s}': {s}",
.{ envvar_name, opt.value_ref.value_data.type_name, value, @errorName(err) },
);
unreachable;
};
} else |err| {
Expand All @@ -196,12 +195,9 @@ pub fn Parser(comptime Iterator: type) type {
var args_only = false;
try switch (int.*) {
.option => |opt| self.process_option(&opt),
.double_dash => {
args_only = true;
},
.double_dash => args_only = true,
.other => |some_name| {
const cmd = self.current_command();
switch (cmd.target) {
switch (self.current_command().target) {
.subcommands => |cmds| {
for (cmds) |sc| {
if (std.mem.eql(u8, sc.name, some_name)) {
Expand All @@ -211,9 +207,7 @@ pub fn Parser(comptime Iterator: type) type {
}
self.fail("no such subcommand '{s}'", .{some_name});
},
.action => {
try self.handlePositionalArgument(some_name);
},
.action => try self.handlePositionalArgument(some_name),
}
},
};
Expand All @@ -237,8 +231,15 @@ pub fn Parser(comptime Iterator: type) type {
var opt: *command.Option = switch (option_interpretation.option_type) {
.long => self.find_option_by_name(option_interpretation.name),
.short => a: {
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]);
const active_command = self.current_command();
self.set_concatenated_boolean_options(
active_command,
option_interpretation.name[0 .. option_interpretation.name.len - 1],
);
break :a self.find_option_by_alias(
active_command,
option_interpretation.name[option_interpretation.name.len - 1],
);
},
};

Expand All @@ -257,8 +258,7 @@ pub fn Parser(comptime Iterator: type) type {
return;
}

const following_arg = self.nextArg();
if (following_arg) |arg| {
if (self.nextArg()) |arg| {
if (arg.len > 0 and arg[0] != '-') {
var lw = try self.alloc.alloc(u8, arg.len);
defer self.alloc.free(lw);
Expand All @@ -278,7 +278,14 @@ pub fn Parser(comptime Iterator: type) type {
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) });
self.fail(
"option({s}): cannot parse {s} value: {s}",
.{
opt.long_name,
opt.value_ref.value_data.type_name,
@errorName(err),
},
);
unreachable;
};
}
Expand Down
42 changes: 19 additions & 23 deletions src/tests.zig
Original file line number Diff line number Diff line change
Expand Up @@ -16,43 +16,39 @@ const StringSliceIterator = struct {

pub fn next(self: *StringSliceIterator) ?[]const u8 {
defer self.index += 1;

if (self.index < self.items.len) {
return self.items[self.index];
} else {
return null;
}
return if (self.index < self.items.len) self.items[self.index] else null;
}
};

fn run(app: *const command.App, items: []const []const u8) !void {
const it = StringSliceIterator{
.items = items,
};

var parser = try Parser(StringSliceIterator).init(app, it, alloc);
var parser = try Parser(StringSliceIterator).init(
app,
.{ .items = items },
alloc,
);
_ = try parser.parse();
parser.deinit();
}

fn dummy_action() !void {}

fn runOptionsPArgs(input: []const []const u8, options: []const *command.Option, pargs: ?[]const *command.PositionalArg) !void {
const pa = if (pargs) |p| command.PositionalArgs{ .args = p } else null;
const app = command.App{
.command = command.Command{
.name = "cmd",
.description = command.Description{ .one_line = "short help" },
.options = options,
.target = command.CommandTarget{
.action = command.CommandAction{
.positional_args = pa,
.exec = dummy_action,
try run(
&.{
.command = .{
.name = "cmd",
.description = .{ .one_line = "short help" },
.options = options,
.target = .{
.action = .{
.positional_args = if (pargs) |p| .{ .args = p } else null,
.exec = dummy_action,
},
},
},
},
};
try run(&app, input);
input,
);
}

fn runOptions(input: []const []const u8, options: []const *command.Option) !void {
Expand Down
Loading

0 comments on commit 50201f0

Please sign in to comment.