28 Commits

Author SHA1 Message Date
4bae2c1dd6 update for zig 0.14.0 2025-03-06 01:03:06 -07:00
fd5b724d52 build: update for zig 0.13 2024-06-18 18:05:12 -07:00
d513aa4eaf source: update struct layout enum usage
The enum member name case was changed in
ziglang/zig@099f3c4039. This appears to
be the only change required to bring us up to compatibility with zig
0.12.0-dev.3561+f45ba7d0c.
2024-04-07 14:08:49 -07:00
89360ac197 parser: produce helpful error messages on command line errors
This is the first cut at providing human-readable context for command
line parsing failures. Since these failures are due to incorrect
input (normally produced by a human), closing the information loop at
the human layer makes a hell of a lot more sense than dumping an error
traceback with a (possibly cryptic) error name and calling it a day.

This approach doesn't print anything out by default and still depends
on the user to choose exactly how the handle and print the error
message. Errors are propagated from subcommands, though they end up
being copied, which shouldn't be strictly necessary. Maybe this can be
improved in the future. OutOfMemory has been added to ParseError to
simplify the code a bit.

The demo has been updated with a simplistic example of what presenting
error messages to the user may look like. I don't know that this
produces useful messages for every possible failure scenario, but it
does for the most common ones.
2024-04-07 14:02:16 -07:00
a961b1930a parser: run subcommand parsing/callbacks iteratively
This changes the parse/callback flow so that subcommands are run
iteratively from the base command rather than recursively. The primary
advantages of this approach are some stack space savings and much less
convoluted backtraces for deeply nested command hierarchies. The
overall order of operations has not changed, i.e. the full command
line is parsed before command callback dispatch starts.

Fixes: #12
2024-04-06 16:00:34 -07:00
074db7f4f6 build: update for zig-0.12.0-dev.2208+4debd4338
I may move this commit to a separate branch, since there are a variety
of improvements that I think I want to get applied to the
0.11.x-compatible codebase still. However, I have also not been
motivated to work on those fixes, since this codebase is kind of
crusty due to being the first thing I ever wrote in zig. Doing a
bigger rewrite might supply the motivation to make those improvements.
I will have to think about it. For now, I am going to focus elsewhere.
2024-01-15 22:45:59 -08:00
b77a1f59c2 command: coerce field names to null-terminated strings
ziglang/zig@a02bd81760 changed
builtin.StructField to necessitate this, but this should also be
backwards compatible since these should decay to plain slices just
fine.
2024-01-15 22:42:26 -08:00
03a4404a17 parser: stray var -> const
Surprised there wasn't more of this in here, to be honest.
2024-01-15 22:41:01 -08:00
6e1199afa9 meta: fix redundant qualifiers and improve tuple field generation 2024-01-15 22:40:26 -08:00
70c6cea591 demo: demonstrate preformatted description text functionality
Throw in a lipsum as well to show off the wrapped text better, while
we're at it.
2023-11-08 22:58:01 -08:00
645ef24a4a command: preserve subcommand definition order
This was as simple as switching to an order preserving hashmap. This
lets the user decide which order their subcommands should be presented
in.
2023-11-08 22:56:11 -08:00
768a81e2bd help: hack in support for preformatted lines
This works, but there are probably interesting edge cases around how it
works when directly adjacent to other paragraphs. I will have to think
about it a bit. This wrapping code in general would benefit from term
queries.

Perhaps violating the principle of least astonishment quite severely is
the fact that "> a" and ">" are detected as preformatted, but ">a" is
normal wrapped text. Supporting both ">a" and "> a" leads to
nonobvious whitespace behavior, but this code should not be able to
runtime error outside of the writer dying. This may be reevaluated in
the future, but I will leave it as-is for now.
2023-11-08 22:52:28 -08:00
35915191fb demo: update to use interfaces directly
This adds a group and saves some lines.
2023-09-10 15:27:03 -07:00
76e8dedf14 command: add commandGroup function
This just creates an empty command with an auto-assigned noop callback.
This is useful sugar for creating a group of commands under a common
name because previously the user would have to define their own noop
callback and bind it. This just takes a description string
(and, optionally, a help flag override).
2023-09-10 15:27:03 -07:00
8ac610ae71 command, parser: try to clean up UserContext type handling
This is a feeble attempt to unify some logic, as I realized that
Command.createInterface had different logic for handling the user
context than Parser did, which broke certain use cases (using a slice
as the context for example).

I'm not convinced this really unifies the logic as much as wraps it in
another layer of indirection, but at least the core problem is solved.
2023-09-10 14:50:44 -07:00
8bba68e5a9 help: still print 0-length argument descriptions
I think I had initially intended 0-length descriptions to be "hidden"
options, but this doesn't really work well with arguments, and it also
doesn't make intention clear. Perhaps an additional field should be
added to the parameter specification to support hiding options
(this does not make sense for non-named options).
2023-09-10 14:45:34 -07:00
390a1ba4fd parser: parse into 0-terminated strings
This was kind of an annoying change to make since 0.11.0 has issues
where it will point to the wrong srcloc on compile errors in generic
code (which this 100% is) fortunately fixed in master. The motivation
for this change is that the arg vector already contains 0-terminated
strings, so we can avoid a lot of copies. This makes forwarding
command-line arguments to C-functions that expect zero-terminated
strings much more straightforward, and they also automatically decay
to normal slices.

Unfortunately, environment variable values are NOT zero-terminated, so
they are currently copied with zero-termination. This seems to be the
fault of Windows/WASI, both of which already are performing
allocations (Windows to convert from UTF-16 to UTF-8, and WASI to get
a copy of the environment). By duplicating the std EnvMap
implementation, we could make a version that generates 0-terminated
env vars without extra copies, but I'll skip on doing that for now.
2023-08-27 13:53:14 -07:00
0695743a1f parser: add addSubcommand to ParserInterface
This means that ParserInterface can do all of the important things that
Parser can do, which makes Command.createInterface a lot more useful
(there wasn't previously a way to add subcommands to an interface
created that way without a mass of extremely suspect casting).

This commit also makes the language around this. They're subcommands,
not children, and they have names, not verbs, associated with them.
Glad we could clear that up.
2023-08-22 21:05:27 -07:00
80c4853171 style: use conventional camelCase naming for functions
I think I still prefer snake_case stylistically, but this style fits in
a lot better with other zig code and is the officially endorsed style,
so it makes sense to use it. Since I don't have good test coverage,
some conversions may have been missed, but the demo still builds,
which is a decent amount of coverage. This was pretty easy to do with
a find and replace, so it should be reasonably thorough.
2023-08-05 13:41:21 -07:00
883218cdca parser: add interface method for retrieving a child interface by name
The main value of this method is that it allows runtime access to the
help description of the subcommand. This could allow implementation of
a help flag that takes the name of a subcommand to print help for or
something. Anyway, it's probably useful.
2023-08-04 00:18:38 -07:00
d091de5686 command: add missing ptrCast
While a lot of values will implicitly coerce to this field value,
slices annoyingly do not and thus the explicit cast is required.
2023-08-04 00:15:29 -07:00
29175d07ce command: add a method for creating owned interfaces
This allocates the interface with its own arena allocator, allowing it
to live beyond its stack lifetime. This enables some useful patterns
for composing a CLI from multiple functions or files. This is actually
probably the preferred method over `create_parser` in most
circumstances.
2023-08-04 00:14:40 -07:00
86342bcd1f help: print byte slice defaults as strings
There are a couple of other places where []u8 is treated implicitly
like a string, which isn't strictly correct. Ultimately, some kind of
metasignal will be required to make this type truly unambiguous in
interpretation.
2023-08-04 00:12:26 -07:00
adf05ca489 readme: zig 0.11 is out 2023-08-04 00:08:12 -07:00
71653858ab build: update for zig 0.11 and format
There was an API change to addInstallArtifact. It now takes a second
argument.
2023-08-04 00:07:41 -07:00
d1803284b4 noclip: repub imports
As far as I can tell, there's no good reason not to do this.
2023-07-31 12:40:37 -07:00
5f0d7b34d7 parser: shove an arena allocator in there
Stay a while and listen to my story.

Due to the design of the parser execution flow, the only reasonable way
to avoid leaking memory in the parser is to use an arena allocator
because the parser itself doesn't have direct access to everything it
allocates, and everything it allocates needs to live for the duration
of whatever callbacks are called.

Now, you might say, if the items it allocates are stored for the
lifetime of whatever callbacks, then that means that the items it
allocates stay allocated for effectively the entire life of the
program. In which case there's really not much point in freeing them
at all, as it's just extra work on exit that the OS should normally
clean up. And you'd be right, except for two details: if the user uses
the current GeneralPurposeAllocator, it will complain about leaks when
deinitialized, which simply isn't groovy. The other detail is that
technically the user can run any code they want after the parser
execution finishes, so forcing the user to leak memory by having an
incomplete API is rude.

The other option would be, as before, forcing the user to supply their
own arena allocator if they don't want to leak, but that's kind of a
rude thing to do and goes against the "all allocators look the same"
design of the standard library, which is what makes it so easy to use
and create allocators with advanced functionality. That seems like an
ugly thing to do, so, instead, each parser gets to eat the memory cost
of storing a pointer to its arena allocator (and the heap cost of the
arena allocator itself).

In theory, subcommands could borrow the arena allocator of their parent
command to save a bit of heap space, but that would make a variety of
creation and cleanup-related tasks less isomorphic between the parents
and the subcommands. I like the current design where commands and
subcommands are the same thing, and I'm not in a rush to disturb that.
I don't think the overhead cost of the arena allocator itself, which
can be measured in double digit bytes, is a particularly steep price
to pay.
2023-07-20 23:15:37 -07:00
efbc6e7b66 all: update for changed zig builtins
enumToInt changed to intFromEnum, and the casting builtins figured out
how to automagically infer the cast type. This results in some minor
simplification, which is nice.
2023-07-19 00:32:14 -07:00
12 changed files with 848 additions and 413 deletions

View File

@@ -1,11 +1,11 @@
const std = @import("std");
pub fn build(b: *std.Build) void {
const target: std.zig.CrossTarget = b.standardTargetOptions(.{});
const target: std.Build.ResolvedTarget = b.standardTargetOptions(.{});
const optimize: std.builtin.Mode = b.standardOptimizeOption(.{});
const noclip = b.addModule("noclip", .{
.source_file = .{ .path = "source/noclip.zig" }
.root_source_file = b.path("source/noclip.zig"),
});
demo(b, noclip, target, optimize);
@@ -13,7 +13,7 @@ pub fn build(b: *std.Build) void {
const test_step = b.step("test", "Run unit tests");
const tests = b.addTest(.{
.name = "tests",
.root_source_file = .{ .path = "source/noclip.zig" },
.root_source_file = b.path("source/noclip.zig"),
.target = target,
.optimize = optimize,
});
@@ -21,17 +21,22 @@ pub fn build(b: *std.Build) void {
test_step.dependOn(&tests.step);
}
fn demo(b: *std.Build, noclip: *std.Build.Module, target: std.zig.CrossTarget, optimize: std.builtin.Mode) void {
fn demo(
b: *std.Build,
noclip: *std.Build.Module,
target: std.Build.ResolvedTarget,
optimize: std.builtin.Mode,
) void {
const demo_step = b.step("demo", "Build and install CLI demo program");
const exe = b.addExecutable(.{
.name = "noclip-demo",
.root_source_file = .{ .path = "demo/demo.zig" },
.root_source_file = b.path("demo/demo.zig"),
.target = target,
.optimize = optimize,
});
exe.addModule("noclip", noclip);
const install_demo = b.addInstallArtifact(exe);
exe.root_module.addImport("noclip", noclip);
const install_demo = b.addInstallArtifact(exe, .{});
demo_step.dependOn(&install_demo.step);
}

13
build.zig.zon Normal file
View File

@@ -0,0 +1,13 @@
.{
.name = .NOCLIP,
.fingerprint = 0xE4C223E8CB9C8ADF,
.version = "0.1.0-pre",
.minimum_zig_version = "0.14.0",
.dependencies = .{},
.paths = .{
"source",
"build.zig",
"build.zig.zon",
"license",
},
}

View File

@@ -8,12 +8,25 @@ const Choice = enum { first, second };
const cli = cmd: {
var cmd = CommandBuilder(*u32){
.description =
\\The definitive noclip demonstration utility
\\The definitive noclip demonstration utility.
\\
\\This command demonstrates the functionality of the noclip library. cool!
\\
\\> // implementing factorial recursively is a silly thing to do
\\> pub fn fact(n: u64) u64 {
\\> if (n == 0) return 1;
\\> return n*fact(n - 1);
\\> }
\\
\\Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
\\incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis
\\nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
\\Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore
\\eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident,
\\sunt in culpa qui officia deserunt mollit anim id est laborum.
,
};
cmd.add_option(.{ .OutputType = struct { u8, u8 } }, .{
cmd.addOption(.{ .OutputType = struct { u8, u8 } }, .{
.name = "test",
.short_tag = "-t",
.long_tag = "--test",
@@ -21,7 +34,7 @@ const cli = cmd: {
.description = "multi-value test option",
.nice_type_name = "int> <int",
});
cmd.add_option(.{ .OutputType = Choice }, .{
cmd.addOption(.{ .OutputType = Choice }, .{
.name = "choice",
.short_tag = "-c",
.long_tag = "--choice",
@@ -30,7 +43,14 @@ const cli = cmd: {
.description = "enum choice option",
.nice_type_name = "choice",
});
cmd.add_option(.{ .OutputType = u32 }, .{
cmd.stringOption(.{
.name = "string",
.short_tag = "-s",
.long_tag = "--string",
.env_var = "NOCLIP_STRING",
.description = "A string value option",
});
cmd.addOption(.{ .OutputType = u32 }, .{
.name = "default",
.short_tag = "-d",
.long_tag = "--default",
@@ -39,33 +59,29 @@ const cli = cmd: {
.description = "default value integer option",
.nice_type_name = "uint",
});
cmd.add_option(.{ .OutputType = u8, .multi = true }, .{
cmd.addOption(.{ .OutputType = u8, .multi = true }, .{
.name = "multi",
.short_tag = "-m",
.long_tag = "--multi",
.description = "multiple specification test option",
});
cmd.add_flag(.{}, .{
cmd.addFlag(.{}, .{
.name = "flag",
.truthy = .{ .short_tag = "-f", .long_tag = "--flag" },
.falsy = .{ .short_tag = "-F", .long_tag = "--no-flag" },
.env_var = "NOCLIP_FLAG",
.description = "boolean flag",
});
cmd.add_flag(.{ .multi = true }, .{
cmd.addFlag(.{ .multi = true }, .{
.name = "multiflag",
.truthy = .{ .short_tag = "-M" },
.description = "multiple specification test flag ",
});
cmd.add_option(.{ .OutputType = u8 }, .{
cmd.addOption(.{ .OutputType = u8 }, .{
.name = "env",
.env_var = "NOCLIP_ENVIRON",
.description = "environment variable only option",
});
cmd.add_argument(.{ .OutputType = []const u8 }, .{
.name = "arg",
.description = "This is an argument that doesn't really do anything, but it's very important.",
});
break :cmd cmd;
};
@@ -73,47 +89,66 @@ const cli = cmd: {
const subcommand = cmd: {
var cmd = CommandBuilder([]const u8){
.description =
\\Perform some sort of work
\\Demonstrate subcommand functionality
\\
\\This subcommand is a mystery. It probably does something, but nobody is sure what.
\\This command demonstrates how subcommands work.
,
};
cmd.simple_flag(.{
cmd.simpleFlag(.{
.name = "flag",
.truthy = .{ .short_tag = "-f", .long_tag = "--flag" },
.falsy = .{ .long_tag = "--no-flag" },
.env_var = "NOCLIP_SUBFLAG",
});
cmd.add_argument(.{ .OutputType = []const u8 }, .{ .name = "argument" });
cmd.stringArgument(.{ .name = "argument" });
cmd.stringArgument(.{
.name = "arg",
.description = "This is an argument that doesn't really do anything, but it's very important.",
});
break :cmd cmd;
};
fn sub_handler(context: []const u8, result: subcommand.Output()) !void {
fn subHandler(context: []const u8, result: subcommand.Output()) !void {
std.debug.print("subcommand: {s}\n", .{result.argument});
std.debug.print("context: {s}\n", .{context});
}
fn cli_handler(context: *u32, result: cli.Output()) !void {
fn cliHandler(context: *u32, result: cli.Output()) !void {
std.debug.print("context: {d}\n", .{context.*});
std.debug.print("callback is working {s}\n", .{result.string orelse "null"});
std.debug.print("callback is working {any}\n", .{result.choice});
std.debug.print("callback is working {d}\n", .{result.default});
context.* += 1;
}
pub fn main() !u8 {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const allocator = arena.allocator();
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const base = try noclip.commandGroup(allocator, .{ .description = "base group" });
defer base.deinitTree();
var parser = cli.create_parser(cli_handler, allocator);
var context: u32 = 2;
const sc: []const u8 = "whassup";
var subcon = subcommand.create_parser(sub_handler, allocator);
try parser.add_subcommand("verb", subcon.interface(&sc));
try base.addSubcommand("main", try cli.createInterface(allocator, cliHandler, &context));
try base.addSubcommand("other", try subcommand.createInterface(allocator, subHandler, &sc));
const iface = parser.interface(&context);
iface.execute() catch return 1;
const group = try noclip.commandGroup(allocator, .{ .description = "final level of a deeply nested subcommand" });
const subcon = try noclip.commandGroup(allocator, .{ .description = "third level of a deeply nested subcommand" });
const nested = try noclip.commandGroup(allocator, .{ .description = "second level of a deeply nested subcommand" });
const deeply = try noclip.commandGroup(allocator, .{ .description = "start of a deeply nested subcommand" });
try base.addSubcommand("deeply", deeply);
try deeply.addSubcommand("nested", nested);
try nested.addSubcommand("subcommand", subcon);
try subcon.addSubcommand("group", group);
try group.addSubcommand("run", try cli.createInterface(allocator, cliHandler, &context));
base.execute() catch |err| {
std.io.getStdErr().writeAll(base.getParseError()) catch {};
return err;
};
return 0;
}

View File

@@ -22,8 +22,7 @@ ____
== Hello
Requires a pre-release version of Zig `0.11.0`. `0.11.0-dev.1844+75ff34db9` is the
oldest version specifically known to me to work.
Requires Zig `0.13.x`. May work with `0.12.x`.
=== Features
@@ -40,7 +39,7 @@ and bugs likely exist.
* arbitrarily nestable subcommands for building sophisticated tools
* parser that supports many conventional CLI behaviors
** congealed short flags (e.g. `-abc` is the same as `-a -b -c`)
** `--` to force early of argument parsing
** `--` to force early end of flag parsing
** both `--long value` and `--long=value` styles are supported
* option values are converted into rich native types using conversion callback functions
** integers

View File

@@ -12,8 +12,8 @@ const OptionConfig = parameters.OptionConfig;
const FlagConfig = parameters.FlagConfig;
const ShortLongPair = parameters.ShortLongPair;
const FlagBias = parameters.FlagBias;
const make_option = parameters.make_option;
const make_argument = parameters.make_argument;
const makeOption = parameters.makeOption;
const makeArgument = parameters.makeArgument;
const Parser = parser.Parser;
const ParserInterface = parser.ParserInterface;
@@ -24,7 +24,7 @@ fn BuilderGenerics(comptime UserContext: type) type {
value_count: ValueCount = .{ .fixed = 1 },
multi: bool = false,
pub fn arg_gen(comptime self: @This()) ParameterGenerics {
pub fn argGen(comptime self: @This()) ParameterGenerics {
if (self.value_count == .flag) @compileError("argument may not be a flag");
if (self.value_count == .count) @compileError("argument may not be a count");
@@ -32,12 +32,12 @@ fn BuilderGenerics(comptime UserContext: type) type {
.UserContext = UserContext,
.OutputType = self.OutputType,
.param_type = .Ordinal,
.value_count = ParameterGenerics.fixed_value_count(self.OutputType, self.value_count),
.value_count = ParameterGenerics.fixedValueCount(self.OutputType, self.value_count),
.multi = self.multi,
};
}
pub fn opt_gen(comptime self: @This()) ParameterGenerics {
pub fn optGen(comptime self: @This()) ParameterGenerics {
if (self.value_count == .flag) @compileError("option may not be a flag");
if (self.value_count == .count) @compileError("option may not be a count");
@@ -45,12 +45,12 @@ fn BuilderGenerics(comptime UserContext: type) type {
.UserContext = UserContext,
.OutputType = self.OutputType,
.param_type = .Nominal,
.value_count = ParameterGenerics.fixed_value_count(self.OutputType, self.value_count),
.value_count = ParameterGenerics.fixedValueCount(self.OutputType, self.value_count),
.multi = self.multi,
};
}
pub fn count_gen(comptime _: @This()) ParameterGenerics {
pub fn countGen(comptime _: @This()) ParameterGenerics {
return ParameterGenerics{
.UserContext = UserContext,
.OutputType = usize,
@@ -60,7 +60,7 @@ fn BuilderGenerics(comptime UserContext: type) type {
};
}
pub fn flag_gen(comptime self: @This()) ParameterGenerics {
pub fn flagGen(comptime self: @This()) ParameterGenerics {
return ParameterGenerics{
.UserContext = UserContext,
.OutputType = bool,
@@ -72,6 +72,76 @@ fn BuilderGenerics(comptime UserContext: type) type {
};
}
pub const GroupOptions = struct {
help_flag: ShortLongPair = .{ .short_tag = "-h", .long_tag = "--help" },
description: []const u8,
};
pub fn commandGroup(allocator: std.mem.Allocator, comptime options: GroupOptions) !ParserInterface {
const cmd = comptime CommandBuilder(void){
.help_flag = options.help_flag,
.description = options.description,
.subcommand_required = true,
};
return try cmd.createInterface(allocator, cmd.noopCallback());
}
fn InterfaceCreator(comptime Command: type) type {
return if (Command.ICC.InputType()) |Type|
struct {
pub fn createInterface(
comptime self: Command,
allocator: std.mem.Allocator,
comptime callback: self.CallbackSignature(),
context: Type,
) !ParserInterface {
return try self._createInterfaceImpl(allocator, callback, context);
}
}
else
struct {
pub fn createInterface(
comptime self: Command,
allocator: std.mem.Allocator,
comptime callback: self.CallbackSignature(),
) !ParserInterface {
return try self._createInterfaceImpl(allocator, callback, void{});
}
};
}
pub const InterfaceContextCategory = union(enum) {
empty,
pointer: type,
value: type,
pub fn fromType(comptime ContextType: type) InterfaceContextCategory {
return switch (@typeInfo(ContextType)) {
.void => .empty,
.pointer => |info| if (info.size == .slice) .{ .value = ContextType } else .{ .pointer = ContextType },
// technically, i0, u0, and struct{} should be treated as empty, probably
else => .{ .value = ContextType },
};
}
pub fn InputType(comptime self: InterfaceContextCategory) ?type {
return switch (self) {
.empty => null,
.pointer => |Type| Type,
.value => |Type| *const Type,
};
}
pub fn OutputType(comptime self: InterfaceContextCategory) type {
return switch (self) {
.empty => void,
.pointer => |Type| Type,
.value => |Type| Type,
};
}
};
pub fn CommandBuilder(comptime UserContext: type) type {
return struct {
param_spec: ncmeta.TupleBuilder = .{},
@@ -82,71 +152,106 @@ pub fn CommandBuilder(comptime UserContext: type) type {
description: []const u8,
pub const UserContextType = UserContext;
pub const ICC: InterfaceContextCategory = InterfaceContextCategory.fromType(UserContextType);
pub fn create_parser(
pub fn createParser(
comptime self: @This(),
comptime callback: self.CallbackSignature(),
allocator: std.mem.Allocator,
) Parser(self, callback) {
) !Parser(self, callback) {
// note: this is freed in Parser.deinit
var arena = try allocator.create(std.heap.ArenaAllocator);
arena.* = std.heap.ArenaAllocator.init(allocator);
const arena_alloc = arena.allocator();
return Parser(self, callback){
.allocator = allocator,
.subcommands = std.hash_map.StringHashMap(ParserInterface).init(allocator),
.help_builder = help.HelpBuilder(self).init(allocator),
.arena = arena,
.allocator = arena_alloc,
.subcommands = parser.CommandMap.init(arena_alloc),
.help_builder = help.HelpBuilder(self).init(arena_alloc),
};
}
pub fn set_help_flag(
pub const ifc = InterfaceCreator(@This());
pub const createInterface = ifc.createInterface;
fn _createInterfaceImpl(
comptime self: @This(),
allocator: std.mem.Allocator,
comptime callback: self.CallbackSignature(),
context: (ICC.InputType() orelse void),
) !ParserInterface {
var arena = try allocator.create(std.heap.ArenaAllocator);
arena.* = std.heap.ArenaAllocator.init(allocator);
const arena_alloc = arena.allocator();
var this_parser = try arena_alloc.create(Parser(self, callback));
this_parser.* = .{
.arena = arena,
.allocator = arena_alloc,
.subcommands = parser.CommandMap.init(arena_alloc),
.help_builder = help.HelpBuilder(self).init(arena_alloc),
};
if (comptime ICC == .empty) {
return this_parser.interface();
} else {
return this_parser.interface(context);
}
}
pub fn setHelpFlag(
comptime self: *@This(),
comptime tags: ShortLongPair,
) void {
self.help_flag = tags;
}
const string_generics = BuilderGenerics(UserContext){ .OutputType = []const u8 };
const string_generics = BuilderGenerics(UserContext){ .OutputType = [:0]const u8 };
pub fn string_option(
pub fn stringOption(
comptime self: *@This(),
comptime cfg: OptionConfig(string_generics.opt_gen()),
comptime cfg: OptionConfig(string_generics.optGen()),
) void {
const config = if (cfg.nice_type_name == null)
ncmeta.copy_struct(@TypeOf(cfg), cfg, .{ .nice_type_name = "string" })
ncmeta.copyStruct(@TypeOf(cfg), cfg, .{ .nice_type_name = "string" })
else
cfg;
self.add_option(string_generics, config);
self.addOption(string_generics, config);
}
pub fn string_argument(
pub fn stringArgument(
comptime self: *@This(),
comptime cfg: OptionConfig(string_generics.arg_gen()),
comptime cfg: OptionConfig(string_generics.argGen()),
) void {
const config = if (cfg.nice_type_name == null)
ncmeta.copy_struct(@TypeOf(cfg), cfg, .{ .nice_type_name = "string" })
ncmeta.copyStruct(@TypeOf(cfg), cfg, .{ .nice_type_name = "string" })
else
cfg;
self.add_argument(string_generics, config);
self.addArgument(string_generics, config);
}
pub fn simple_flag(
pub fn simpleFlag(
comptime self: *@This(),
comptime cfg: FlagConfig(string_generics.flag_gen()),
comptime cfg: FlagConfig(string_generics.flagGen()),
) void {
self.add_flag(string_generics, cfg);
self.addFlag(string_generics, cfg);
}
pub fn add_argument(
pub fn addArgument(
comptime self: *@This(),
comptime bgen: BuilderGenerics(UserContext),
comptime config: OptionConfig(bgen.arg_gen()),
comptime config: OptionConfig(bgen.argGen()),
) void {
self.param_spec.add(make_argument(bgen.arg_gen(), config));
self.param_spec.add(makeArgument(bgen.argGen(), config));
}
pub fn add_option(
pub fn addOption(
comptime self: *@This(),
comptime bgen: BuilderGenerics(UserContext),
comptime config: OptionConfig(bgen.opt_gen()),
comptime config: OptionConfig(bgen.optGen()),
) void {
if (comptime bgen.value_count == .fixed and bgen.value_count.fixed == 0) {
@compileError(
@@ -155,13 +260,13 @@ pub fn CommandBuilder(comptime UserContext: type) type {
);
}
self.param_spec.add(make_option(bgen.opt_gen(), config));
self.param_spec.add(makeOption(bgen.optGen(), config));
}
pub fn add_flag(
pub fn addFlag(
comptime self: *@This(),
comptime bgen: BuilderGenerics(UserContext),
comptime config: FlagConfig(bgen.flag_gen()),
comptime config: FlagConfig(bgen.flagGen()),
) void {
comptime {
if (config.truthy == null and config.falsy == null and config.env_var == null) {
@@ -172,7 +277,7 @@ pub fn CommandBuilder(comptime UserContext: type) type {
);
}
const generics = bgen.flag_gen();
const generics = bgen.flagGen();
var args = OptionConfig(generics){
.name = config.name,
//
@@ -205,7 +310,7 @@ pub fn CommandBuilder(comptime UserContext: type) type {
args.long_tag = truthy_pair.long_tag;
args.flag_bias = .truthy;
self.param_spec.add(make_option(generics, args));
self.param_spec.add(makeOption(generics, args));
}
if (config.falsy) |falsy_pair| {
@@ -221,7 +326,7 @@ pub fn CommandBuilder(comptime UserContext: type) type {
args.long_tag = falsy_pair.long_tag;
args.flag_bias = .falsy;
self.param_spec.add(make_option(generics, args));
self.param_spec.add(makeOption(generics, args));
}
if (config.env_var) |env_var| {
@@ -231,7 +336,7 @@ pub fn CommandBuilder(comptime UserContext: type) type {
args.env_var = env_var;
args.flag_bias = .unbiased;
self.param_spec.add(make_option(generics, args));
self.param_spec.add(makeOption(generics, args));
}
}
}
@@ -240,8 +345,14 @@ pub fn CommandBuilder(comptime UserContext: type) type {
return self.param_spec.realTuple();
}
pub fn noopCallback(comptime self: @This()) self.CallbackSignature() {
return struct {
fn callback(_: UserContextType, _: self.Output()) !void {}
}.callback;
}
pub fn CallbackSignature(comptime self: @This()) type {
return *const fn (UserContext, self.Output()) anyerror!void;
return *const fn (UserContextType, self.Output()) anyerror!void;
}
pub fn Output(comptime self: @This()) type {
@@ -262,27 +373,29 @@ pub fn CommandBuilder(comptime UserContext: type) type {
// global tags and env_vars would conflict, which is less common.
if (param.short_tag) |short|
tag_fields = tag_fields ++ &[_]StructField{.{
.name = short,
// this goofy construct coerces the comptime []const u8 to
// [:0]const u8.
.name = short ++ "",
.type = void,
.default_value = null,
.default_value_ptr = null,
.is_comptime = false,
.alignment = 0,
}};
if (param.long_tag) |long|
tag_fields = tag_fields ++ &[_]StructField{.{
.name = long,
.name = long ++ "",
.type = void,
.default_value = null,
.default_value_ptr = null,
.is_comptime = false,
.alignment = 0,
}};
if (param.env_var) |env_var|
env_var_fields = env_var_fields ++ &[_]StructField{.{
.name = env_var,
.name = env_var ++ "",
.type = void,
.default_value = null,
.default_value_ptr = null,
.is_comptime = false,
.alignment = 0,
}};
@@ -296,16 +409,16 @@ pub fn CommandBuilder(comptime UserContext: type) type {
if (PType.is_flag) {
var peek = idx + 1;
var bias_seen: [ncmeta.enum_length(FlagBias)]bool = [_]bool{false} ** ncmeta.enum_length(FlagBias);
bias_seen[@enumToInt(param.flag_bias)] = true;
var bias_seen: [ncmeta.enumLength(FlagBias)]bool = [_]bool{false} ** ncmeta.enumLength(FlagBias);
bias_seen[@intFromEnum(param.flag_bias)] = true;
while (peek < spec.len) : (peek += 1) {
const peek_param = spec[peek];
if (@TypeOf(peek_param).is_flag and std.mem.eql(u8, param.name, peek_param.name)) {
if (bias_seen[@enumToInt(peek_param.flag_bias)] == true) {
if (bias_seen[@intFromEnum(peek_param.flag_bias)] == true) {
@compileError("redundant flag!!!! " ++ param.name);
} else {
bias_seen[@enumToInt(peek_param.flag_bias)] = true;
bias_seen[@intFromEnum(peek_param.flag_bias)] = true;
}
flag_skip += 1;
} else {
@@ -325,30 +438,30 @@ pub fn CommandBuilder(comptime UserContext: type) type {
const default = if (param.default) |def| &@as(FieldType, def) else @as(?*const anyopaque, null);
fields = fields ++ &[_]StructField{.{
.name = param.name,
.name = param.name ++ "",
.type = FieldType,
.default_value = default,
.default_value_ptr = @ptrCast(default),
.is_comptime = false,
.alignment = @alignOf(FieldType),
}};
}
_ = @Type(.{ .Struct = .{
.layout = .Auto,
_ = @Type(.{ .@"struct" = .{
.layout = .auto,
.fields = tag_fields,
.decls = &.{},
.is_tuple = false,
} });
_ = @Type(.{ .Struct = .{
.layout = .Auto,
_ = @Type(.{ .@"struct" = .{
.layout = .auto,
.fields = env_var_fields,
.decls = &.{},
.is_tuple = false,
} });
return @Type(.{ .Struct = .{
.layout = .Auto,
return @Type(.{ .@"struct" = .{
.layout = .auto,
.fields = fields,
.decls = &.{},
.is_tuple = false,
@@ -371,16 +484,16 @@ pub fn CommandBuilder(comptime UserContext: type) type {
const PType = @TypeOf(param);
if (PType.is_flag) {
var peek = idx + 1;
var bias_seen: [ncmeta.enum_length(FlagBias)]bool = [_]bool{false} ** ncmeta.enum_length(FlagBias);
bias_seen[@enumToInt(param.flag_bias)] = true;
var bias_seen: [ncmeta.enumLength(FlagBias)]bool = [_]bool{false} ** ncmeta.enumLength(FlagBias);
bias_seen[@intFromEnum(param.flag_bias)] = true;
while (peek < spec.len) : (peek += 1) {
const peek_param = spec[peek];
if (@TypeOf(peek_param).is_flag and std.mem.eql(u8, param.name, peek_param.name)) {
if (bias_seen[@enumToInt(peek_param.flag_bias)] == true) {
if (bias_seen[@intFromEnum(peek_param.flag_bias)] == true) {
@compileError("redundant flag!!!! " ++ param.name);
} else {
bias_seen[@enumToInt(peek_param.flag_bias)] = true;
bias_seen[@intFromEnum(peek_param.flag_bias)] = true;
}
flag_skip += 1;
} else {
@@ -395,22 +508,19 @@ pub fn CommandBuilder(comptime UserContext: type) type {
?PType.G.IntermediateType();
fields = &(@as([fields.len]StructField, fields[0..fields.len].*) ++ [1]StructField{.{
.name = param.name,
.name = param.name ++ "",
.type = FieldType,
.default_value = @ptrCast(
?*const anyopaque,
&@as(
FieldType,
if (PType.value_count == .count) 0 else null,
),
),
.default_value_ptr = @ptrCast(&@as(
FieldType,
if (PType.value_count == .count) 0 else null,
)),
.is_comptime = false,
.alignment = @alignOf(?[]const u8),
}});
}
return @Type(.{ .Struct = .{
.layout = .Auto,
return @Type(.{ .@"struct" = .{
.layout = .auto,
.fields = fields,
.decls = &.{},
.is_tuple = false,

View File

@@ -17,30 +17,30 @@ pub fn ConverterSignature(comptime gen: ParameterGenerics) type {
) ConversionError!gen.ConvertedType();
}
pub fn default_converter(comptime gen: ParameterGenerics) ?ConverterSignature(gen) {
pub fn DefaultConverter(comptime gen: ParameterGenerics) ?ConverterSignature(gen) {
return if (comptime gen.multi)
multi_converter(gen)
MultiConverter(gen)
else switch (@typeInfo(gen.OutputType)) {
.Bool => flag_converter(gen),
.Int => int_converter(gen),
.Pointer => |info| if (info.size == .Slice and info.child == u8)
string_converter(gen)
.bool => FlagConverter(gen),
.int => IntConverter(gen),
.pointer => |info| if (info.size == .slice and info.child == u8)
StringConverter(gen)
else
null,
.Enum => |info| if (info.is_exhaustive) choice_converter(gen) else null,
.@"enum" => |info| if (info.is_exhaustive) ChoiceConverter(gen) else null,
// TODO: how to handle structs with field defaults? maybe this should only work
// for tuples, which I don't think can have defaults.
.Struct => |info| if (gen.value_count == .fixed and gen.value_count.fixed == info.fields.len)
struct_converter(gen)
.@"struct" => |info| if (gen.value_count == .fixed and gen.value_count.fixed == info.fields.len)
StructConverter(gen)
else
null,
else => null,
};
}
fn multi_converter(comptime gen: ParameterGenerics) ?ConverterSignature(gen) {
const converter = default_converter(
ncmeta.copy_struct(ParameterGenerics, gen, .{ .multi = false }),
fn MultiConverter(comptime gen: ParameterGenerics) ?ConverterSignature(gen) {
const converter = DefaultConverter(
ncmeta.copyStruct(ParameterGenerics, gen, .{ .multi = false }),
) orelse
@compileError("no default converter");
const Intermediate = gen.IntermediateType();
@@ -59,9 +59,9 @@ fn multi_converter(comptime gen: ParameterGenerics) ?ConverterSignature(gen) {
}.handler;
}
fn flag_converter(comptime gen: ParameterGenerics) ConverterSignature(gen) {
fn FlagConverter(comptime gen: ParameterGenerics) ConverterSignature(gen) {
return struct {
pub fn handler(_: gen.UserContext, input: []const u8, _: ErrorWriter) ConversionError!bool {
pub fn handler(_: gen.UserContext, input: [:0]const u8, _: ErrorWriter) ConversionError!bool {
// treat an empty string as falsy
if (input.len == 0) return false;
@@ -79,19 +79,19 @@ fn flag_converter(comptime gen: ParameterGenerics) ConverterSignature(gen) {
}.handler;
}
fn string_converter(comptime gen: ParameterGenerics) ConverterSignature(gen) {
fn StringConverter(comptime gen: ParameterGenerics) ConverterSignature(gen) {
return struct {
pub fn handler(_: gen.UserContext, input: []const u8, _: ErrorWriter) ConversionError![]const u8 {
pub fn handler(_: gen.UserContext, input: [:0]const u8, _: ErrorWriter) ConversionError![:0]const u8 {
return input;
}
}.handler;
}
fn int_converter(comptime gen: ParameterGenerics) ConverterSignature(gen) {
fn IntConverter(comptime gen: ParameterGenerics) ConverterSignature(gen) {
const IntType = gen.OutputType;
return struct {
pub fn handler(_: gen.UserContext, input: []const u8, failure: ErrorWriter) ConversionError!IntType {
pub fn handler(_: gen.UserContext, input: [:0]const u8, failure: ErrorWriter) ConversionError!IntType {
return std.fmt.parseInt(IntType, input, 0) catch {
try failure.print("cannot interpret \"{s}\" as an integer", .{input});
return ConversionError.ConversionFailed;
@@ -100,9 +100,9 @@ fn int_converter(comptime gen: ParameterGenerics) ConverterSignature(gen) {
}.handler;
}
fn struct_converter(comptime gen: ParameterGenerics) ConverterSignature(gen) {
fn StructConverter(comptime gen: ParameterGenerics) ConverterSignature(gen) {
const StructType = gen.OutputType;
const type_info = @typeInfo(StructType).Struct;
const type_info = @typeInfo(StructType).@"struct";
const Intermediate = gen.IntermediateType();
return struct {
@@ -117,15 +117,15 @@ fn struct_converter(comptime gen: ParameterGenerics) ConverterSignature(gen) {
var result: StructType = undefined;
inline for (comptime type_info.fields, 0..) |field, idx| {
const converter = comptime default_converter(
ncmeta.copy_struct(ParameterGenerics, gen, .{
const Converter = comptime DefaultConverter(
ncmeta.copyStruct(ParameterGenerics, gen, .{
.OutputType = field.type,
.value_count = .{ .fixed = 1 },
.value_count = @as(parameters.ValueCount, .{ .fixed = 1 }),
}),
) orelse
@compileError("cannot get converter for field" ++ field.name);
@field(result, field.name) = try converter(context, input.items[idx], failure);
@field(result, field.name) = try Converter(context, input.items[idx], failure);
}
return result;
@@ -133,11 +133,11 @@ fn struct_converter(comptime gen: ParameterGenerics) ConverterSignature(gen) {
}.handler;
}
fn choice_converter(comptime gen: ParameterGenerics) ConverterSignature(gen) {
fn ChoiceConverter(comptime gen: ParameterGenerics) ConverterSignature(gen) {
const EnumType = gen.OutputType;
return struct {
pub fn handler(_: gen.UserContext, input: []const u8, failure: ErrorWriter) ConversionError!EnumType {
pub fn handler(_: gen.UserContext, input: [:0]const u8, failure: ErrorWriter) ConversionError!EnumType {
return std.meta.stringToEnum(gen.ConvertedType(), input) orelse {
try failure.print("\"{s}\" is not a valid choice", .{input});
return ConversionError.ConversionFailed;

View File

@@ -12,6 +12,7 @@ pub const ParseError = error{
UnknownLongTagParameter,
UnknownShortTagParameter,
RequiredParameterMissing,
OutOfMemory,
};
pub const NoclipError = ParseError || ConversionError;

View File

@@ -20,7 +20,7 @@ pub fn StructuredPrinter(comptime Writer: type) type {
wrap_width: usize = 100,
writer: Writer,
pub fn print_pair(self: *@This(), pair: AlignablePair, leading_indent: u8, tabstop: usize) !void {
pub fn printPair(self: *@This(), pair: AlignablePair, leading_indent: u8, tabstop: usize) !void {
try self.writer.writeByteNTimes(' ', leading_indent);
const left = std.mem.trim(u8, pair.left, " \n");
try self.writer.writeAll(left);
@@ -30,40 +30,53 @@ pub fn StructuredPrinter(comptime Writer: type) type {
if (offset > tabstop) return NoclipError.UnexpectedFailure;
try self.writer.writeByteNTimes(' ', tabstop - offset);
try self.print_rewrap(std.mem.trim(u8, pair.right, " \n"), tabstop);
try self.printRewrap(std.mem.trim(u8, pair.right, " \n"), tabstop);
try self.writer.writeByte('\n');
}
pub fn print_pair_brief(self: *@This(), pair: AlignablePair, leading_indent: u8, tabstop: usize) !void {
pub fn printPairBrief(self: *@This(), pair: AlignablePair, leading_indent: u8, tabstop: usize) !void {
const brief = ncmeta.partition(u8, pair.right, &[_][]const u8{"\n\n"})[0];
const simulacrum: AlignablePair = .{
.left = pair.left,
.right = brief,
};
try self.print_pair(simulacrum, leading_indent, tabstop);
try self.printPair(simulacrum, leading_indent, tabstop);
}
pub fn print_wrapped(self: *@This(), text: []const u8, leading_indent: usize) !void {
pub fn printWrapped(self: *@This(), text: []const u8, leading_indent: usize) !void {
try self.writer.writeByteNTimes(' ', leading_indent);
try self.print_rewrap(std.mem.trim(u8, text, "\n"), leading_indent);
try self.printRewrap(std.mem.trim(u8, text, "\n"), leading_indent);
}
fn print_rewrap(self: *@This(), text: []const u8, indent: usize) !void {
fn printRewrap(self: *@This(), text: []const u8, indent: usize) !void {
// TODO: lol return a real error
if (indent >= self.wrap_width) return NoclipError.UnexpectedFailure;
if (text.len == 0) return;
// this assumes output stream has already had the first line properly
// indented.
var splitter = std.mem.split(u8, text, "\n");
var splitter = std.mem.splitScalar(u8, text, '\n');
var location: usize = indent;
while (splitter.next()) |line| {
if (line.len == 0) {
// we have a trailing line that needs to be cleaned up
if (location > indent)
_ = try self.clear_line(indent);
location = try self.clear_line(indent);
_ = try self.clearLine(indent);
location = try self.clearLine(indent);
continue;
}
if (line[0] == '>') maybe: {
if (line.len > 1) {
if (line[1] == ' ') {
try self.writer.writeAll(line[2..]);
} else break :maybe;
}
location = try self.clearLine(indent);
continue;
}
@@ -91,7 +104,7 @@ pub fn StructuredPrinter(comptime Writer: type) type {
break;
}
if (location != indent)
location = try self.clear_line(indent);
location = try self.clearLine(indent);
need_forced_break = true;
continue :choppa;
@@ -99,14 +112,15 @@ pub fn StructuredPrinter(comptime Writer: type) type {
}
if (location > indent)
try self.writer.writeByte(' ');
try self.writer.writeAll(choppee[0..split]);
location = try self.clear_line(indent);
location = try self.clearLine(indent);
choppee = choppee[split + 1 ..];
}
}
}
fn clear_line(self: *@This(), indent: usize) !usize {
fn clearLine(self: *@This(), indent: usize) !usize {
try self.writer.writeByte('\n');
try self.writer.writeByteNTimes(' ', indent);
return indent;
@@ -115,7 +129,7 @@ pub fn StructuredPrinter(comptime Writer: type) type {
}
pub fn HelpBuilder(comptime command: anytype) type {
const help_info = opt_info(command.generate());
const help_info = optInfo(command.generate());
return struct {
writebuffer: std.ArrayList(u8),
@@ -126,7 +140,7 @@ pub fn HelpBuilder(comptime command: anytype) type {
};
}
pub fn build_message(
pub fn buildMessage(
self: *@This(),
name: []const u8,
subcommands: parser.CommandMap,
@@ -136,26 +150,26 @@ pub fn HelpBuilder(comptime command: anytype) type {
"Usage: {s}{s}{s}{s}\n\n",
.{
name,
self.option_brief(),
try self.args_brief(),
self.subcommands_brief(subcommands),
self.optionBrief(),
try self.argsBrief(),
self.subcommandsBrief(subcommands),
},
);
var printer = StructuredPrinter(@TypeOf(writer)){ .writer = writer };
try printer.print_wrapped(command.description, 2);
try printer.printWrapped(command.description, 2);
try writer.writeAll("\n\n");
const arguments = try self.describe_arguments();
const options = try self.describe_options();
const env_vars = try self.describe_env();
const subcs = try self.describe_subcommands(subcommands);
const arguments = try self.describeArguments();
const options = try self.describeOptions();
const env_vars = try self.describeEnv();
const subcs = try self.describeSubcommands(subcommands);
const max_just = @max(arguments.just, @max(options.just, @max(env_vars.just, subcs.just)));
if (arguments.pairs.len > 0) {
try writer.writeAll("Arguments:\n");
for (arguments.pairs) |pair|
try printer.print_pair(pair, 2, max_just + 4);
try printer.printPair(pair, 2, max_just + 4);
try writer.writeAll("\n");
}
@@ -163,7 +177,7 @@ pub fn HelpBuilder(comptime command: anytype) type {
if (options.pairs.len > 0) {
try writer.writeAll("Options:\n");
for (options.pairs) |pair|
try printer.print_pair(pair, 2, max_just + 4);
try printer.printPair(pair, 2, max_just + 4);
try writer.writeAll("\n");
}
@@ -171,7 +185,7 @@ pub fn HelpBuilder(comptime command: anytype) type {
if (env_vars.pairs.len > 0) {
try writer.writeAll("Environment variables:\n");
for (env_vars.pairs) |pair|
try printer.print_pair(pair, 2, max_just + 4);
try printer.printPair(pair, 2, max_just + 4);
try writer.writeAll("\n");
}
@@ -179,7 +193,7 @@ pub fn HelpBuilder(comptime command: anytype) type {
if (subcs.pairs.len > 0) {
try writer.writeAll("Subcommands:\n");
for (subcs.pairs) |pair|
try printer.print_pair_brief(pair, 2, max_just + 4);
try printer.printPairBrief(pair, 2, max_just + 4);
try writer.writeAll("\n");
}
@@ -187,14 +201,14 @@ pub fn HelpBuilder(comptime command: anytype) type {
return self.writebuffer.toOwnedSlice();
}
fn option_brief(_: @This()) []const u8 {
fn optionBrief(_: @This()) []const u8 {
return if (comptime help_info.options.len > 0)
" [options...]"
else
"";
}
fn args_brief(self: @This()) ![]const u8 {
fn argsBrief(self: @This()) ![]const u8 {
var buf = std.ArrayList(u8).init(self.writebuffer.allocator);
defer buf.deinit();
@@ -214,21 +228,19 @@ pub fn HelpBuilder(comptime command: anytype) type {
return buf.toOwnedSlice();
}
fn subcommands_brief(_: @This(), subcommands: parser.CommandMap) []const u8 {
fn subcommandsBrief(_: @This(), subcommands: parser.CommandMap) []const u8 {
return if (subcommands.count() > 0)
" <subcommand ...>"
else
"";
}
fn describe_arguments(self: @This()) !OptionDescription {
fn describeArguments(self: @This()) !OptionDescription {
var pairs = std.ArrayList(AlignablePair).init(self.writebuffer.allocator);
defer pairs.deinit();
var just: usize = 0;
inline for (comptime help_info.arguments) |arg| {
if (comptime arg.description.len == 0) continue;
inline for (comptime help_info.arguments) |arg| {
const pair: AlignablePair = .{
.left = arg.name,
.right = arg.description,
@@ -243,13 +255,13 @@ pub fn HelpBuilder(comptime command: anytype) type {
};
}
fn describe_options(self: @This()) !OptionDescription {
fn describeOptions(self: @This()) !OptionDescription {
var pairs = std.ArrayList(AlignablePair).init(self.writebuffer.allocator);
defer pairs.deinit();
var just: usize = 0;
inline for (help_info.options) |opt| {
const pair = try self.describe_option(opt);
const pair = try self.describeOption(opt);
if (pair.left.len > just) just = pair.left.len;
try pairs.append(pair);
}
@@ -260,7 +272,7 @@ pub fn HelpBuilder(comptime command: anytype) type {
};
}
fn describe_option(self: @This(), comptime opt: OptHelp) !AlignablePair {
fn describeOption(self: @This(), comptime opt: OptHelp) !AlignablePair {
var buffer = std.ArrayList(u8).init(self.writebuffer.allocator);
defer buffer.deinit();
const writer = buffer.writer();
@@ -321,7 +333,7 @@ pub fn HelpBuilder(comptime command: anytype) type {
return .{ .left = left, .right = right };
}
fn describe_env(self: @This()) !OptionDescription {
fn describeEnv(self: @This()) !OptionDescription {
var pairs = std.ArrayList(AlignablePair).init(self.writebuffer.allocator);
defer pairs.deinit();
@@ -343,16 +355,15 @@ pub fn HelpBuilder(comptime command: anytype) type {
};
}
fn describe_subcommands(self: @This(), subcommands: parser.CommandMap) !OptionDescription {
fn describeSubcommands(self: @This(), subcommands: parser.CommandMap) !OptionDescription {
var pairs = std.ArrayList(AlignablePair).init(self.writebuffer.allocator);
defer pairs.deinit();
var just: usize = 0;
var iter = subcommands.keyIterator();
while (iter.next()) |key| {
for (subcommands.keys()) |key| {
const pair: AlignablePair = .{
.left = key.*,
.right = subcommands.get(key.*).?.describe(),
.left = key,
.right = subcommands.get(key).?.describe(),
};
if (pair.left.len > just) just = pair.left.len;
try pairs.append(pair);
@@ -402,7 +413,7 @@ const ArgHelp = struct {
required: bool = true,
};
pub fn opt_info(comptime command: anytype) CommandHelp {
pub fn optInfo(comptime command: anytype) CommandHelp {
// TODO: this could be runtime and it would be slightly simpler.
comptime {
var options: []const OptHelp = &[_]OptHelp{};
@@ -428,7 +439,7 @@ pub fn opt_info(comptime command: anytype) CommandHelp {
if (!std.mem.eql(u8, last_name, param.name)) {
if (last_name.len > 0) {
if (env_only(last_option)) {
if (envOnly(last_option)) {
env_vars = env_vars ++ &[_]EnvHelp{.{
.env_var = last_option.env_var,
.description = last_option.description,
@@ -471,13 +482,23 @@ pub fn opt_info(comptime command: anytype) CommandHelp {
// TODO: this is only acceptable for some types. It behaves poorly on
// enum-based choice types because it prints the whole type name rather
// than just the tag name. Roll our own eventually.
writer.print("{any}", .{def}) catch @compileError("whoah");
blk: {
switch (@typeInfo(@TypeOf(def))) {
.pointer => |info| if (info.size == .Slice and info.child == u8) {
writer.print("{s}", .{def}) catch @compileError("no");
break :blk;
},
else => {},
}
writer.print("{any}", .{def}) catch @compileError("whoah");
}
last_option.default = buf.buffer;
}
}
if (last_name.len > 0) {
if (env_only(last_option)) {
if (envOnly(last_option)) {
env_vars = env_vars ++ &[_]EnvHelp{.{
.env_var = last_option.env_var.?,
.description = last_option.description,
@@ -496,7 +517,7 @@ pub fn opt_info(comptime command: anytype) CommandHelp {
}
}
inline fn env_only(option: OptHelp) bool {
inline fn envOnly(option: OptHelp) bool {
return option.short_truthy == null and
option.long_truthy == null and
option.short_falsy == null and

View File

@@ -10,7 +10,7 @@ pub fn UpdateDefaults(comptime input: type, comptime defaults: anytype) type {
comptime {
const inputInfo = @typeInfo(input);
const fieldcount = switch (inputInfo) {
.Struct => |spec| blk: {
.@"struct" => |spec| blk: {
if (spec.decls.len > 0) {
@compileError("UpdateDefaults only works on structs " ++
"without decls due to limitations in @Type.");
@@ -21,7 +21,7 @@ pub fn UpdateDefaults(comptime input: type, comptime defaults: anytype) type {
};
var fields: [fieldcount]StructField = undefined;
for (inputInfo.Struct.fields, 0..) |field, idx| {
for (inputInfo.@"struct".fields, 0..) |field, idx| {
fields[idx] = .{
.name = field.name,
.field_type = field.field_type,
@@ -29,27 +29,27 @@ pub fn UpdateDefaults(comptime input: type, comptime defaults: anytype) type {
// setting null defaults work, and it converts comptime_int to
// the appropriate type, which is nice for ergonomics. Not sure
// if it introduces weird edge cases. Probably it's fine?
.default_value = if (@hasField(@TypeOf(defaults), field.name))
@ptrCast(?*const anyopaque, &@as(field.field_type, @field(defaults, field.name)))
.default_value_ptr = if (@hasField(@TypeOf(defaults), field.name))
@ptrCast(&@as(field.field_type, @field(defaults, field.name)))
else
field.default_value,
field.default_value_ptr,
.is_comptime = field.is_comptime,
.alignment = field.alignment,
};
}
return @Type(.{ .Struct = .{
.layout = inputInfo.Struct.layout,
.backing_integer = inputInfo.Struct.backing_integer,
return @Type(.{ .@"struct" = .{
.layout = inputInfo.@"struct".layout,
.backing_integer = inputInfo.@"struct".backing_integer,
.fields = &fields,
.decls = inputInfo.Struct.decls,
.is_tuple = inputInfo.Struct.is_tuple,
.decls = inputInfo.@"struct".decls,
.is_tuple = inputInfo.@"struct".is_tuple,
} });
}
}
pub fn enum_length(comptime T: type) comptime_int {
return @typeInfo(T).Enum.fields.len;
pub fn enumLength(comptime T: type) comptime_int {
return @typeInfo(T).@"enum".fields.len;
}
pub fn partition(comptime T: type, input: []const T, wedge: []const []const T) [3][]const T {
@@ -174,14 +174,47 @@ pub fn SliceIterator(comptime T: type) type {
};
}
pub fn copy_struct(comptime T: type, source: T, field_overrides: anytype) T {
pub fn MutatingZSplitter(comptime T: type) type {
return struct {
buffer: [:0]T,
delimiter: T,
index: ?usize = 0,
const Self = @This();
/// Returns a slice of the next field, or null if splitting is complete.
pub fn next(self: *Self) ?[:0]T {
const start = self.index orelse return null;
const end = if (std.mem.indexOfScalarPos(T, self.buffer, start, self.delimiter)) |delim_idx| blk: {
self.buffer[delim_idx] = 0;
self.index = delim_idx + 1;
break :blk delim_idx;
} else blk: {
self.index = null;
break :blk self.buffer.len;
};
return self.buffer[start..end :0];
}
/// Returns a slice of the remaining bytes. Does not affect iterator state.
pub fn rest(self: Self) [:0]T {
const end = self.buffer.len;
const start = self.index orelse end;
return self.buffer[start..end :0];
}
};
}
pub fn copyStruct(comptime T: type, source: T, field_overrides: anytype) T {
var result: T = undefined;
comptime inline for (@typeInfo(@TypeOf(field_overrides)).Struct.fields) |field| {
comptime for (@typeInfo(@TypeOf(field_overrides)).@"struct".fields) |field| {
if (!@hasField(T, field.name)) @compileError("override contains bad field" ++ field);
};
inline for (comptime @typeInfo(T).Struct.fields) |field| {
inline for (comptime @typeInfo(T).@"struct".fields) |field| {
if (comptime @hasField(@TypeOf(field_overrides), field.name))
@field(result, field.name) = @field(field_overrides, field.name)
else
@@ -203,7 +236,7 @@ pub const TupleBuilder = struct {
}
pub fn retrieve(comptime self: @This(), comptime index: comptime_int) self.types[index] {
return @ptrCast(*const self.types[index], @alignCast(@alignOf(*const self.types[index]), self.pointers[index])).*;
return @as(*const self.types[index], @ptrCast(@alignCast(self.pointers[index]))).*;
}
pub fn realTuple(comptime self: @This()) self.TupleType() {
@@ -221,19 +254,18 @@ pub const TupleBuilder = struct {
comptime {
var fields: [self.types.len]StructField = undefined;
for (self.types, 0..) |Type, idx| {
var num_buf: [128]u8 = undefined;
fields[idx] = .{
.name = std.fmt.bufPrint(&num_buf, "{d}", .{idx}) catch @compileError("failed to write field"),
.name = std.fmt.comptimePrint("{d}", .{idx}),
.type = Type,
.default_value = null,
.default_value_ptr = null,
// TODO: is this the right thing to do?
.is_comptime = false,
.alignment = if (@sizeOf(Type) > 0) @alignOf(Type) else 0,
};
}
return @Type(.{ .Struct = .{
.layout = .Auto,
return @Type(.{ .@"struct" = .{
.layout = .auto,
.fields = &fields,
.decls = &.{},
.is_tuple = true,

View File

@@ -1,9 +1,11 @@
const command = @import("./command.zig");
const converters = @import("./converters.zig");
const errors = @import("./errors.zig");
const help = @import("./help.zig");
const ncmeta = @import("./meta.zig");
const parameters = @import("./parameters.zig");
const parser = @import("./parser.zig");
pub const command = @import("./command.zig");
pub const converters = @import("./converters.zig");
pub const errors = @import("./errors.zig");
pub const help = @import("./help.zig");
pub const ncmeta = @import("./meta.zig");
pub const parameters = @import("./parameters.zig");
pub const parser = @import("./parser.zig");
pub const CommandBuilder = command.CommandBuilder;
pub const commandGroup = command.commandGroup;
pub const ParserInterface = parser.ParserInterface;

View File

@@ -18,7 +18,7 @@ pub const FlagBias = enum {
truthy,
unbiased,
pub fn string(comptime self: @This()) []const u8 {
pub fn string(comptime self: @This()) [:0]const u8 {
return switch (comptime self) {
.truthy => "true",
.falsy => "false",
@@ -34,6 +34,7 @@ pub const ParameterGenerics = struct {
/// useful for implementing complex conversion that produces output through its
/// side effects or by modifying the user context.
OutputType: type = void,
param_type: ParameterType,
value_count: ValueCount,
/// allow this named parameter to be passed multiple times.
@@ -44,29 +45,29 @@ pub const ParameterGenerics = struct {
// many-to-many and the many-to-one cases.
multi: bool,
pub fn fixed_value_count(comptime OutputType: type, comptime value_count: ValueCount) ValueCount {
pub fn fixedValueCount(comptime OutputType: type, comptime value_count: ValueCount) ValueCount {
return comptime if (value_count == .fixed)
switch (@typeInfo(OutputType)) {
.Struct => |info| .{ .fixed = info.fields.len },
.Array => |info| .{ .fixed = info.len },
.@"struct" => |info| .{ .fixed = info.fields.len },
.array => |info| .{ .fixed = info.len },
// TODO: this is a bit sloppy, but it can be refined later.
// .Pointer covers slices, which may be a many-to-many conversion.
.Pointer => value_count,
.pointer => value_count,
else => .{ .fixed = 1 },
}
else
value_count;
}
pub fn has_context(comptime self: @This()) bool {
pub fn hasContext(comptime self: @This()) bool {
return comptime self.UserContext != void;
}
pub fn has_output(comptime self: @This()) bool {
pub fn hasOutput(comptime self: @This()) bool {
return self.OutputType != void;
}
pub fn is_flag(comptime self: @This()) bool {
pub fn isFlag(comptime self: @This()) bool {
return comptime switch (self.value_count) {
.flag, .count => true,
.fixed => false,
@@ -109,12 +110,12 @@ pub const ParameterGenerics = struct {
pub fn IntermediateValue(comptime self: @This()) type {
return comptime switch (self.value_count) {
.flag => []const u8,
.flag => [:0]const u8,
.count => usize,
.fixed => |count| switch (count) {
0 => @compileError("bad fixed-zero parameter"),
1 => []const u8,
else => std.ArrayList([]const u8),
1 => [:0]const u8,
else => std.ArrayList([:0]const u8),
},
};
}
@@ -185,10 +186,10 @@ fn OptionType(comptime generics: ParameterGenerics) type {
return struct {
pub const G: ParameterGenerics = generics;
pub const param_type: ParameterType = generics.param_type;
pub const is_flag: bool = generics.is_flag();
pub const is_flag: bool = generics.isFlag();
pub const value_count: ValueCount = generics.value_count;
pub const multi: bool = generics.multi;
pub const has_output: bool = generics.has_output();
pub const has_output: bool = generics.hasOutput();
name: []const u8,
short_tag: ?[]const u8,
@@ -222,23 +223,43 @@ fn OptionType(comptime generics: ParameterGenerics) type {
/// want weird things to happen.
flag_bias: FlagBias,
pub fn describe(self: @This(), allocator: std.mem.Allocator) std.mem.Allocator.Error![]const u8 {
var buf = std.ArrayList(u8).init(allocator);
try buf.append('"');
try buf.appendSlice(self.name);
try buf.append('"');
if (self.short_tag != null or self.long_tag != null) {
try buf.appendSlice(" (");
if (self.short_tag) |short|
try buf.appendSlice(short);
if (self.short_tag != null and self.long_tag != null)
try buf.appendSlice(", ");
if (self.long_tag) |long|
try buf.appendSlice(long);
try buf.append(')');
}
return try buf.toOwnedSlice();
}
pub fn IntermediateValue(comptime _: @This()) type {
return generics.IntermediateValue();
}
};
}
fn check_short(comptime short_tag: ?[]const u8) void {
fn checkShort(comptime short_tag: ?[]const u8) void {
const short = comptime short_tag orelse return;
if (short.len != 2 or short[0] != '-') @compileError("bad short tag: " ++ short);
}
fn check_long(comptime long_tag: ?[]const u8) void {
fn checkLong(comptime long_tag: ?[]const u8) void {
const long = comptime long_tag orelse return;
if (long.len < 3 or long[0] != '-' or long[1] != '-') @compileError("bad long tag: " ++ long);
}
pub fn make_option(comptime generics: ParameterGenerics, comptime opts: OptionConfig(generics)) OptionType(generics) {
pub fn makeOption(comptime generics: ParameterGenerics, comptime opts: OptionConfig(generics)) OptionType(generics) {
if (opts.short_tag == null and opts.long_tag == null and opts.env_var == null) {
@compileError(
"option " ++
@@ -247,8 +268,8 @@ pub fn make_option(comptime generics: ParameterGenerics, comptime opts: OptionCo
);
}
check_short(opts.short_tag);
check_long(opts.long_tag);
checkShort(opts.short_tag);
checkLong(opts.long_tag);
// perform the logic to create the default converter here? Could be done
// when creating the OptionConfig instead. Need to do it here because there
@@ -257,7 +278,7 @@ pub fn make_option(comptime generics: ParameterGenerics, comptime opts: OptionCo
// whereas the OptionType is an instance of an object that has been
// validated.
const converter = opts.converter orelse
(converters.default_converter(generics) orelse @compileError(
(converters.DefaultConverter(generics) orelse @compileError(
"no converter provided for " ++
opts.name ++
"and no default exists",
@@ -284,7 +305,7 @@ pub fn make_option(comptime generics: ParameterGenerics, comptime opts: OptionCo
};
}
pub fn make_argument(
pub fn makeArgument(
comptime generics: ParameterGenerics,
comptime opts: OptionConfig(generics),
) OptionType(generics) {
@@ -298,7 +319,7 @@ pub fn make_argument(
}
const converter = opts.converter orelse
(converters.default_converter(generics) orelse @compileError(
(converters.DefaultConverter(generics) orelse @compileError(
"no converter provided for " ++
opts.name ++
"and no default exists",

View File

@@ -10,90 +10,77 @@ const NoclipError = errors.NoclipError;
pub const ParserInterface = struct {
const Vtable = struct {
execute: *const fn (parser: *anyopaque, context: *anyopaque) anyerror!void,
parse: *const fn (parser: *anyopaque, context: *anyopaque, name: []const u8, args: [][:0]u8, env: std.process.EnvMap) anyerror!void,
finish: *const fn (parser: *anyopaque, context: *anyopaque) anyerror!void,
parse: *const fn (parser: *anyopaque, context: *anyopaque, name: []const u8, args: [][:0]u8, env: std.process.EnvMap) anyerror!?ParseResult,
finish: *const fn (parser: *anyopaque, context: *anyopaque) anyerror!?ParserInterface,
getParseError: *const fn (parser: *anyopaque) []const u8,
addSubcommand: *const fn (parser: *anyopaque, name: []const u8, subcommand: ParserInterface) std.mem.Allocator.Error!void,
getSubcommand: *const fn (parser: *anyopaque, name: []const u8) ?ParserInterface,
describe: *const fn () []const u8,
deinit: *const fn (parser: *anyopaque) void,
deinitTree: *const fn (parser: *anyopaque) void,
};
parser: *anyopaque,
context: *anyopaque,
methods: *const Vtable,
fn create(comptime ParserType: type, parser: *anyopaque, context: *anyopaque) @This() {
return .{
.parser = parser,
.context = context,
.methods = &.{
.execute = ParserType._wrapExecute,
.parse = ParserType._wrapParse,
.finish = ParserType._wrapFinish,
.getParseError = ParserType._wrapGetParseError,
.addSubcommand = ParserType._wrapAddSubcommand,
.getSubcommand = ParserType._wrapGetSubcommand,
.describe = ParserType._wrapDescribe,
.deinit = ParserType._wrapDeinit,
.deinitTree = ParserType._wrapDeinitTree,
},
};
}
pub fn execute(self: @This()) anyerror!void {
return try self.methods.execute(self.parser, self.context);
}
pub fn parse(self: @This(), name: []const u8, args: [][:0]u8, env: std.process.EnvMap) anyerror!void {
pub fn parse(self: @This(), name: []const u8, args: [][:0]u8, env: std.process.EnvMap) anyerror!?ParseResult {
return try self.methods.parse(self.parser, self.context, name, args, env);
}
pub fn finish(self: @This()) anyerror!void {
pub fn finish(self: @This()) anyerror!?ParserInterface {
return try self.methods.finish(self.parser, self.context);
}
pub fn getParseError(self: @This()) []const u8 {
return self.methods.getParseError(self.parser);
}
pub fn addSubcommand(self: @This(), name: []const u8, subcommand: ParserInterface) std.mem.Allocator.Error!void {
return try self.methods.addSubcommand(self.parser, name, subcommand);
}
pub fn getSubcommand(self: @This(), name: []const u8) ?ParserInterface {
return self.methods.getSubcommand(self.parser, name);
}
pub fn describe(self: @This()) []const u8 {
return self.methods.describe();
}
pub fn deinit(self: @This()) void {
self.methods.deinit(self.parser);
}
pub fn deinitTree(self: @This()) void {
self.methods.deinitTree(self.parser);
}
};
fn InterfaceGen(comptime ParserType: type, comptime UserContext: type) type {
const CtxInfo = @typeInfo(UserContext);
return if (CtxInfo == .Void) struct {
pub fn interface(self: *ParserType) ParserInterface {
return .{
.parser = self,
.context = @constCast(&void{}),
.methods = &.{
.execute = ParserType.wrap_execute,
.parse = ParserType.wrap_parse,
.finish = ParserType.wrap_finish,
.describe = ParserType.describe,
},
};
}
fn cast_context(_: *anyopaque) void {
return void{};
}
} else if (CtxInfo == .Pointer and CtxInfo.Pointer.size != .Slice) struct {
pub fn interface(self: *ParserType, context: UserContext) ParserInterface {
return .{
.parser = self,
.context = @ptrCast(*anyopaque, @constCast(context)),
.methods = &.{
.execute = ParserType.wrap_execute,
.parse = ParserType.wrap_parse,
.finish = ParserType.wrap_finish,
.describe = ParserType.describe,
},
};
}
fn cast_context(ctx: *anyopaque) UserContext {
return @ptrCast(UserContext, @alignCast(std.meta.alignment(UserContext), ctx));
}
} else struct {
pub fn interface(self: *ParserType, context: *const UserContext) ParserInterface {
return .{
.parser = self,
.context = @ptrCast(*anyopaque, @constCast(context)),
.methods = &.{
.execute = ParserType.wrap_execute,
.parse = ParserType.wrap_parse,
.finish = ParserType.wrap_finish,
.describe = ParserType.describe,
},
};
}
fn cast_context(ctx: *anyopaque) UserContext {
return @ptrCast(*const UserContext, @alignCast(@alignOf(UserContext), ctx)).*;
}
};
}
pub const CommandMap = std.hash_map.StringHashMap(ParserInterface);
pub const CommandMap = std.StringArrayHashMap(ParserInterface);
const ParseResult = struct { name: []const u8, args: [][:0]u8, parser: ParserInterface };
// the parser is generated by the bind method of the CommandBuilder, so we can
// be extremely type-sloppy here, which simplifies the signature.
@@ -104,99 +91,141 @@ pub fn Parser(comptime command: anytype, comptime callback: anytype) type {
const Output = command.Output();
return struct {
const command_description = command.description;
intermediate: Intermediate = .{},
output: Output = undefined,
consumed_args: u32 = 0,
progname: ?[]const u8 = null,
has_global_tags: bool = false,
arena: *std.heap.ArenaAllocator,
allocator: std.mem.Allocator,
subcommands: CommandMap,
subcommand: ?ParserInterface = null,
error_message: std.ArrayListUnmanaged(u8) = .{},
help_builder: help.HelpBuilder(command),
pub fn add_subcommand(self: *@This(), verb: []const u8, parser: ParserInterface) !void {
try self.subcommands.put(verb, parser);
}
// This is a slightly annoying hack to work around the fact that there's no way to
// provide a method signature conditionally.
const Interface = InterfaceGen(@This(), UserContext);
pub usingnamespace Interface;
inline fn cast_interface_parser(parser: *anyopaque) *@This() {
return @ptrCast(*@This(), @alignCast(@alignOf(@This()), parser));
}
fn wrap_execute(parser: *anyopaque, ctx: *anyopaque) anyerror!void {
const self = cast_interface_parser(parser);
// this is a slightly annoying hack to work around the problem that void has
// 0 alignment, which alignCast chokes on.
const context = Interface.cast_context(ctx);
return try self.execute(context);
}
fn wrap_parse(parser: *anyopaque, ctx: *anyopaque, name: []const u8, args: [][:0]u8, env: std.process.EnvMap) anyerror!void {
const self = cast_interface_parser(parser);
const context = Interface.cast_context(ctx);
return try self.subparse(context, name, args, env);
}
fn wrap_finish(parser: *anyopaque, ctx: *anyopaque) anyerror!void {
const self = cast_interface_parser(parser);
const context = Interface.cast_context(ctx);
return try self.finish(context);
}
fn describe() []const u8 {
return command.description;
}
pub fn subparse(self: *@This(), context: UserContext, name: []const u8, args: [][:0]u8, env: std.process.EnvMap) anyerror!void {
const sliceto = try self.parse(name, args);
try self.read_environment(env);
try self.convert_eager(context);
if (self.subcommand) |verb| {
const verbname = try std.mem.join(
self.allocator,
" ",
&[_][]const u8{ name, args[sliceto - 1] },
);
try verb.parse(verbname, args[sliceto..], env);
} else if (self.subcommands.count() > 0 and command.subcommand_required) {
const stderr = std.io.getStdErr().writer();
try stderr.writeAll("A subcommand is required.\n\n");
self.print_help(name);
}
}
pub fn finish(self: *@This(), context: UserContext) anyerror!void {
try self.convert(context);
try callback(context, self.output);
if (self.subcommand) |verb| try verb.finish();
}
// This is a slightly annoying hack to work around the fact that there's no way
// to provide a method signature conditionally.
pub usingnamespace InterfaceGen(@This(), @TypeOf(command).ICC);
// This is attached to the struct this way because these are all "private"
// methods that exist exclusively to cast the type-erased interface object back
// into something usable. Their implementations aren't meaningful and just
// cognitively clutter this struct.
pub usingnamespace InterfaceWrappers(@This());
pub fn execute(self: *@This(), context: UserContext) anyerror!void {
const args = try std.process.argsAlloc(self.allocator);
defer std.process.argsFree(self.allocator, args);
var env = try std.process.getEnvMap(self.allocator);
defer env.deinit();
const args = std.process.argsAlloc(self.allocator) catch |err| {
try self.error_message.appendSlice(
self.allocator,
"Failed to allocate process arg vector\n",
);
return err;
};
const env = std.process.getEnvMap(self.allocator) catch |err| {
try self.error_message.appendSlice(
self.allocator,
"Failed to allocate process environment variable map\n",
);
return err;
};
if (args.len < 1) return ParseError.EmptyArgs;
if (args.len < 1) {
try self.error_message.appendSlice(
self.allocator,
"The argument list for the base CLI entry point is empty.\n",
);
return ParseError.EmptyArgs;
}
self.progname = std.fs.path.basename(args[0]);
try self.subparse(context, self.progname.?, args[1..], env);
try self.finish(context);
{
var subc = try self.subparse(context, self.progname.?, args[1..], env);
while (subc) |next| {
subc = next.parser.parse(next.name, next.args, env) catch |err| {
try self.error_message.appendSlice(self.allocator, next.parser.getParseError());
return err;
};
}
}
{
var subc = try self.finish(context);
while (subc) |next| {
subc = next.finish() catch |err| {
try self.error_message.appendSlice(self.allocator, next.getParseError());
return err;
};
}
}
}
fn print_value(self: @This(), value: anytype, comptime indent: []const u8) void {
pub fn subparse(
self: *@This(),
context: UserContext,
name: []const u8,
args: [][:0]u8,
env: std.process.EnvMap,
) anyerror!?ParseResult {
const sliceto = try self.parse(name, args);
try self.readEnvironment(env);
try self.convertEager(context);
if (self.subcommand) |subcommand| {
const grafted_name = try std.mem.join(
self.allocator,
" ",
&.{ name, args[sliceto - 1] },
);
return .{ .name = grafted_name, .args = args[sliceto..], .parser = subcommand };
} else if (self.subcommands.count() > 0 and command.subcommand_required) {
const stderr = std.io.getStdErr().writer();
try stderr.print("'{s}' requires a subcommand.\n\n", .{name});
self.printHelp(name);
}
return null;
}
pub fn finish(self: *@This(), context: UserContext) anyerror!?ParserInterface {
try self.convert(context);
try callback(context, self.output);
return self.subcommand;
}
pub fn getParseError(self: @This()) []const u8 {
return if (self.error_message.items.len == 0)
"An unexpected error occurred.\n"
else
self.error_message.items;
}
pub fn deinit(self: @This()) void {
self.arena.deinit();
self.arena.child_allocator.destroy(self.arena);
}
pub fn deinitTree(self: @This()) void {
for (self.subcommands.values()) |subcommand| {
subcommand.deinitTree();
}
self.deinit();
}
pub fn addSubcommand(self: *@This(), name: []const u8, parser: ParserInterface) !void {
try self.subcommands.put(name, parser);
}
pub fn getSubcommand(self: @This(), name: []const u8) ?ParserInterface {
return self.subcommands.get(name);
}
fn printValue(self: @This(), value: anytype, comptime indent: []const u8) void {
if (comptime @hasField(@TypeOf(value), "items")) {
std.debug.print("{s}[\n", .{indent});
for (value.items) |item| {
self.print_value(item, indent ++ " ");
self.printValue(item, indent ++ " ");
}
std.debug.print("{s}]\n", .{indent});
} else {
@@ -250,11 +279,11 @@ pub fn Parser(comptime command: anytype, comptime callback: anytype) type {
if (!forced_ordinal and arg.len > 1 and arg[0] == '-') {
if (arg.len > 2 and arg[1] == '-') {
try self.parse_long_tag(name, arg, &argit);
try self.parseLongTag(name, arg, &argit);
continue :argloop;
} else if (arg.len > 1) {
for (arg[1..], 1..) |short, idx| {
try self.parse_short_tag(name, short, arg.len - idx - 1, &argit);
try self.parseShortTag(name, short, arg.len - idx - 1, &argit);
}
continue :argloop;
}
@@ -264,8 +293,8 @@ pub fn Parser(comptime command: anytype, comptime callback: anytype) type {
forced_ordinal = true;
}
if (try self.parse_ordinals(arg, &argit)) |verb| {
self.subcommand = verb;
if (try self.parseOrdinals(arg, &argit)) |subcommand| {
self.subcommand = subcommand;
// TODO: return slice of remaining or offset index
return argit.index;
}
@@ -274,15 +303,15 @@ pub fn Parser(comptime command: anytype, comptime callback: anytype) type {
return 0;
}
fn parse_long_tag(
fn parseLongTag(
self: *@This(),
name: []const u8,
arg: []const u8,
arg: [:0]u8,
argit: *ncmeta.SliceIterator([][:0]u8),
) ParseError!void {
if (comptime command.help_flag.long_tag) |long|
if (std.mem.eql(u8, arg, long))
self.print_help(name);
self.printHelp(name);
inline for (comptime parameters) |param| {
const PType = @TypeOf(param);
@@ -292,19 +321,23 @@ pub fn Parser(comptime command: anytype, comptime callback: anytype) type {
if (std.mem.startsWith(u8, arg, tag)) match: {
if (arg.len == tag.len) {
try self.apply_param_values(param, argit, false);
try self.applyParamValues(param, argit, false);
} else if (arg[tag.len] == '=') {
try self.apply_fused_values(param, arg[tag.len + 1 ..]);
try self.applyFusedValues(param, arg[tag.len + 1 ..]);
} else break :match;
return;
}
}
try self.error_message.writer(self.allocator).print(
"Could not parse command line: unknown option \"{s}\"\n",
.{arg},
);
return ParseError.UnknownLongTagParameter;
}
fn parse_short_tag(
fn parseShortTag(
self: *@This(),
name: []const u8,
arg: u8,
@@ -313,7 +346,7 @@ pub fn Parser(comptime command: anytype, comptime callback: anytype) type {
) ParseError!void {
if (comptime command.help_flag.short_tag) |short|
if (arg == short[1])
self.print_help(name);
self.printHelp(name);
inline for (comptime parameters) |param| {
const PType = @TypeOf(param);
@@ -323,20 +356,29 @@ pub fn Parser(comptime command: anytype, comptime callback: anytype) type {
if (arg == tag[1]) {
if (comptime !PType.is_flag)
if (remaining > 0)
if (remaining > 0) {
try self.error_message.writer(self.allocator).print(
"Could not parse command line: \"-{c}\" is fused to another flag, but it requires a value\n",
.{arg},
);
return ParseError.FusedShortTagValueMissing;
};
try self.apply_param_values(param, argit, false);
try self.applyParamValues(param, argit, false);
return;
}
}
try self.error_message.writer(self.allocator).print(
"Could not parse command line: unknown option \"-{c}\"\n",
.{arg},
);
return ParseError.UnknownShortTagParameter;
}
fn parse_ordinals(
fn parseOrdinals(
self: *@This(),
arg: []const u8,
arg: [:0]u8,
argit: *ncmeta.SliceIterator([][:0]u8),
) ParseError!?ParserInterface {
comptime var arg_index: u32 = 0;
@@ -346,9 +388,9 @@ pub fn Parser(comptime command: anytype, comptime callback: anytype) type {
if (self.consumed_args == arg_index) {
argit.rewind();
if (comptime @TypeOf(param).G.multi) {
while (argit.peek()) |_| try self.apply_param_values(param, argit, false);
while (argit.peek()) |_| try self.applyParamValues(param, argit, false);
} else {
try self.apply_param_values(param, argit, false);
try self.applyParamValues(param, argit, false);
}
self.consumed_args += 1;
return null;
@@ -357,10 +399,23 @@ pub fn Parser(comptime command: anytype, comptime callback: anytype) type {
arg_index += 1;
}
return self.subcommands.get(arg) orelse ParseError.ExtraValue;
return self.subcommands.get(arg) orelse {
const writer = self.error_message.writer(self.allocator);
if (self.subcommands.count() > 0)
try writer.print(
"Could not parse command line: unknown subcommand \"{s}\"\n",
.{arg},
)
else
try writer.print(
"Could not parse command line: unexpected extra argument \"{s}\"\n",
.{arg},
);
return ParseError.ExtraValue;
};
}
fn push_intermediate_value(
fn pushIntermediateValue(
self: *@This(),
comptime param: anytype,
// @TypeOf(param).G.IntermediateValue() should work but appears to trigger a
@@ -372,7 +427,7 @@ pub fn Parser(comptime command: anytype, comptime callback: anytype) type {
if (@field(self.intermediate, param.name) == null) {
@field(self.intermediate, param.name) = gen.IntermediateType().init(self.allocator);
}
@field(self.intermediate, param.name).?.append(value) catch return ParseError.UnexpectedFailure;
try @field(self.intermediate, param.name).?.append(value);
} else if (comptime @TypeOf(param).G.nonscalar()) {
if (@field(self.intermediate, param.name)) |list| list.deinit();
@field(self.intermediate, param.name) = value;
@@ -381,62 +436,100 @@ pub fn Parser(comptime command: anytype, comptime callback: anytype) type {
}
}
fn apply_param_values(
fn applyParamValues(
self: *@This(),
comptime param: anytype,
argit: anytype,
bounded: bool,
) ParseError!void {
switch (comptime @TypeOf(param).G.value_count) {
.flag => try self.push_intermediate_value(param, comptime param.flag_bias.string()),
.flag => try self.pushIntermediateValue(param, comptime param.flag_bias.string()),
.count => @field(self.intermediate, param.name) += 1,
.fixed => |count| switch (count) {
0 => return ParseError.ExtraValue,
1 => try self.push_intermediate_value(param, argit.next() orelse return ParseError.MissingValue),
0 => {
const writer = self.error_message.writer(self.allocator);
const desc = try param.describe(self.allocator);
defer self.allocator.free(desc);
try writer.print(
"Could not parse command line: {s} takes no value.\n",
.{desc},
);
return ParseError.ExtraValue;
},
1 => try self.pushIntermediateValue(param, argit.next() orelse {
const writer = self.error_message.writer(self.allocator);
const desc = try param.describe(self.allocator);
defer self.allocator.free(desc);
try writer.print(
"Could not parse command line: {s} requires a value.\n",
.{desc},
);
return ParseError.MissingValue;
}),
else => |total| {
var list = std.ArrayList([]const u8).initCapacity(self.allocator, total) catch
return ParseError.UnexpectedFailure;
var list = try std.ArrayList([:0]const u8).initCapacity(self.allocator, total);
var consumed: u32 = 0;
while (consumed < total) : (consumed += 1) {
const next = argit.next() orelse return ParseError.MissingValue;
list.append(next) catch return ParseError.UnexpectedFailure;
}
if (bounded and argit.next() != null) return ParseError.ExtraValue;
const next = argit.next() orelse {
const writer = self.error_message.writer(self.allocator);
const desc = try param.describe(self.allocator);
defer self.allocator.free(desc);
try writer.print(
"Could not parse command line: {s} is missing one or more values (need {d}, got {d}).\n",
.{ desc, total, consumed },
);
return ParseError.MissingValue;
};
try self.push_intermediate_value(param, list);
list.appendAssumeCapacity(next);
}
if (bounded and argit.next() != null) {
const writer = self.error_message.writer(self.allocator);
const desc = try param.describe(self.allocator);
defer self.allocator.free(desc);
try writer.print(
"Could not parse command line: {s} has too many values (need {d}).\n",
.{ desc, total },
);
return ParseError.ExtraValue;
}
try self.pushIntermediateValue(param, list);
},
},
}
}
fn apply_fused_values(
fn applyFusedValues(
self: *@This(),
comptime param: anytype,
value: []const u8,
value: [:0]u8,
) ParseError!void {
var iter = std.mem.split(u8, value, ",");
return try self.apply_param_values(param, &iter, true);
var iter = ncmeta.MutatingZSplitter(u8){ .buffer = value, .delimiter = ',' };
return try self.applyParamValues(param, &iter, true);
}
fn read_environment(self: *@This(), env: std.process.EnvMap) !void {
fn readEnvironment(self: *@This(), env: std.process.EnvMap) !void {
inline for (comptime parameters) |param| {
if (comptime param.env_var) |env_var| blk: {
if (@field(self.intermediate, param.name) != null) break :blk;
const val = env.get(env_var) orelse break :blk;
const val = try self.allocator.dupeZ(u8, env.get(env_var) orelse break :blk);
if (comptime @TypeOf(param).G.value_count == .flag) {
try self.push_intermediate_value(param, val);
try self.pushIntermediateValue(param, val);
} else {
try self.apply_fused_values(param, val);
try self.applyFusedValues(param, val);
}
}
}
}
fn convert_eager(self: *@This(), context: UserContext) NoclipError!void {
fn convertEager(self: *@This(), context: UserContext) NoclipError!void {
inline for (comptime parameters) |param| {
if (comptime param.eager) {
try self.convert_param(param, context);
try self.convertParam(param, context);
}
}
}
@@ -444,31 +537,39 @@ pub fn Parser(comptime command: anytype, comptime callback: anytype) type {
fn convert(self: *@This(), context: UserContext) NoclipError!void {
inline for (comptime parameters) |param| {
if (comptime !param.eager) {
try self.convert_param(param, context);
try self.convertParam(param, context);
}
}
}
fn convert_param(self: *@This(), comptime param: anytype, context: UserContext) NoclipError!void {
fn convertParam(self: *@This(), comptime param: anytype, context: UserContext) NoclipError!void {
if (@field(self.intermediate, param.name)) |intermediate| {
var buffer = std.ArrayList(u8).init(self.allocator);
const writer = buffer.writer();
if (comptime @TypeOf(param).has_output) {
@field(self.output, param.name) = param.converter(context, intermediate, writer) catch |err| {
const stderr = std.io.getStdErr().writer();
stderr.print("Error parsing option \"{s}\": {s}\n", .{ param.name, buffer.items }) catch {};
const err_writer = self.error_message.writer(self.allocator);
const desc = try param.describe(self.allocator);
defer self.allocator.free(desc);
try err_writer.print("Error parsing option {s}: {s}\n", .{ desc, buffer.items });
return err;
};
} else {
param.converter(context, intermediate, writer) catch |err| {
const stderr = std.io.getStdErr().writer();
stderr.print("Error parsing option \"{s}\": {s}\n", .{ param.name, buffer.items }) catch {};
const err_writer = self.error_message.writer(self.allocator);
const desc = try param.describe(self.allocator);
defer self.allocator.free(desc);
try err_writer.print("Error parsing option {s}: {s}\n", .{ desc, buffer.items });
return err;
};
}
} else {
if (comptime param.required) {
const err_writer = self.error_message.writer(self.allocator);
const desc = try param.describe(self.allocator);
defer self.allocator.free(desc);
try err_writer.print("Could not parse command line: required parameter {s} is missing\n", .{desc});
return ParseError.RequiredParameterMissing;
} else if (comptime @TypeOf(param).has_output) {
if (comptime param.default) |def| {
@@ -484,14 +585,109 @@ pub fn Parser(comptime command: anytype, comptime callback: anytype) type {
}
}
fn print_help(self: *@This(), name: []const u8) noreturn {
fn printHelp(self: *@This(), name: []const u8) noreturn {
defer std.process.exit(0);
const stderr = std.io.getStdErr().writer();
if (self.help_builder.build_message(name, self.subcommands)) |message|
if (self.help_builder.buildMessage(name, self.subcommands)) |message|
stderr.writeAll(message) catch return
else |_|
stderr.writeAll("There was a problem generating the help.") catch return;
}
};
}
fn InterfaceWrappers(comptime ParserType: type) type {
return struct {
inline fn castInterfaceParser(parser: *anyopaque) *ParserType {
return @ptrCast(@alignCast(parser));
}
fn _wrapExecute(parser: *anyopaque, ctx: *anyopaque) anyerror!void {
const self = castInterfaceParser(parser);
const context = self.castContext(ctx);
return try self.execute(context);
}
fn _wrapParse(
parser: *anyopaque,
ctx: *anyopaque,
name: []const u8,
args: [][:0]u8,
env: std.process.EnvMap,
) anyerror!?ParseResult {
const self = castInterfaceParser(parser);
const context = self.castContext(ctx);
return try self.subparse(context, name, args, env);
}
fn _wrapFinish(parser: *anyopaque, ctx: *anyopaque) anyerror!?ParserInterface {
const self = castInterfaceParser(parser);
const context = self.castContext(ctx);
return try self.finish(context);
}
fn _wrapGetParseError(parser: *anyopaque) []const u8 {
const self = castInterfaceParser(parser);
return self.getParseError();
}
fn _wrapAddSubcommand(parser: *anyopaque, name: []const u8, subcommand: ParserInterface) !void {
const self = castInterfaceParser(parser);
return self.addSubcommand(name, subcommand);
}
fn _wrapGetSubcommand(parser: *anyopaque, name: []const u8) ?ParserInterface {
const self = castInterfaceParser(parser);
return self.getSubcommand(name);
}
fn _wrapDeinit(parser: *anyopaque) void {
const self = castInterfaceParser(parser);
self.deinit();
}
fn _wrapDeinitTree(parser: *anyopaque) void {
const self = castInterfaceParser(parser);
self.deinitTree();
}
fn _wrapDescribe() []const u8 {
return ParserType.command_description;
}
};
}
// TODO: figure out a better way of consolidating this logic with that in command.zig?
fn InterfaceGen(comptime ParserType: type, comptime ICC: anytype) type {
return switch (ICC) {
.empty => struct {
pub fn interface(self: *ParserType) ParserInterface {
return ParserInterface.create(ParserType, self, @constCast(&void{}));
}
fn castContext(_: ParserType, _: *anyopaque) void {
return void{};
}
},
.pointer => struct {
pub fn interface(self: *ParserType, context: ICC.InputType().?) ParserInterface {
return ParserInterface.create(ParserType, self, @constCast(context));
}
fn castContext(_: ParserType, ctx: *anyopaque) ICC.OutputType() {
return @ptrCast(@alignCast(ctx));
}
},
.value => struct {
pub fn interface(self: *ParserType, context: ICC.InputType().?) ParserInterface {
return ParserInterface.create(ParserType, self, @ptrCast(@constCast(context)));
}
fn castContext(_: ParserType, ctx: *anyopaque) ICC.OutputType() {
return @as(ICC.InputType().?, @ptrCast(@alignCast(ctx))).*;
}
},
};
}