Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
4297ad8
Add prototype of declarative argument parser
Argmaster May 9, 2025
b3b9e3a
Add prototype of introspection based argument parser
Argmaster May 10, 2025
75c3a68
Change prototype design
Argmaster May 10, 2025
2ea6565
Adjust the design as requested by Quantum
Argmaster May 10, 2025
8b052ff
Unpack structs + migrate TP command
Argmaster May 11, 2025
691d39c
Update src/server/command/tp.zig
Argmaster May 12, 2025
c9a563a
Autocompletion sketch, requests, other stuff
Argmaster May 12, 2025
bc37a3e
Improve error message on failure
Argmaster May 12, 2025
0ac44e4
Add checkExists param to BiomeId to enable testing
Argmaster May 12, 2025
f00de3b
Apply message suggestions
Argmaster May 13, 2025
2b0515d
Remove extra empty line between error messages
Argmaster May 13, 2025
85aa3d3
Merge remote-tracking branch 'origin/master' into feature/declargs
Argmaster May 13, 2025
38923d4
Merge remote-tracking branch 'origin/master' into feature/declargs
Argmaster May 13, 2025
2fbb740
Use argument names for error messages
Argmaster May 14, 2025
f25893a
Fix text
Argmaster May 14, 2025
350f46f
Foos and bars may break my spams but pr needs some features
Argmaster May 15, 2025
eaef925
Tests go fix fix
Argmaster May 15, 2025
57c59fc
Replace manual loop with std.mem.concat
Argmaster May 18, 2025
598982f
Merge remote-tracking branch 'origin/master' into feature/declargs
Argmaster May 18, 2025
a4bc1e8
Fixes yet again
Argmaster May 18, 2025
668655b
Merge branch 'master' into feature/declargs
Argmaster May 20, 2025
fe4d8e3
Merge remote-tracking branch 'origin/master' into feature/declargs
Argmaster May 25, 2025
738feb6
Merge remote-tracking branch 'origin/master' into feature/declargs
Argmaster Jun 28, 2025
3e44684
Use our param for errors
Argmaster Jun 28, 2025
eb1c602
Idk how those bugs got here
Argmaster Jun 29, 2025
b6eb4aa
Remove continue compiler complains about
Argmaster Jun 29, 2025
bebc17e
This is not working for some reason
Argmaster Jun 29, 2025
13ac15d
Use errors for arguments
Argmaster Jun 30, 2025
52da735
Now it works
Argmaster Jun 30, 2025
3e532b1
Return error.ParseError
Argmaster Jun 30, 2025
33443ac
Discard changes from tp.zig
Argmaster Dec 14, 2025
1e1d0f2
Discard changes from biomes.zig
Argmaster Dec 14, 2025
47e7d1b
Merge remote-tracking branch 'origin/master' into feature/declargs
Argmaster Dec 14, 2025
d5dfb5b
Discard biome id implementation
Argmaster Dec 14, 2025
0fbf47f
Remove biome id tests
Argmaster Dec 14, 2025
86cf529
Update time command
Argmaster Dec 14, 2025
fa71fca
Use enum instead of boolean
Argmaster Dec 14, 2025
841e672
Polish behavior quirks of enums and whitespace
Argmaster Dec 15, 2025
84b291d
Reorder cases in args switch of time
Argmaster Dec 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
382 changes: 382 additions & 0 deletions src/argparse.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,382 @@
const std = @import("std");

const main = @import("main");
const NeverFailingAllocator = main.heap.NeverFailingAllocator;
const ListUnmanaged = main.ListUnmanaged;
const utils = main.utils;

pub const Options = struct {
commandName: []const u8,
};

const ResolveMode = enum {
Parse,
Autocomplete,
};

pub fn Parser(comptime T: type, comptime options: Options) type {
return struct {
const Self = @This();

pub fn parse(allocator: NeverFailingAllocator, args: []const u8, errorMessage: *ListUnmanaged(u8)) error{ParseError}!T {
return resolve(ResolveMode.Parse, allocator, args, errorMessage);
}

pub fn autocomplete(allocator: NeverFailingAllocator, args: []const u8, errorMessage: *ListUnmanaged(u8)) AutocompleteResult {
return resolve(ResolveMode.Autocomplete, allocator, args, errorMessage);
}

pub fn resolve(
comptime mode: ResolveMode,
allocator: NeverFailingAllocator,
args: []const u8,
errorMessage: *ListUnmanaged(u8),
) switch(mode) {
.Autocomplete => AutocompleteResult,
.Parse => error{ParseError}!T,
} {
switch(@typeInfo(T)) {
inline .@"struct" => |s| {
return resolveStruct(mode, s, allocator, args, errorMessage);
},
inline .@"union" => |u| {
if(u.tag_type == null) @compileError("Union must have a tag type");
return switch(mode) {
.Autocomplete => autocompleteUnion(u, allocator, args, errorMessage),
.Parse => parseUnion(u, allocator, args, errorMessage),
};
},
else => @compileError("Only structs and unions are supported"),
}
}

fn resolveStruct(
comptime mode: ResolveMode,
comptime s: std.builtin.Type.Struct,
allocator: NeverFailingAllocator,
args: []const u8,
errorMessage: *ListUnmanaged(u8),
) switch(mode) {
.Autocomplete => AutocompleteResult,
.Parse => error{ParseError}!T,
} {
var result: T = undefined;
var split = std.mem.splitScalar(u8, args, ' ');

var tempErrorMessage: ListUnmanaged(u8) = .{};
defer tempErrorMessage.deinit(allocator);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All local allocations should use the stackAllocator

@codemob-dev we need the fix for #1286

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@codemob-dev we need the fix for #1286

Not again...


var nextArgument: ?[]const u8 = split.next();

inline for(s.fields) |field| {
const value = resolveArgument(field.type, allocator, field.name[0..], nextArgument, &tempErrorMessage);

if(value == error.ParseError) {
if(@typeInfo(field.type) == .optional) {
@field(result, field.name) = null;
tempErrorMessage.clearRetainingCapacity();
} else {
errorMessage.appendSlice(allocator, tempErrorMessage.items);
return error.ParseError;
}
} else {
@field(result, field.name) = value catch unreachable;
tempErrorMessage.clearRetainingCapacity();
nextArgument = split.next();
}
}

const isLeftoverArgumentWhitespace = nextArgument == null or main.utils.all(std.ascii.isWhitespace, nextArgument.?);
const areUnusedArgumentsWhitespace = main.utils.all(std.ascii.isWhitespace, split.rest());

if(!isLeftoverArgumentWhitespace or !areUnusedArgumentsWhitespace) {
failWithMessage(allocator, errorMessage, "Too many arguments for command, expected {}", .{s.fields.len});
return error.ParseError;
}

return result;
}

fn resolveArgument(comptime Field: type, allocator: NeverFailingAllocator, name: []const u8, argument: ?[]const u8, errorMessage: *ListUnmanaged(u8)) error{ParseError}!Field {
switch(@typeInfo(Field)) {
inline .optional => |optionalInfo| {
if(argument == null) return error.ParseError;
return resolveArgument(optionalInfo.child, allocator, name, argument, errorMessage) catch |err| {
return err;
};
},
inline .@"struct" => {
const arg = argument orelse {
failWithMessage(allocator, errorMessage, missingArgumentMessage, .{name});
return error.ParseError;
};
if(!@hasDecl(Field, "parse")) @compileError("Struct must have a parse function");
return @field(Field, "parse")(allocator, name, arg, errorMessage);
},
inline .@"enum" => {
const arg = argument orelse {
failWithMessage(allocator, errorMessage, missingArgumentMessage, .{name});
return error.ParseError;
};
return std.meta.stringToEnum(Field, arg) orelse {
const str = main.meta.concatComptime("/", std.meta.fieldNames(Field));
failWithMessage(allocator, errorMessage, "Expected one of {s} for <{s}>, found \"{s}\"", .{str, name, arg});
return error.ParseError;
};
},
inline .float => |floatInfo| return {
const arg = argument orelse {
failWithMessage(allocator, errorMessage, missingArgumentMessage, .{name});
return error.ParseError;
};
return std.fmt.parseFloat(std.meta.Float(floatInfo.bits), arg) catch {
failWithMessage(allocator, errorMessage, "Expected a number for <{s}>, found \"{s}\"", .{name, arg});
return error.ParseError;
};
},
inline .int => |intInfo| {
const arg = argument orelse {
failWithMessage(allocator, errorMessage, missingArgumentMessage, .{name});
return error.ParseError;
};
return std.fmt.parseInt(std.meta.Int(intInfo.signedness, intInfo.bits), arg, 0) catch {
failWithMessage(allocator, errorMessage, "Expected an integer for <{s}>, found \"{s}\"", .{name, arg});
return error.ParseError;
};
},
inline else => |other| @compileError("Unsupported type " ++ @tagName(other)),
}
}

const missingArgumentMessage = "Missing argument at position <{s}>";

fn autocompleteArgument(comptime Field: type, allocator: NeverFailingAllocator, _arg: ?[]const u8) AutocompleteResult {
const arg = _arg orelse return .{};
switch(@typeInfo(Field)) {
inline .@"struct" => {
if(!@hasDecl(Field, "autocomplete")) @compileError("Struct must have an autocomplete function");
return try @field(Field, "autocomplete")(allocator, arg);
},
inline .@"enum" => {
var result: AutocompleteResult = .{};
inline for(std.meta.fieldNames(Field)) |fieldName| {
if(!std.mem.startsWith(u8, fieldName, arg)) continue;
result.suggestions.append(allocator, allocator.dupe(u8, fieldName));
}
return result;
},
inline else => return .{},
}
}

fn parseUnion(comptime u: std.builtin.Type.Union, allocator: NeverFailingAllocator, args: []const u8, errorMessage: *ListUnmanaged(u8)) error{ParseError}!T {
var tempErrorMessage: ListUnmanaged(u8) = .{};
defer tempErrorMessage.deinit(allocator);

tempErrorMessage.appendSlice(allocator, "---");

inline for(u.fields) |field| {
tempErrorMessage.append(allocator, '\n');
tempErrorMessage.appendSlice(allocator, field.name);
tempErrorMessage.append(allocator, '\n');

const result = Parser(field.type, options).resolve(.Parse, allocator, args, &tempErrorMessage);
if(result != error.ParseError) {
return @unionInit(T, field.name, result catch unreachable);
}
tempErrorMessage.appendSlice(allocator, "\n---");
}

errorMessage.appendSlice(allocator, tempErrorMessage.items);
return error.ParseError;
}

fn autocompleteUnion(comptime u: std.builtin.Type.Union, allocator: NeverFailingAllocator, args: []const u8) AutocompleteResult {
var result: AutocompleteResult = .{};

inline for(u.fields) |field| {
var completion = Parser(field.type).resolve(true, allocator, args);
defer completion.deinit(allocator);

result.takeSuggestions(allocator, &completion);
}

return result;
}
};
}

fn failWithMessage(allocator: NeverFailingAllocator, errorMessage: *ListUnmanaged(u8), comptime fmt: []const u8, args: anytype) void {
const msg = std.fmt.allocPrint(allocator.allocator, fmt, args) catch unreachable;
defer allocator.free(msg);
errorMessage.appendSlice(allocator, msg);
}

pub const AutocompleteResult = struct {
suggestions: ListUnmanaged([]const u8) = .{},

pub fn takeSuggestions(self: *AutocompleteResult, allocator: NeverFailingAllocator, other: *AutocompleteResult) void {
for(other.suggestions.items) |message| {
self.suggestions.append(allocator, message);
}
other.suggestions.clearAndFree(allocator);
}

pub fn deinit(self: AutocompleteResult, allocator: NeverFailingAllocator) void {
for(self.suggestions.items) |item| {
allocator.free(item);
}
self.suggestions.deinit(allocator);
}
};

const Test = struct {
var testingAllocator = main.heap.ErrorHandlingAllocator.init(std.testing.allocator);
var allocator = testingAllocator.allocator();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also not needed with #1286


const OnlyX = Parser(struct {x: f64}, .{.commandName = ""});

const @"Union X or XY" = Parser(union(enum) {
x: struct {x: f64},
xy: struct {x: f64, y: f64},
}, .{.commandName = ""});

const @"subCommands foo or bar" = Parser(union(enum) {
foo: struct {cmd: enum(u1) {foo}, x: f64},
bar: struct {cmd: enum(u1) {bar}, x: f64, y: f64},
}, .{.commandName = ""});
};

test "float" {
var errors: ListUnmanaged(u8) = .{};
defer errors.deinit(Test.allocator);

const result = try Test.OnlyX.parse(Test.allocator, "33.0", &errors);

try std.testing.expect(errors.items.len == 0);
try std.testing.expect(result.x == 33.0);
}

test "float negative" {
var errors: ListUnmanaged(u8) = .{};
defer errors.deinit(Test.allocator);

const result = Test.OnlyX.parse(Test.allocator, "foo", &errors);

try std.testing.expectError(error.ParseError, result);
try std.testing.expect(errors.items.len != 0);
}

test "enum" {
const ArgParser = Parser(struct {
cmd: enum(u1) {foo},
}, .{.commandName = "c"});

var errors: ListUnmanaged(u8) = .{};
defer errors.deinit(Test.allocator);

const result = try ArgParser.parse(Test.allocator, "foo", &errors);

try std.testing.expect(errors.items.len == 0);
try std.testing.expect(result.cmd == .foo);
}

test "float int float" {
const ArgParser = Parser(struct {
x: f64,
y: i32,
z: f32,
}, .{.commandName = ""});

var errors: ListUnmanaged(u8) = .{};
defer errors.deinit(Test.allocator);

const result = try ArgParser.parse(Test.allocator, "33.0 154 -5654.0", &errors);

try std.testing.expect(errors.items.len == 0);
try std.testing.expect(result.x == 33.0);
try std.testing.expect(result.y == 154);
try std.testing.expect(result.z == -5654.0);
}

test "float int optional float missing" {
const ArgParser = Parser(struct {
x: f64,
y: i32,
z: ?f32,
}, .{.commandName = ""});

var errors: ListUnmanaged(u8) = .{};
defer errors.deinit(Test.allocator);

const result = try ArgParser.parse(Test.allocator, "33.0 154", &errors);

try std.testing.expect(errors.items.len == 0);
try std.testing.expect(result.x == 33.0);
try std.testing.expect(result.y == 154);
try std.testing.expect(result.z == null);
}

test "x or xy case x" {
var errors: ListUnmanaged(u8) = .{};
defer errors.deinit(Test.allocator);

const result = try Test.@"Union X or XY".parse(Test.allocator, "0.9", &errors);

try std.testing.expect(errors.items.len == 0);
try std.testing.expect(result.x.x == 0.9);
}

test "x or xy case xy" {
var errors: ListUnmanaged(u8) = .{};
defer errors.deinit(Test.allocator);

const result = try Test.@"Union X or XY".parse(Test.allocator, "0.9 1.0", &errors);

try std.testing.expect(errors.items.len == 0);
try std.testing.expect(result.xy.x == 0.9);
try std.testing.expect(result.xy.y == 1.0);
}

test "x or xy negative empty" {
var errors: ListUnmanaged(u8) = .{};
defer errors.deinit(Test.allocator);

const result = Test.@"Union X or XY".parse(Test.allocator, "", &errors);

try std.testing.expect(errors.items.len != 0);
try std.testing.expectError(error.ParseError, result);
}

test "x or xy negative too much" {
var errors: ListUnmanaged(u8) = .{};
defer errors.deinit(Test.allocator);

const result = Test.@"Union X or XY".parse(Test.allocator, "1.0 3.0 5.0", &errors);

try std.testing.expect(errors.items.len != 0);
try std.testing.expectError(error.ParseError, result);
}

test "subCommands foo" {
var errors: ListUnmanaged(u8) = .{};
defer errors.deinit(Test.allocator);

const result = try Test.@"subCommands foo or bar".parse(Test.allocator, "foo 1.0", &errors);

try std.testing.expect(errors.items.len == 0);
try std.testing.expect(result.foo.cmd == .foo);
try std.testing.expect(result.foo.x == 1.0);
}

test "subCommands bar" {
var errors: ListUnmanaged(u8) = .{};
defer errors.deinit(Test.allocator);

const result = try Test.@"subCommands foo or bar".parse(Test.allocator, "bar 2.0 3.0", &errors);

try std.testing.expect(errors.items.len == 0);
try std.testing.expect(result.bar.cmd == .bar);
try std.testing.expect(result.bar.x == 2.0);
try std.testing.expect(result.bar.y == 3.0);
}
1 change: 1 addition & 0 deletions src/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pub const gui = @import("gui/gui.zig");
pub const server = @import("server/server.zig");

pub const audio = @import("audio.zig");
pub const argparse = @import("argparse.zig");
pub const assets = @import("assets.zig");
pub const block_entity = @import("block_entity.zig");
pub const blocks = @import("blocks.zig");
Expand Down
Loading