Skip to content
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@

zig-cache/
.zig-cache/
zig-out/
/release/
/debug/
Expand Down
33 changes: 11 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,38 +58,27 @@ fantastic projects from the Zig community:

## Installing

Requires [zig v0.12.x](https://ziglang.org).
1. Initialize your project repository:
```bash
git init
```
2. Create a `libs` directory inside the root of your project:
```bash
mkdir libs
```
3. Add this library as a submodule of your project:
```bash
git submodule add https://github.com/TSxo/zli libs/zli
```
4. Inside your `build.zig`, bring in the `zli` module and add it as an import
Requires [zig v0.14.x](https://ziglang.org).
1. `zig fetch --save "git+https://github.com/TSxo/zli#v0.1.1"`
2. Inside your `build.zig`, bring in the `zli` module and add it as an import
to your exe:
```zig
const module = b.addModule("zli", .{
.root_source_file = .{
.path = "libs/zli/src/zli.zig",
},
});

// Your typical exe
const exe = b.addExecutable(.{
.name = "project",
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});

exe.root_module.addImport("zli", module);
// Now, add zli to the exe
const zli = b.dependency("zli", .{
.target = target,
.optimize = optimize,
});
exe.root_module.addImport("zli", zli.module("zli"));
```
5. `zli` is now usable in your project:
3. `zli` is now usable in your project:
```zig
const zli = @import("zli");
```
Expand Down
10 changes: 3 additions & 7 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,11 @@ pub fn build(b: *std.Build) void {
const optimize = b.standardOptimizeOption(.{});

const module = b.addModule("zli", .{
.root_source_file = .{
.path = "src/zli.zig",
},
.root_source_file = b.path("src/zli.zig"),
});

const main_tests = b.addTest(.{
.root_source_file = .{ .path = "src/test.zig" },
.root_source_file = b.path("src/test.zig"),
.optimize = optimize,
.target = target,
});
Expand All @@ -24,9 +22,7 @@ pub fn build(b: *std.Build) void {
inline for (.{ "subcommands", "args", "simple" }) |name| {
const example = b.addExecutable(.{
.name = name,
.root_source_file = .{
.path = b.fmt("example/{s}.zig", .{name}),
},
.root_source_file = b.path(b.fmt("example/{s}.zig", .{name})),
.target = target,
.optimize = optimize,
});
Expand Down
44 changes: 44 additions & 0 deletions build.zig.zon
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
.{
.name = .zli,
.version = "0.1.1",
.fingerprint = 0x5b01c9c38221e7a5, // Changing this has security and trust implications.
.minimum_zig_version = "0.14.0",
.dependencies = .{
// See `zig fetch --save <url>` for a command-line interface for adding dependencies.
//.example = .{
// // When updating this field to a new URL, be sure to delete the corresponding
// // `hash`, otherwise you are communicating that you expect to find the old hash at
// // the new URL. If the contents of a URL change this will result in a hash mismatch
// // which will prevent zig from using it.
// .url = "https://example.com/foo.tar.gz",
//
// // This is computed from the file contents of the directory of files that is
// // obtained after fetching `url` and applying the inclusion rules given by
// // `paths`.
// //
// // This field is the source of truth; packages do not come from a `url`; they
// // come from a `hash`. `url` is just one of many possible mirrors for how to
// // obtain a package matching this `hash`.
// //
// // Uses the [multihash](https://multiformats.io/multihash/) format.
// .hash = "...",
//
// // When this is provided, the package is found in a directory relative to the
// // build root. In this case the package's hash is irrelevant and therefore not
// // computed. This field and `url` are mutually exclusive.
// .path = "foo",
//
// // When this is set to `true`, a package is declared to be lazily
// // fetched. This makes the dependency only get fetched if it is
// // actually used.
// .lazy = false,
//},
},

.paths = .{
"build.zig",
"build.zig.zon",
"src",
"LICENSE",
},
}
6 changes: 5 additions & 1 deletion example/simple.zig
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,10 @@ pub fn main() !void {
verbose: bool = false,
positional: struct {
p1: []const u8,
p2: []const u8,
// optional, with default value
p2: []const u8 = "hello",
// optional, with default null
p3: ?[]const u8 = null,
},

pub const aliases = .{
Expand Down Expand Up @@ -95,6 +98,7 @@ pub fn main() !void {
try writer.print("\tVerbose: {}\n", .{values.verbose});
try writer.print("\tPos 1: {s}\n", .{values.positional.p1});
try writer.print("\tPos 2: {s}\n", .{values.positional.p2});
try writer.print("\tPos 3: {?s}\n", .{values.positional.p3});
},
.required => |values| {
try writer.print("Required:\n", .{});
Expand Down
43 changes: 26 additions & 17 deletions src/arg.zig
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ const strings = @import("./strings.zig");
/// Asserts the type of a given value is valid.
pub fn assert_valid_value_type(comptime T: type) void {
comptime {
if (T == []const u8 or T == [:0]const u8 or @typeInfo(T) == .Int) {
if (T == []const u8 or T == [:0]const u8 or @typeInfo(T) == .int) {
return;
}

if (@typeInfo(T) == .Enum) {
const info = @typeInfo(T).Enum;
if (@typeInfo(T) == .@"enum") {
const info = @typeInfo(T).@"enum";
assert(info.is_exhaustive);
assert(info.fields.len >= 2);
return;
Expand All @@ -33,14 +33,13 @@ pub fn assert_valid_value_type(comptime T: type) void {
/// assert(std.mem.eql(name("-v"), "v"));
/// ```
pub fn name(arg: []const u8) []const u8 {
assert(arg.len > 1);
assert(arg[0] == '-');
if (arg.len <= 1) return "";
if (arg[0] != '-') return "";

var from: usize = 1;
var to: usize = arg.len;

if (arg[1] == '-') {
assert(arg.len > 2);
if (arg.len > 2 and arg[1] == '-') {
from = 2;
}

Expand All @@ -66,9 +65,12 @@ test name {
/// assert(std.mem.eql(unparsed_value("-n=5"), "5"));
/// ```
fn unparsed_value(arg: []const u8) []const u8 {
const split_at = strings.index_of(arg, "=").? + 1;
assert(arg.len > split_at);
return arg[split_at..];
if (strings.index_of(arg, "=")) |pos_of_equ| {
const split_at = pos_of_equ + 1;
if (arg.len > split_at)
return arg[split_at..];
}
return "";
}

test unparsed_value {
Expand All @@ -86,7 +88,9 @@ test unparsed_value {
pub fn parse_arg(comptime T: type, arg: []const u8) T {
const arg_name = name(arg);
const val = unparsed_value(arg);
assert(val.len > 0);
if (val.len == 0) {
fatal("Could not parse argument `{s}`: value length is 0. Did you forget the `=`? (like: `-{s}=`)", .{ arg_name, arg_name });
}
return parse_value(T, arg_name, val);
}

Expand All @@ -99,16 +103,21 @@ pub fn parse_arg(comptime T: type, arg: []const u8) T {
/// ```
pub fn parse_value(comptime T: type, arg_name: []const u8, arg_value: []const u8) T {
if (T == bool) return true;
assert(arg_value.len > 0);

// this error is usually handled in parse_arg() already but checking for it
// here doesn't harm.
if (arg_value.len == 0) {
fatal("Value for argument `{s}` has zero length!", .{arg_name});
}

const V = switch (@typeInfo(T)) {
.Optional => |optional| optional.child,
.optional => |optional| optional.child,
else => T,
};

if (V == []const u8 or V == [:0]const u8) return arg_value;
if (@typeInfo(V) == .Int) return parse_value_int(V, arg_name, arg_value);
if (@typeInfo(V) == .Enum) return parse_value_enum(V, arg_name, arg_value);
if (@typeInfo(V) == .int) return parse_value_int(V, arg_name, arg_value);
if (@typeInfo(V) == .@"enum") return parse_value_enum(V, arg_name, arg_value);
comptime unreachable;
}

Expand All @@ -123,7 +132,7 @@ fn parse_value_int(comptime T: type, arg_name: []const u8, val: []const u8) T {
switch (err) {
error.Overflow => fatal(
"{s}: value exceeds {d}-bit {s} integer: '{s}'",
.{ arg_name, @typeInfo(T).Int.bits, @tagName(@typeInfo(T).Int.signedness), val },
.{ arg_name, @typeInfo(T).int.bits, @tagName(@typeInfo(T).int.signedness), val },
),
error.InvalidCharacter => fatal(
"{s}: expected an integer value, but found '{s}' (invalid digit)",
Expand Down Expand Up @@ -151,7 +160,7 @@ test parse_value_int {
/// assert(parse_value_enum(E, "test-enum", "not_ok"), .not_ok);
/// ```
fn parse_value_enum(comptime E: type, arg_name: []const u8, val: []const u8) E {
comptime assert(@typeInfo(E).Enum.is_exhaustive);
comptime assert(@typeInfo(E).@"enum".is_exhaustive);

return std.meta.stringToEnum(E, val) orelse fatal(
"{s}: expected one of {s}, but found '{s}'",
Expand Down
12 changes: 6 additions & 6 deletions src/structs.zig
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ const std = @import("std");
const StructField = std.builtin.Type.StructField;
const assert = std.debug.assert;

/// This is essentially `field.default_value`, but with a useful type instead of
/// This is essentially `field.default_value_ptr`, but with a useful type instead of
/// `?*const anyopaque`.
pub fn default_value(comptime field: StructField) ?field.type {
var out: ?field.type = null;

if (field.default_value) |default_opaque| {
if (field.default_value_ptr) |default_opaque| {
out = @as(*const field.type, @ptrCast(@alignCast(default_opaque))).*;
}

Expand All @@ -22,22 +22,22 @@ pub fn default_value(comptime field: StructField) ?field.type {
/// Each field is of type `Data` and has the provided default, which may be
/// undefined.
pub fn struct_field_struct(comptime S: type, comptime Data: type, comptime default: ?Data) type {
assert(@typeInfo(S) == .Struct);
assert(@typeInfo(S) == .@"struct");

const fields_in = @typeInfo(S).Struct.fields;
const fields_in = @typeInfo(S).@"struct".fields;
var fields_out: [fields_in.len]StructField = undefined;

for (&fields_out, fields_in) |*field_out, field_in| {
field_out.* = .{
.name = field_in.name,
.type = Data,
.default_value = if (default) |d| @as(?*const anyopaque, @ptrCast(&d)) else null,
.default_value_ptr = if (default) |d| @as(?*const anyopaque, @ptrCast(&d)) else null,
.is_comptime = false,
.alignment = if (@sizeOf(Data) > 0) @alignOf(Data) else 0,
};
}

return @Type(.{ .Struct = .{
return @Type(.{ .@"struct" = .{
.layout = .auto,
.fields = &fields_out,
.decls = &.{},
Expand Down
36 changes: 23 additions & 13 deletions src/zli.zig
Original file line number Diff line number Diff line change
Expand Up @@ -114,14 +114,14 @@ pub fn parse(args: *ArgIterator, comptime CLIArgs: type) CLIArgs {
assert(args.skip()); // Discard executable name.

return switch (@typeInfo(CLIArgs)) {
.Union => parse_commands(args, CLIArgs),
.Struct => parse_args(args, CLIArgs),
.@"union" => parse_commands(args, CLIArgs),
.@"struct" => parse_args(args, CLIArgs),
else => unreachable,
};
}

fn parse_commands(args: *ArgIterator, comptime Commands: type) Commands {
comptime assert(@typeInfo(Commands) == .Union);
comptime assert(@typeInfo(Commands) == .@"union");
comptime assert(std.meta.fields(Commands).len > 1);

const command = args.next() orelse fatal(
Expand Down Expand Up @@ -154,7 +154,7 @@ fn parse_args(args: *ArgIterator, comptime Args: type) Args {
return {};
}

comptime assert(@typeInfo(Args) == .Struct);
comptime assert(@typeInfo(Args) == .@"struct");

comptime var fields: [std.meta.fields(Args).len]StructField = undefined;
comptime var field_count = 0;
Expand All @@ -163,21 +163,26 @@ fn parse_args(args: *ArgIterator, comptime Args: type) Args {

comptime for (std.meta.fields(Args)) |field| {
if (strings.eql(field.name, "positional")) {
assert(@typeInfo(field.type) == .Struct);
assert(@typeInfo(field.type) == .@"struct");

positional_fields = std.meta.fields(field.type);

for (positional_fields) |positional_field| {
assert(structs.default_value(positional_field) == null);
argx.assert_valid_value_type(positional_field.type);
switch (@typeInfo(positional_field.type)) {
.optional => |optional| {
// if no default: will be required
argx.assert_valid_value_type(optional.child);
},
else => argx.assert_valid_value_type(positional_field.type),
}
}
} else {
switch (@typeInfo(field.type)) {
.Bool => {
assert(structs.default_value(field).? == false); // boolean flags should have a default
.bool => {
assert(structs.default_value(field).? == false); // boolean flags should have a default of false
},
.Optional => |optional| {
assert(structs.default_value(field).? == null); // optional flags should have a default
.optional => |optional| {
assert(structs.default_value(field).? == null); // optional flags should have a default of null
argx.assert_valid_value_type(optional.child);
},
else => {
Expand Down Expand Up @@ -274,8 +279,13 @@ fn parse_args(args: *ArgIterator, comptime Args: type) Args {
if (@hasField(Args, "positional")) {
assert(counts.positional <= positional_fields.len);
inline for (positional_fields, 0..) |field, idx| {
if (counts.positional == idx) {
fatal("{s}: argument is required", .{field.name});
if (idx >= counts.positional) {
if (positional_fields[idx].default_value_ptr) |_| {
// fill with default
@field(result.positional, positional_fields[idx].name) = positional_fields[idx].defaultValue() orelse null;
} else {
fatal("{s}: argument is required", .{field.name});
}
}
}
}
Expand Down