diff --git a/.gitignore b/.gitignore index 59cbdc3..5de1d86 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ zig-cache/ +.zig-cache/ zig-out/ /release/ /debug/ diff --git a/README.md b/README.md index 0ccd0ae..97a5853 100644 --- a/README.md +++ b/README.md @@ -58,28 +58,12 @@ 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"), @@ -87,9 +71,14 @@ Requires [zig v0.12.x](https://ziglang.org). .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"); ``` diff --git a/build.zig b/build.zig index 0419eb4..e9c5787 100644 --- a/build.zig +++ b/build.zig @@ -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, }); @@ -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, }); diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..cb720e3 --- /dev/null +++ b/build.zig.zon @@ -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 ` 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", + }, +} diff --git a/example/simple.zig b/example/simple.zig index 20232d8..b19f621 100644 --- a/example/simple.zig +++ b/example/simple.zig @@ -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 = .{ @@ -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", .{}); diff --git a/src/arg.zig b/src/arg.zig index bf71cb7..ca0d2ba 100644 --- a/src/arg.zig +++ b/src/arg.zig @@ -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; @@ -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; } @@ -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 { @@ -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); } @@ -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; } @@ -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)", @@ -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}'", diff --git a/src/structs.zig b/src/structs.zig index 55dd47d..3fd9a12 100644 --- a/src/structs.zig +++ b/src/structs.zig @@ -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))).*; } @@ -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 = &.{}, diff --git a/src/zli.zig b/src/zli.zig index 7c68dd3..95e0ecb 100644 --- a/src/zli.zig +++ b/src/zli.zig @@ -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( @@ -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; @@ -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 => { @@ -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}); + } } } }