zig-lys/src/args/help.zig

260 lines
7.1 KiB
Zig
Raw Normal View History

2024-11-28 01:14:52 +00:00
const std = @import("std");
2024-11-28 15:57:54 +00:00
const log = std.log.scoped(.help);
2024-11-28 01:14:52 +00:00
2024-11-28 15:57:54 +00:00
const argLib = @import("args.zig");
const Marker = argLib.Marker;
const Extra = argLib.Extra;
2024-12-09 05:28:16 +00:00
const niceTypeName = @import("../util/utils.zig").niceTypeName;
2024-11-28 15:57:54 +00:00
pub fn printHelp(comptime T: type, comptime name: []const u8, writer: std.io.AnyWriter) !void {
const info = @typeInfo(T);
switch (info) {
.Struct => {},
else => {
log.warn("We only support printing the help of `Struct`s, not {s}", .{@tagName(info)});
return error.NotImplemented;
},
}
var hasFlags = false;
inline for (info.Struct.fields) |field| {
if (std.mem.eql(u8, field.name, "allocator")) {
comptime continue;
}
if (field.type == Marker(bool)) {
hasFlags = true;
comptime break;
}
}
var hasPositionals = false;
inline for (info.Struct.fields) |field| {
if (std.mem.eql(u8, field.name, "allocator")) {
comptime continue;
}
const valueOpaque = field.default_value orelse @panic("Missing default value for field " ++ field.name);
const valueMarker: *const field.type = @alignCast(@ptrCast(valueOpaque));
const value: Extra = @field(valueMarker, "extra");
switch (value) {
.Positional => {
hasPositionals = true;
comptime break;
},
else => {},
}
}
var hasRemainder = false;
inline for (info.Struct.fields) |field| {
if (std.mem.eql(u8, field.name, "allocator")) {
comptime continue;
}
const valueOpaque = field.default_value orelse @panic("Missing default value for field " ++ field.name);
const valueMarker: *const field.type = @alignCast(@ptrCast(valueOpaque));
const value: Extra = @field(valueMarker, "extra");
switch (value) {
.Remainder => {
hasRemainder = true;
comptime break;
},
else => {},
}
}
try writer.print("Usage: {s}", .{name});
if (hasFlags) {
try writer.print(" [flags]", .{});
}
if (hasPositionals) {
inline for (info.Struct.fields) |field| {
if (std.mem.eql(u8, field.name, "allocator")) {
comptime continue;
}
const valueOpaque = field.default_value orelse @panic("Missing default value for field " ++ field.name);
const valueMarker: *const field.type = @alignCast(@ptrCast(valueOpaque));
const value: Extra = @field(valueMarker, "extra");
switch (value) {
.Positional => {
try writer.print(" <{s}>", .{field.name});
},
else => {},
}
}
}
if (hasRemainder) {
try writer.print(" [...]", .{});
}
try writer.print("\n", .{});
try writer.print("Legend: <required> [optional]\n\n", .{});
inline for (info.Struct.fields) |field| {
if (std.mem.eql(u8, field.name, "allocator")) {
comptime continue;
}
const valueOpaque = field.default_value orelse @panic("Missing default value for field " ++ field.name);
const valueMarker: *const field.type = @alignCast(@ptrCast(valueOpaque));
const valueType = niceTypeName(@TypeOf(valueMarker.*.value));
const isOptional = std.mem.startsWith(u8, valueType, "?");
switch (valueMarker.*.extra) {
.Remainder => {
comptime continue;
},
.Positional => |pos| {
try writer.print("* <{s}>: {s}", .{
field.name,
valueType,
});
if (pos.typeHint) |typeHint| {
try writer.print(" ({s})", .{typeHint});
}
if (pos.about) |about| {
try writer.print(" | {s}", .{about});
}
},
.Flag => |flag| {
try writer.print("* ", .{});
try writer.print("--{s}", .{
flag.name,
});
if (flag.takesValue) {
try writer.print("=<value>", .{});
}
if (flag.short) |short| {
try writer.print(" (-{s}", .{short});
if (flag.takesValue) {
try writer.print("=<value>", .{});
}
try writer.print(")", .{});
}
if (flag.takesValue) {
try writer.print(": {s}", .{valueType});
2024-11-28 15:57:54 +00:00
if (flag.typeHint) |typeHint| {
try writer.print(" ({s})", .{typeHint});
}
2024-11-28 15:57:54 +00:00
}
if (!isOptional and !flag.toggle) {
2024-11-28 15:57:54 +00:00
try writer.print(" <required>", .{});
}
if (flag.about) |about| {
try writer.print(" | {s}", .{about});
}
},
}
try writer.print("\n", .{});
}
}
const t = std.testing;
test "empty help" {
var buf = std.ArrayList(u8).init(t.allocator);
defer buf.deinit();
const Demo = struct {};
try printHelp(Demo, "demo", buf.writer().any());
try t.expectEqualStrings(
\\Usage: demo
\\Legend: <required> [optional]
\\
\\
, buf.items);
}
test "basic help" {
var buf = std.ArrayList(u8).init(t.allocator);
defer buf.deinit();
const Demo = struct {
2024-11-28 20:31:30 +00:00
verbose: Marker(bool) = .{
2024-11-28 15:57:54 +00:00
.value = undefined,
.extra = .{
.Flag = .{
.name = "verbose",
.short = "v",
2024-11-28 15:57:54 +00:00
.toggle = true,
},
},
},
2024-11-28 20:31:30 +00:00
positional: Marker([]const u8) = .{
2024-11-28 15:57:54 +00:00
.value = undefined,
.extra = .{ .Positional = .{} },
},
2024-11-28 20:31:30 +00:00
remainder: Marker(std.ArrayList([]const u8)) = .{
2024-11-28 15:57:54 +00:00
.value = undefined,
.extra = .{ .Remainder = {} },
},
};
try printHelp(Demo, "demo", buf.writer().any());
try t.expectEqualStrings(
\\Usage: demo [flags] <positional> [...]
\\Legend: <required> [optional]
\\
\\* --verbose (-v)
2024-11-28 15:57:54 +00:00
\\* <positional>: string
\\
, buf.items);
}
test "about and type hint" {
var buf = std.ArrayList(u8).init(t.allocator);
defer buf.deinit();
const Demo = struct {
2024-11-28 20:31:30 +00:00
verbose: Marker(bool) = .{
2024-11-28 15:57:54 +00:00
.value = undefined,
.extra = .{ .Flag = .{
.name = "verbose",
.short = "v",
.takesValue = true,
.about = "makes the output verbose",
.typeHint = "yes/no",
} },
},
};
try printHelp(Demo, "demo", buf.writer().any());
try t.expectEqualStrings(
\\Usage: demo [flags]
\\Legend: <required> [optional]
\\
\\* --verbose=<value> (-v=<value>): bool (yes/no) <required> | makes the output verbose
\\
, buf.items);
2024-11-28 01:14:52 +00:00
}