2 Commits

Author SHA1 Message Date
419d8994ba demo: update to use interfaces directly
This adds a group and saves some lines.
2023-09-10 14:54:41 -07:00
a8652f71c4 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 14:52:49 -07:00
12 changed files with 108 additions and 305 deletions

View File

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

View File

@@ -1,13 +0,0 @@
.{
.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,22 +8,9 @@ const Choice = enum { first, second };
const cli = cmd: { const cli = cmd: {
var cmd = CommandBuilder(*u32){ var cmd = CommandBuilder(*u32){
.description = .description =
\\The definitive noclip demonstration utility. \\The definitive noclip demonstration utility
\\ \\
\\This command demonstrates the functionality of the noclip library. cool! \\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.addOption(.{ .OutputType = struct { u8, u8 } }, .{ cmd.addOption(.{ .OutputType = struct { u8, u8 } }, .{
@@ -135,20 +122,7 @@ pub fn main() !u8 {
try base.addSubcommand("main", try cli.createInterface(allocator, cliHandler, &context)); try base.addSubcommand("main", try cli.createInterface(allocator, cliHandler, &context));
try base.addSubcommand("other", try subcommand.createInterface(allocator, subHandler, &sc)); try base.addSubcommand("other", try subcommand.createInterface(allocator, subHandler, &sc));
const group = try noclip.commandGroup(allocator, .{ .description = "final level of a deeply nested subcommand" }); try base.execute();
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; return 0;
} }

View File

@@ -22,7 +22,7 @@ ____
== Hello == Hello
Requires Zig `0.13.x`. May work with `0.12.x`. Requires Zig `0.11.x`.
=== Features === Features

View File

@@ -118,8 +118,8 @@ pub const InterfaceContextCategory = union(enum) {
pub fn fromType(comptime ContextType: type) InterfaceContextCategory { pub fn fromType(comptime ContextType: type) InterfaceContextCategory {
return switch (@typeInfo(ContextType)) { return switch (@typeInfo(ContextType)) {
.void => .empty, .Void => .empty,
.pointer => |info| if (info.size == .slice) .{ .value = ContextType } else .{ .pointer = ContextType }, .Pointer => |info| if (info.size == .Slice) .{ .value = ContextType } else .{ .pointer = ContextType },
// technically, i0, u0, and struct{} should be treated as empty, probably // technically, i0, u0, and struct{} should be treated as empty, probably
else => .{ .value = ContextType }, else => .{ .value = ContextType },
}; };
@@ -167,13 +167,12 @@ pub fn CommandBuilder(comptime UserContext: type) type {
return Parser(self, callback){ return Parser(self, callback){
.arena = arena, .arena = arena,
.allocator = arena_alloc, .allocator = arena_alloc,
.subcommands = parser.CommandMap.init(arena_alloc), .subcommands = std.hash_map.StringHashMap(ParserInterface).init(arena_alloc),
.help_builder = help.HelpBuilder(self).init(arena_alloc), .help_builder = help.HelpBuilder(self).init(arena_alloc),
}; };
} }
pub const ifc = InterfaceCreator(@This()); pub usingnamespace InterfaceCreator(@This());
pub const createInterface = ifc.createInterface;
fn _createInterfaceImpl( fn _createInterfaceImpl(
comptime self: @This(), comptime self: @This(),
@@ -189,7 +188,7 @@ pub fn CommandBuilder(comptime UserContext: type) type {
this_parser.* = .{ this_parser.* = .{
.arena = arena, .arena = arena,
.allocator = arena_alloc, .allocator = arena_alloc,
.subcommands = parser.CommandMap.init(arena_alloc), .subcommands = std.hash_map.StringHashMap(ParserInterface).init(arena_alloc),
.help_builder = help.HelpBuilder(self).init(arena_alloc), .help_builder = help.HelpBuilder(self).init(arena_alloc),
}; };
@@ -373,29 +372,27 @@ pub fn CommandBuilder(comptime UserContext: type) type {
// global tags and env_vars would conflict, which is less common. // global tags and env_vars would conflict, which is less common.
if (param.short_tag) |short| if (param.short_tag) |short|
tag_fields = tag_fields ++ &[_]StructField{.{ tag_fields = tag_fields ++ &[_]StructField{.{
// this goofy construct coerces the comptime []const u8 to .name = short,
// [:0]const u8.
.name = short ++ "",
.type = void, .type = void,
.default_value_ptr = null, .default_value = null,
.is_comptime = false, .is_comptime = false,
.alignment = 0, .alignment = 0,
}}; }};
if (param.long_tag) |long| if (param.long_tag) |long|
tag_fields = tag_fields ++ &[_]StructField{.{ tag_fields = tag_fields ++ &[_]StructField{.{
.name = long ++ "", .name = long,
.type = void, .type = void,
.default_value_ptr = null, .default_value = null,
.is_comptime = false, .is_comptime = false,
.alignment = 0, .alignment = 0,
}}; }};
if (param.env_var) |env_var| if (param.env_var) |env_var|
env_var_fields = env_var_fields ++ &[_]StructField{.{ env_var_fields = env_var_fields ++ &[_]StructField{.{
.name = env_var ++ "", .name = env_var,
.type = void, .type = void,
.default_value_ptr = null, .default_value = null,
.is_comptime = false, .is_comptime = false,
.alignment = 0, .alignment = 0,
}}; }};
@@ -438,30 +435,30 @@ pub fn CommandBuilder(comptime UserContext: type) type {
const default = if (param.default) |def| &@as(FieldType, def) else @as(?*const anyopaque, null); const default = if (param.default) |def| &@as(FieldType, def) else @as(?*const anyopaque, null);
fields = fields ++ &[_]StructField{.{ fields = fields ++ &[_]StructField{.{
.name = param.name ++ "", .name = param.name,
.type = FieldType, .type = FieldType,
.default_value_ptr = @ptrCast(default), .default_value = @ptrCast(default),
.is_comptime = false, .is_comptime = false,
.alignment = @alignOf(FieldType), .alignment = @alignOf(FieldType),
}}; }};
} }
_ = @Type(.{ .@"struct" = .{ _ = @Type(.{ .Struct = .{
.layout = .auto, .layout = .Auto,
.fields = tag_fields, .fields = tag_fields,
.decls = &.{}, .decls = &.{},
.is_tuple = false, .is_tuple = false,
} }); } });
_ = @Type(.{ .@"struct" = .{ _ = @Type(.{ .Struct = .{
.layout = .auto, .layout = .Auto,
.fields = env_var_fields, .fields = env_var_fields,
.decls = &.{}, .decls = &.{},
.is_tuple = false, .is_tuple = false,
} }); } });
return @Type(.{ .@"struct" = .{ return @Type(.{ .Struct = .{
.layout = .auto, .layout = .Auto,
.fields = fields, .fields = fields,
.decls = &.{}, .decls = &.{},
.is_tuple = false, .is_tuple = false,
@@ -508,9 +505,9 @@ pub fn CommandBuilder(comptime UserContext: type) type {
?PType.G.IntermediateType(); ?PType.G.IntermediateType();
fields = &(@as([fields.len]StructField, fields[0..fields.len].*) ++ [1]StructField{.{ fields = &(@as([fields.len]StructField, fields[0..fields.len].*) ++ [1]StructField{.{
.name = param.name ++ "", .name = param.name,
.type = FieldType, .type = FieldType,
.default_value_ptr = @ptrCast(&@as( .default_value = @ptrCast(&@as(
FieldType, FieldType,
if (PType.value_count == .count) 0 else null, if (PType.value_count == .count) 0 else null,
)), )),
@@ -519,8 +516,8 @@ pub fn CommandBuilder(comptime UserContext: type) type {
}}); }});
} }
return @Type(.{ .@"struct" = .{ return @Type(.{ .Struct = .{
.layout = .auto, .layout = .Auto,
.fields = fields, .fields = fields,
.decls = &.{}, .decls = &.{},
.is_tuple = false, .is_tuple = false,

View File

@@ -21,16 +21,16 @@ pub fn DefaultConverter(comptime gen: ParameterGenerics) ?ConverterSignature(gen
return if (comptime gen.multi) return if (comptime gen.multi)
MultiConverter(gen) MultiConverter(gen)
else switch (@typeInfo(gen.OutputType)) { else switch (@typeInfo(gen.OutputType)) {
.bool => FlagConverter(gen), .Bool => FlagConverter(gen),
.int => IntConverter(gen), .Int => IntConverter(gen),
.pointer => |info| if (info.size == .slice and info.child == u8) .Pointer => |info| if (info.size == .Slice and info.child == u8)
StringConverter(gen) StringConverter(gen)
else else
null, null,
.@"enum" => |info| if (info.is_exhaustive) ChoiceConverter(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 // TODO: how to handle structs with field defaults? maybe this should only work
// for tuples, which I don't think can have defaults. // 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 => |info| if (gen.value_count == .fixed and gen.value_count.fixed == info.fields.len)
StructConverter(gen) StructConverter(gen)
else else
null, null,
@@ -102,7 +102,7 @@ fn IntConverter(comptime gen: ParameterGenerics) ConverterSignature(gen) {
fn StructConverter(comptime gen: ParameterGenerics) ConverterSignature(gen) { fn StructConverter(comptime gen: ParameterGenerics) ConverterSignature(gen) {
const StructType = gen.OutputType; const StructType = gen.OutputType;
const type_info = @typeInfo(StructType).@"struct"; const type_info = @typeInfo(StructType).Struct;
const Intermediate = gen.IntermediateType(); const Intermediate = gen.IntermediateType();
return struct { return struct {
@@ -120,7 +120,7 @@ fn StructConverter(comptime gen: ParameterGenerics) ConverterSignature(gen) {
const Converter = comptime DefaultConverter( const Converter = comptime DefaultConverter(
ncmeta.copyStruct(ParameterGenerics, gen, .{ ncmeta.copyStruct(ParameterGenerics, gen, .{
.OutputType = field.type, .OutputType = field.type,
.value_count = @as(parameters.ValueCount, .{ .fixed = 1 }), .value_count = .{ .fixed = 1 },
}), }),
) orelse ) orelse
@compileError("cannot get converter for field" ++ field.name); @compileError("cannot get converter for field" ++ field.name);

View File

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

View File

@@ -57,7 +57,7 @@ pub fn StructuredPrinter(comptime Writer: type) type {
// this assumes output stream has already had the first line properly // this assumes output stream has already had the first line properly
// indented. // indented.
var splitter = std.mem.splitScalar(u8, text, '\n'); var splitter = std.mem.split(u8, text, "\n");
var location: usize = indent; var location: usize = indent;
while (splitter.next()) |line| { while (splitter.next()) |line| {
@@ -65,17 +65,6 @@ pub fn StructuredPrinter(comptime Writer: type) type {
// we have a trailing line that needs to be cleaned up // we have a trailing line that needs to be cleaned up
if (location > indent) if (location > indent)
_ = try self.clearLine(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); location = try self.clearLine(indent);
continue; continue;
} }
@@ -112,7 +101,6 @@ pub fn StructuredPrinter(comptime Writer: type) type {
} }
if (location > indent) if (location > indent)
try self.writer.writeByte(' '); try self.writer.writeByte(' ');
try self.writer.writeAll(choppee[0..split]); try self.writer.writeAll(choppee[0..split]);
location = try self.clearLine(indent); location = try self.clearLine(indent);
choppee = choppee[split + 1 ..]; choppee = choppee[split + 1 ..];
@@ -360,10 +348,11 @@ pub fn HelpBuilder(comptime command: anytype) type {
defer pairs.deinit(); defer pairs.deinit();
var just: usize = 0; var just: usize = 0;
for (subcommands.keys()) |key| { var iter = subcommands.keyIterator();
while (iter.next()) |key| {
const pair: AlignablePair = .{ const pair: AlignablePair = .{
.left = key, .left = key.*,
.right = subcommands.get(key).?.describe(), .right = subcommands.get(key.*).?.describe(),
}; };
if (pair.left.len > just) just = pair.left.len; if (pair.left.len > just) just = pair.left.len;
try pairs.append(pair); try pairs.append(pair);
@@ -484,7 +473,7 @@ pub fn optInfo(comptime command: anytype) CommandHelp {
// than just the tag name. Roll our own eventually. // than just the tag name. Roll our own eventually.
blk: { blk: {
switch (@typeInfo(@TypeOf(def))) { switch (@typeInfo(@TypeOf(def))) {
.pointer => |info| if (info.size == .Slice and info.child == u8) { .Pointer => |info| if (info.size == .Slice and info.child == u8) {
writer.print("{s}", .{def}) catch @compileError("no"); writer.print("{s}", .{def}) catch @compileError("no");
break :blk; break :blk;
}, },

View File

@@ -10,7 +10,7 @@ pub fn UpdateDefaults(comptime input: type, comptime defaults: anytype) type {
comptime { comptime {
const inputInfo = @typeInfo(input); const inputInfo = @typeInfo(input);
const fieldcount = switch (inputInfo) { const fieldcount = switch (inputInfo) {
.@"struct" => |spec| blk: { .Struct => |spec| blk: {
if (spec.decls.len > 0) { if (spec.decls.len > 0) {
@compileError("UpdateDefaults only works on structs " ++ @compileError("UpdateDefaults only works on structs " ++
"without decls due to limitations in @Type."); "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; var fields: [fieldcount]StructField = undefined;
for (inputInfo.@"struct".fields, 0..) |field, idx| { for (inputInfo.Struct.fields, 0..) |field, idx| {
fields[idx] = .{ fields[idx] = .{
.name = field.name, .name = field.name,
.field_type = field.field_type, .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 // setting null defaults work, and it converts comptime_int to
// the appropriate type, which is nice for ergonomics. Not sure // the appropriate type, which is nice for ergonomics. Not sure
// if it introduces weird edge cases. Probably it's fine? // if it introduces weird edge cases. Probably it's fine?
.default_value_ptr = if (@hasField(@TypeOf(defaults), field.name)) .default_value = if (@hasField(@TypeOf(defaults), field.name))
@ptrCast(&@as(field.field_type, @field(defaults, field.name))) @ptrCast(&@as(field.field_type, @field(defaults, field.name)))
else else
field.default_value_ptr, field.default_value,
.is_comptime = field.is_comptime, .is_comptime = field.is_comptime,
.alignment = field.alignment, .alignment = field.alignment,
}; };
} }
return @Type(.{ .@"struct" = .{ return @Type(.{ .Struct = .{
.layout = inputInfo.@"struct".layout, .layout = inputInfo.Struct.layout,
.backing_integer = inputInfo.@"struct".backing_integer, .backing_integer = inputInfo.Struct.backing_integer,
.fields = &fields, .fields = &fields,
.decls = inputInfo.@"struct".decls, .decls = inputInfo.Struct.decls,
.is_tuple = inputInfo.@"struct".is_tuple, .is_tuple = inputInfo.Struct.is_tuple,
} }); } });
} }
} }
pub fn enumLength(comptime T: type) comptime_int { pub fn enumLength(comptime T: type) comptime_int {
return @typeInfo(T).@"enum".fields.len; return @typeInfo(T).Enum.fields.len;
} }
pub fn partition(comptime T: type, input: []const T, wedge: []const []const T) [3][]const T { pub fn partition(comptime T: type, input: []const T, wedge: []const []const T) [3][]const T {
@@ -210,11 +210,11 @@ pub fn MutatingZSplitter(comptime T: type) type {
pub fn copyStruct(comptime T: type, source: T, field_overrides: anytype) T { pub fn copyStruct(comptime T: type, source: T, field_overrides: anytype) T {
var result: T = undefined; var result: T = undefined;
comptime for (@typeInfo(@TypeOf(field_overrides)).@"struct".fields) |field| { comptime inline for (@typeInfo(@TypeOf(field_overrides)).Struct.fields) |field| {
if (!@hasField(T, field.name)) @compileError("override contains bad field" ++ 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)) if (comptime @hasField(@TypeOf(field_overrides), field.name))
@field(result, field.name) = @field(field_overrides, field.name) @field(result, field.name) = @field(field_overrides, field.name)
else else
@@ -254,18 +254,19 @@ pub const TupleBuilder = struct {
comptime { comptime {
var fields: [self.types.len]StructField = undefined; var fields: [self.types.len]StructField = undefined;
for (self.types, 0..) |Type, idx| { for (self.types, 0..) |Type, idx| {
var num_buf: [128]u8 = undefined;
fields[idx] = .{ fields[idx] = .{
.name = std.fmt.comptimePrint("{d}", .{idx}), .name = std.fmt.bufPrint(&num_buf, "{d}", .{idx}) catch @compileError("failed to write field"),
.type = Type, .type = Type,
.default_value_ptr = null, .default_value = null,
// TODO: is this the right thing to do? // TODO: is this the right thing to do?
.is_comptime = false, .is_comptime = false,
.alignment = if (@sizeOf(Type) > 0) @alignOf(Type) else 0, .alignment = if (@sizeOf(Type) > 0) @alignOf(Type) else 0,
}; };
} }
return @Type(.{ .@"struct" = .{ return @Type(.{ .Struct = .{
.layout = .auto, .layout = .Auto,
.fields = &fields, .fields = &fields,
.decls = &.{}, .decls = &.{},
.is_tuple = true, .is_tuple = true,

View File

@@ -7,5 +7,4 @@ pub const parameters = @import("./parameters.zig");
pub const parser = @import("./parser.zig"); pub const parser = @import("./parser.zig");
pub const CommandBuilder = command.CommandBuilder; pub const CommandBuilder = command.CommandBuilder;
pub const commandGroup = command.commandGroup;
pub const ParserInterface = parser.ParserInterface; pub const ParserInterface = parser.ParserInterface;

View File

@@ -48,11 +48,11 @@ pub const ParameterGenerics = struct {
pub fn fixedValueCount(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) return comptime if (value_count == .fixed)
switch (@typeInfo(OutputType)) { switch (@typeInfo(OutputType)) {
.@"struct" => |info| .{ .fixed = info.fields.len }, .Struct => |info| .{ .fixed = info.fields.len },
.array => |info| .{ .fixed = info.len }, .Array => |info| .{ .fixed = info.len },
// TODO: this is a bit sloppy, but it can be refined later. // TODO: this is a bit sloppy, but it can be refined later.
// .Pointer covers slices, which may be a many-to-many conversion. // .Pointer covers slices, which may be a many-to-many conversion.
.pointer => value_count, .Pointer => value_count,
else => .{ .fixed = 1 }, else => .{ .fixed = 1 },
} }
else else
@@ -223,26 +223,6 @@ fn OptionType(comptime generics: ParameterGenerics) type {
/// want weird things to happen. /// want weird things to happen.
flag_bias: FlagBias, 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 { pub fn IntermediateValue(comptime _: @This()) type {
return generics.IntermediateValue(); return generics.IntermediateValue();
} }

View File

@@ -10,9 +10,8 @@ const NoclipError = errors.NoclipError;
pub const ParserInterface = struct { pub const ParserInterface = struct {
const Vtable = struct { const Vtable = struct {
execute: *const fn (parser: *anyopaque, context: *anyopaque) anyerror!void, 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!?ParseResult, 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!?ParserInterface, finish: *const fn (parser: *anyopaque, context: *anyopaque) anyerror!void,
getParseError: *const fn (parser: *anyopaque) []const u8,
addSubcommand: *const fn (parser: *anyopaque, name: []const u8, subcommand: ParserInterface) std.mem.Allocator.Error!void, addSubcommand: *const fn (parser: *anyopaque, name: []const u8, subcommand: ParserInterface) std.mem.Allocator.Error!void,
getSubcommand: *const fn (parser: *anyopaque, name: []const u8) ?ParserInterface, getSubcommand: *const fn (parser: *anyopaque, name: []const u8) ?ParserInterface,
describe: *const fn () []const u8, describe: *const fn () []const u8,
@@ -32,7 +31,6 @@ pub const ParserInterface = struct {
.execute = ParserType._wrapExecute, .execute = ParserType._wrapExecute,
.parse = ParserType._wrapParse, .parse = ParserType._wrapParse,
.finish = ParserType._wrapFinish, .finish = ParserType._wrapFinish,
.getParseError = ParserType._wrapGetParseError,
.addSubcommand = ParserType._wrapAddSubcommand, .addSubcommand = ParserType._wrapAddSubcommand,
.getSubcommand = ParserType._wrapGetSubcommand, .getSubcommand = ParserType._wrapGetSubcommand,
.describe = ParserType._wrapDescribe, .describe = ParserType._wrapDescribe,
@@ -46,18 +44,14 @@ pub const ParserInterface = struct {
return try self.methods.execute(self.parser, self.context); 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!?ParseResult { pub fn parse(self: @This(), name: []const u8, args: [][:0]u8, env: std.process.EnvMap) anyerror!void {
return try self.methods.parse(self.parser, self.context, name, args, env); return try self.methods.parse(self.parser, self.context, name, args, env);
} }
pub fn finish(self: @This()) anyerror!?ParserInterface { pub fn finish(self: @This()) anyerror!void {
return try self.methods.finish(self.parser, self.context); 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 { pub fn addSubcommand(self: @This(), name: []const u8, subcommand: ParserInterface) std.mem.Allocator.Error!void {
return try self.methods.addSubcommand(self.parser, name, subcommand); return try self.methods.addSubcommand(self.parser, name, subcommand);
} }
@@ -79,8 +73,7 @@ pub const ParserInterface = struct {
} }
}; };
pub const CommandMap = std.StringArrayHashMap(ParserInterface); pub const CommandMap = std.hash_map.StringHashMap(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 // the parser is generated by the bind method of the CommandBuilder, so we can
// be extremely type-sloppy here, which simplifies the signature. // be extremely type-sloppy here, which simplifies the signature.
@@ -102,7 +95,6 @@ pub fn Parser(comptime command: anytype, comptime callback: anytype) type {
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
subcommands: CommandMap, subcommands: CommandMap,
subcommand: ?ParserInterface = null, subcommand: ?ParserInterface = null,
error_message: std.ArrayListUnmanaged(u8) = .{},
help_builder: help.HelpBuilder(command), help_builder: help.HelpBuilder(command),
// This is a slightly annoying hack to work around the fact that there's no way // This is a slightly annoying hack to work around the fact that there's no way
@@ -114,59 +106,13 @@ pub fn Parser(comptime command: anytype, comptime callback: anytype) type {
// cognitively clutter this struct. // cognitively clutter this struct.
pub usingnamespace InterfaceWrappers(@This()); pub usingnamespace InterfaceWrappers(@This());
pub fn execute(self: *@This(), context: UserContext) anyerror!void {
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) {
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]);
{
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;
};
}
}
}
pub fn subparse( pub fn subparse(
self: *@This(), self: *@This(),
context: UserContext, context: UserContext,
name: []const u8, name: []const u8,
args: [][:0]u8, args: [][:0]u8,
env: std.process.EnvMap, env: std.process.EnvMap,
) anyerror!?ParseResult { ) anyerror!void {
const sliceto = try self.parse(name, args); const sliceto = try self.parse(name, args);
try self.readEnvironment(env); try self.readEnvironment(env);
try self.convertEager(context); try self.convertEager(context);
@@ -175,30 +121,21 @@ pub fn Parser(comptime command: anytype, comptime callback: anytype) type {
const grafted_name = try std.mem.join( const grafted_name = try std.mem.join(
self.allocator, self.allocator,
" ", " ",
&.{ name, args[sliceto - 1] }, &[_][]const u8{ name, args[sliceto - 1] },
); );
return .{ .name = grafted_name, .args = args[sliceto..], .parser = subcommand }; try subcommand.parse(grafted_name, args[sliceto..], env);
} else if (self.subcommands.count() > 0 and command.subcommand_required) { } else if (self.subcommands.count() > 0 and command.subcommand_required) {
const stderr = std.io.getStdErr().writer(); const stderr = std.io.getStdErr().writer();
try stderr.print("'{s}' requires a subcommand.\n\n", .{name}); try stderr.writeAll("A subcommand is required.\n\n");
self.printHelp(name); self.printHelp(name);
} }
return null;
} }
pub fn finish(self: *@This(), context: UserContext) anyerror!?ParserInterface { pub fn finish(self: *@This(), context: UserContext) anyerror!void {
try self.convert(context); try self.convert(context);
try callback(context, self.output); try callback(context, self.output);
return self.subcommand; if (self.subcommand) |subcommand| try subcommand.finish();
}
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 { pub fn deinit(self: @This()) void {
@@ -207,7 +144,8 @@ pub fn Parser(comptime command: anytype, comptime callback: anytype) type {
} }
pub fn deinitTree(self: @This()) void { pub fn deinitTree(self: @This()) void {
for (self.subcommands.values()) |subcommand| { var iterator = self.subcommands.valueIterator();
while (iterator.next()) |subcommand| {
subcommand.deinitTree(); subcommand.deinitTree();
} }
self.deinit(); self.deinit();
@@ -221,6 +159,18 @@ pub fn Parser(comptime command: anytype, comptime callback: anytype) type {
return self.subcommands.get(name); return self.subcommands.get(name);
} }
pub fn execute(self: *@This(), context: UserContext) anyerror!void {
const args = try std.process.argsAlloc(self.allocator);
var env = try std.process.getEnvMap(self.allocator);
if (args.len < 1) return ParseError.EmptyArgs;
self.progname = std.fs.path.basename(args[0]);
try self.subparse(context, self.progname.?, args[1..], env);
try self.finish(context);
}
fn printValue(self: @This(), value: anytype, comptime indent: []const u8) void { fn printValue(self: @This(), value: anytype, comptime indent: []const u8) void {
if (comptime @hasField(@TypeOf(value), "items")) { if (comptime @hasField(@TypeOf(value), "items")) {
std.debug.print("{s}[\n", .{indent}); std.debug.print("{s}[\n", .{indent});
@@ -330,10 +280,6 @@ pub fn Parser(comptime command: anytype, comptime callback: anytype) type {
} }
} }
try self.error_message.writer(self.allocator).print(
"Could not parse command line: unknown option \"{s}\"\n",
.{arg},
);
return ParseError.UnknownLongTagParameter; return ParseError.UnknownLongTagParameter;
} }
@@ -356,23 +302,14 @@ pub fn Parser(comptime command: anytype, comptime callback: anytype) type {
if (arg == tag[1]) { if (arg == tag[1]) {
if (comptime !PType.is_flag) 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; return ParseError.FusedShortTagValueMissing;
};
try self.applyParamValues(param, argit, false); try self.applyParamValues(param, argit, false);
return; return;
} }
} }
try self.error_message.writer(self.allocator).print(
"Could not parse command line: unknown option \"-{c}\"\n",
.{arg},
);
return ParseError.UnknownShortTagParameter; return ParseError.UnknownShortTagParameter;
} }
@@ -399,20 +336,7 @@ pub fn Parser(comptime command: anytype, comptime callback: anytype) type {
arg_index += 1; arg_index += 1;
} }
return self.subcommands.get(arg) orelse { return self.subcommands.get(arg) orelse ParseError.ExtraValue;
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 pushIntermediateValue( fn pushIntermediateValue(
@@ -427,7 +351,7 @@ pub fn Parser(comptime command: anytype, comptime callback: anytype) type {
if (@field(self.intermediate, param.name) == null) { if (@field(self.intermediate, param.name) == null) {
@field(self.intermediate, param.name) = gen.IntermediateType().init(self.allocator); @field(self.intermediate, param.name) = gen.IntermediateType().init(self.allocator);
} }
try @field(self.intermediate, param.name).?.append(value); @field(self.intermediate, param.name).?.append(value) catch return ParseError.UnexpectedFailure;
} else if (comptime @TypeOf(param).G.nonscalar()) { } else if (comptime @TypeOf(param).G.nonscalar()) {
if (@field(self.intermediate, param.name)) |list| list.deinit(); if (@field(self.intermediate, param.name)) |list| list.deinit();
@field(self.intermediate, param.name) = value; @field(self.intermediate, param.name) = value;
@@ -446,54 +370,19 @@ pub fn Parser(comptime command: anytype, comptime callback: anytype) type {
.flag => try self.pushIntermediateValue(param, comptime param.flag_bias.string()), .flag => try self.pushIntermediateValue(param, comptime param.flag_bias.string()),
.count => @field(self.intermediate, param.name) += 1, .count => @field(self.intermediate, param.name) += 1,
.fixed => |count| switch (count) { .fixed => |count| switch (count) {
0 => { 0 => return ParseError.ExtraValue,
const writer = self.error_message.writer(self.allocator); 1 => try self.pushIntermediateValue(param, argit.next() orelse return ParseError.MissingValue),
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| { else => |total| {
var list = try std.ArrayList([:0]const u8).initCapacity(self.allocator, total); var list = std.ArrayList([:0]const u8).initCapacity(self.allocator, total) catch
return ParseError.UnexpectedFailure;
var consumed: u32 = 0; var consumed: u32 = 0;
while (consumed < total) : (consumed += 1) { while (consumed < total) : (consumed += 1) {
const next = argit.next() orelse { const next = argit.next() orelse return ParseError.MissingValue;
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;
};
list.appendAssumeCapacity(next); list.append(next) catch return ParseError.UnexpectedFailure;
}
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;
} }
if (bounded and argit.next() != null) return ParseError.ExtraValue;
try self.pushIntermediateValue(param, list); try self.pushIntermediateValue(param, list);
}, },
@@ -515,7 +404,8 @@ pub fn Parser(comptime command: anytype, comptime callback: anytype) type {
if (comptime param.env_var) |env_var| blk: { if (comptime param.env_var) |env_var| blk: {
if (@field(self.intermediate, param.name) != null) break :blk; if (@field(self.intermediate, param.name) != null) break :blk;
const val = try self.allocator.dupeZ(u8, env.get(env_var) orelse break :blk); const val = self.allocator.dupeZ(u8, env.get(env_var) orelse break :blk) catch
return ParseError.UnexpectedFailure;
if (comptime @TypeOf(param).G.value_count == .flag) { if (comptime @TypeOf(param).G.value_count == .flag) {
try self.pushIntermediateValue(param, val); try self.pushIntermediateValue(param, val);
@@ -549,27 +439,19 @@ pub fn Parser(comptime command: anytype, comptime callback: anytype) type {
if (comptime @TypeOf(param).has_output) { if (comptime @TypeOf(param).has_output) {
@field(self.output, param.name) = param.converter(context, intermediate, writer) catch |err| { @field(self.output, param.name) = param.converter(context, intermediate, writer) catch |err| {
const err_writer = self.error_message.writer(self.allocator); const stderr = std.io.getStdErr().writer();
const desc = try param.describe(self.allocator); stderr.print("Error parsing option \"{s}\": {s}\n", .{ param.name, buffer.items }) catch {};
defer self.allocator.free(desc);
try err_writer.print("Error parsing option {s}: {s}\n", .{ desc, buffer.items });
return err; return err;
}; };
} else { } else {
param.converter(context, intermediate, writer) catch |err| { param.converter(context, intermediate, writer) catch |err| {
const err_writer = self.error_message.writer(self.allocator); const stderr = std.io.getStdErr().writer();
const desc = try param.describe(self.allocator); stderr.print("Error parsing option \"{s}\": {s}\n", .{ param.name, buffer.items }) catch {};
defer self.allocator.free(desc);
try err_writer.print("Error parsing option {s}: {s}\n", .{ desc, buffer.items });
return err; return err;
}; };
} }
} else { } else {
if (comptime param.required) { 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; return ParseError.RequiredParameterMissing;
} else if (comptime @TypeOf(param).has_output) { } else if (comptime @TypeOf(param).has_output) {
if (comptime param.default) |def| { if (comptime param.default) |def| {
@@ -616,23 +498,18 @@ fn InterfaceWrappers(comptime ParserType: type) type {
name: []const u8, name: []const u8,
args: [][:0]u8, args: [][:0]u8,
env: std.process.EnvMap, env: std.process.EnvMap,
) anyerror!?ParseResult { ) anyerror!void {
const self = castInterfaceParser(parser); const self = castInterfaceParser(parser);
const context = self.castContext(ctx); const context = self.castContext(ctx);
return try self.subparse(context, name, args, env); return try self.subparse(context, name, args, env);
} }
fn _wrapFinish(parser: *anyopaque, ctx: *anyopaque) anyerror!?ParserInterface { fn _wrapFinish(parser: *anyopaque, ctx: *anyopaque) anyerror!void {
const self = castInterfaceParser(parser); const self = castInterfaceParser(parser);
const context = self.castContext(ctx); const context = self.castContext(ctx);
return try self.finish(context); 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 { fn _wrapAddSubcommand(parser: *anyopaque, name: []const u8, subcommand: ParserInterface) !void {
const self = castInterfaceParser(parser); const self = castInterfaceParser(parser);
return self.addSubcommand(name, subcommand); return self.addSubcommand(name, subcommand);