00JCIV00 / cova

Commands, Options, Values, Arguments. A simple yet robust cross-platform command line argument parsing library for Zig.
https://00jciv00.github.io/cova/
MIT License
110 stars 5 forks source link

Further format configuration options? #27

Closed p7r0x7 closed 1 year ago

p7r0x7 commented 1 year ago

The structure and forethought behind cova is excellent, and I'm sure I'll be using it plenty going forward, but the formatting options of output is somewhat limited in its current state.

Some parts are customizable via subcmds_help_fmt; subcmds_usage_fmt; vals_help_fmt; and vals_usage_fmt, but others hardcoded into the library and can't easily be changed from an end users codebase. I'm sure you know which parts I mean, and I leave it up to you how you want to handle this feature request.

Thanks for your hard work!

00JCIV00 commented 1 year ago

So, looking at the code for the Argument Configs and reading through the tail end of #24, it looks like I've made an oversight in some of the changes for v0.8.0. The vals_help_fmt and vals_usage_fmt should've been moved under the Value.Config struct. That way, Values would mirror the setup of both Commands and Options more closely. Definitely a blunder on my part, and that probably made it harder to find the Options formatting fields (because you'd expect them to be with the other formatters). I'll definitely be sure to fix this soon.

Separately though, if there are other areas that you believe are too hard coded, please let me know! I'll do another review through the library to see if there are any other spots I can make more customizable and keep that functionality in mind for future features. I'll keep this issue open at least until I move the Values to their proper spot. That'll be a small breaking change if you're currently customizing the Value formatters, but it'll be as simple as moving those fields under the Value.Config struct.

Also, thanks a ton for the kudos. It's great seeing that the library and its design are actually useful!

p7r0x7 commented 1 year ago

I was thinking specifically about how the help and usage menu structure printout not being very customizable.

p7r0x7 commented 1 year ago

So, looking at the code for the Argument Configs and reading through the tail end of #24, it looks like I've made an oversight in some of the changes for v0.8.0. The vals_help_fmt and vals_usage_fmt should've been moved under the Value.Config struct. That way, Values would mirror the setup of both Commands and Options more closely. Definitely a blunder on my part, and that probably made it harder to find the Options formatting fields (because you'd expect them to be with the other formatters). I'll definitely be sure to fix this soon.

Separately though, if there are other areas that you believe are too hard coded, please let me know! I'll do another review through the library to see if there are any other spots I can make more customizable and keep that functionality in mind for future features. I'll keep this issue open at least until I move the Values to their proper spot. That'll be a small breaking change if you're currently customizing the Value formatters, but it'll be as simple as moving those fields under the Value.Config struct.

Also, thanks a ton for the kudos. It's great seeing that the library and its design are actually useful!

Please let me know when the API change is pushed somewhere!

00JCIV00 commented 1 year ago

Made this change on the working v0.9.0 branch in the commit seen above (44c16c2).

Small note, I'm still working on the main features for that release. Namely, Manpage and Tab Completion Script generation. Those features both currently work (for single Command Manpages and Bash Tab Completion), but they're a work in progress so there may be breaking changes with them. That shouldn't affect the rest of the library that you're already using though.

p7r0x7 commented 1 year ago

Help and usage are subcommands that are both default and fairly uncustomizable, that's what I meant to ask for adjustments on. To clarify. 🤗

00JCIV00 commented 1 year ago

Ahh, I think I see what you mean. Looking under the help() method of Command.Custom there are several hard-coded strings that could be pulled up to the Config instead. I'm guessing it's probably the same for usage(). I'll work on moving those up when I get a chance.

If you have more specific configurations in mind let me know!

00JCIV00 commented 1 year ago

Help and usage are subcommands that are both default and fairly uncustomizable, that's what I meant to ask for adjustments on. To clarify. 🤗

@p7r0x7 Whenever you get a chance, please check out a build with the two commits referenced above (5cec1a8 and 62c7b55). The first allows the previously hardcoded strings in help() and usage() to be set via the Configs. The second allows for completely customized help() and/or usage() functions to be set in Command.Config similar to the parse_fn and valid_fn fields in Value.Typed.

p7r0x7 commented 1 year ago

Help and usage are subcommands that are both default and fairly uncustomizable, that's what I meant to ask for adjustments on. To clarify. 🤗

@p7r0x7 Whenever you get a chance, please check out a build with the two commits referenced above (5cec1a8 and 62c7b55). The first allows the previously hardcoded strings in help() and usage() to be set via the Configs. The second allows for completely customized help() and/or usage() functions to be set in Command.Config similar to the parse_fn and valid_fn fields in Value.Typed.

How exciting!

p7r0x7 commented 1 year ago

I have seen much more configuration options made available, but I don't see a clean way to disable the usage and help menus from being discrete subcommands if I wanted to; at this point I'm able to do just about everything I expected to be able to do with Cova, but I'm trying to cover all bases because I really want to see this project become popular

p7r0x7 commented 1 year ago

It's possible I've just missed something, but I'm also trying to eliminate the section where you've included the option name that gets rendered as "Option name:"

Since I've made the flags themselves rather descriptive, and because below them in the description I have very detailed notes, I'd like to omit the option name entirely.

PS, I was very excited to see the indent string handled properly. In general your coding style seems to be very thorough!

00JCIV00 commented 1 year ago

I have seen much more configuration options made available, but I don't see a clean way to disable the usage and help menus from being discrete subcommands if I wanted to; at this point I'm able to do just about everything I expected to be able to do with Cova, but I'm trying to cover all bases because I really want to see this project become popular

@p7r0x7 Your thorough review and usage of the library has been a huge help in making the library more user friendly. Just wanted to let you know I really appreciate it as always!

Regarding the disablement of help/usage sub Commands and Options, that's actually available in the Command.Custom.InitConfig using the add_help_cmds and add_help_opts fields respectively. That customization is done here because Initialization is when those Commands/Options are actually added. That said, I can see how that might be a little confusing or hard to find. At some point I'll look to make that clearer in the Guides. I'm also open to suggestions on what would have made it more clear to you when searching for that feature.

If you do remove the auto-generated help/usage, be sure to also adjust your cova.ParseConfig that's given to cova.parse(). Specifically, you'll need to set auto_handle_usage_help = false and err_reaction = .None.

Doing this should completely remove all of the help/usage stuff that's auto-generated.

00JCIV00 commented 1 year ago

It's possible I've just missed something, but I'm also trying to eliminate the section where you've included the option name that gets rendered as "Option name:"

Since I've made the flags themselves rather descriptive, and because below them in the description I have very detailed notes, I'd like to omit the option name entirely.

PS, I was very excited to see the indent string handled properly. In general your coding style seems to be very thorough!

I don't think you've missed anything here. Unfortunately, because format strings must be comptime known, it's hard to allow certain fields to be added or removed in a way that's both consistent and flexible. However, I can just add help_fn and usage_fn fields to the Option and Value Configs which will let you write your own custom function using whatever data you want from the given Argument Type (same as I did with Commands). This should be pretty simple to implement, but I might not be able to get to it for a few days.

When it is implemented, you'll be able to do exactly what you're looking for I think.

p7r0x7 commented 1 year ago

Everything but the supplied arguments themselves can be compiletime with the right implementation; though, I have no idea the implications of this nor an understanding of the complexity of its implementing

00JCIV00 commented 1 year ago

I decided to just go ahead and implement the custom help/usage functions while it was fresh in my head. Here's a quick demo of getting rid of the Option Names in their help messages:

// Your custom Command Type
pub const CommandT = Command.Custom(.{
    // Your custom Option Type
    .opt_config = .{ 
         .help_fn = struct{ 
             fn help(self: anytype, writer: anytype) !void { 
                 const indent_fmt = "    "; 
                 try self.usage(writer); 
                 try writer.print("\n{?s}{?s}{?s}{s}", .{ indent_fmt, indent_fmt, indent_fmt, self.description }); 
             } 
         }.help
     },
});
00JCIV00 commented 1 year ago

Everything but the supplied arguments themselves can be compiletime with the right implementation; though, I have no idea the implications of this nor an understanding of the complexity of its implementing

True, but those arguments are the issue I think. In order for the library user to provide those arguments, they need the full context of the Argument Type (in your case, the Option). That's where the custom functions come in. They allow anyone to create a callback function for help/usage that's treated as a "1st-class citizen" within the rest of the library, complete with the required context to print only what's desired.

p7r0x7 commented 1 year ago

complete with the required context to print only what's desired.

I think only this last phrase is what prevents what I'm suggesting to be compiletime

00JCIV00 commented 1 year ago

complete with the required context to print only what's desired.

I think only this last phrase is what prevents what I'm suggesting to be compiletime

The custom help/usage functions are still created at compile time (they actually have to be as far as I know). They also just happen to be the most straightforward way to provide that context that I can think of.

Does the above example work for what you're trying to do?

p7r0x7 commented 1 year ago

I'll let you know if it doesn't!

p7r0x7 commented 1 year ago

I'm sorry, how do you write an option usage_fn?

00JCIV00 commented 1 year ago

Here's a simple example of a usage_fn and help_fn I just made for a project's Option Type:

    .opt_config = .{
        //.usage_fmt = "{c}{?c}, {s}{?s} <{s} ({s})>",
        .usage_fn = struct{
            fn usage(self: anytype, writer: anytype, _: mem.Allocator) !void {
                const short_prefix = @TypeOf(self.*).short_prefix;
                const long_prefix = @TypeOf(self.*).long_prefix;
                try writer.print("{?c}{?c}, {?s}{?s}", .{
                        short_prefix,
                        self.short_name,
                        long_prefix,
                        self.long_name,
                    }
                );
            }
        }.usage,
        .help_fn = struct{
            fn help(self: anytype, writer: anytype, _: mem.Allocator) !void {
                const indent_fmt = @TypeOf(self.*).indent_fmt;
                try self.usage(writer);
                try writer.print("\n{?s}{?s}{?s}{s}", .{ indent_fmt, indent_fmt, indent_fmt, self.description });
            }
        }.help
    },

Note that the usage_fmt wouldn't do anything since the usage_fn takes precedence, so I commented it out.

p7r0x7 commented 1 year ago

So this is what I've come up with so far, and it seems to work well:

cli.zig

const std = @import("std");
const cova = @import("cova");
const io = @import("std").io;
const os = @import("std").os;
const fmt = @import("std").fmt;
const mem = @import("std").mem;
const ascii = @import("std").ascii;
const builtin = @import("builtin");

const blurple = "\x1b[38;5;111m";
const butter = "\x1b[38;5;230m";
const zero = "\x1b[0m";

/// Cova configuration type identity
pub const CommandT = cova.Command.Custom(.{
    .indent_fmt = "    ",
    .subcmds_help_fmt = "{s}:\t" ++ butter ++ "{s}" ++ zero,
    .opt_config = .{
        .usage_fn = struct {
            pub fn usage(self: anytype, writer: anytype, _: mem.Allocator) !void {
                const child = self.val.childType();
                const val_name = self.val.name();
                try writer.print("{?s}{?s} " ++ butter ++ "\"{s}{s}({s})\"" ++ zero, .{
                    @TypeOf(self.*).long_prefix orelse "",
                    self.long_name orelse "",
                    val_name,
                    if (val_name.len > 0) " " else "",
                    child,
                });
                if (mem.eql(u8, child, "bool")) {
                    const val: ?bool = self.val.generic.bool.default_val;
                    if (val) |v| try writer.print(" default: {any}", .{v});
                } else if (mem.eql(u8, child, "[]const u8")) {
                    const val: ?[]const u8 = self.val.generic.string.default_val;
                    if (val) |v| try writer.print(" default: {any}", .{v});
                }
            }
        }.usage,
        .help_fn = struct {
            pub fn help(self: anytype, writer: anytype, _: mem.Allocator) !void {
                try self.usage(writer);
                try writer.print(
                    "\n{s}{s}" ++ blurple ++ "{s}" ++ zero ++ "\n",
                    .{ @TypeOf(self.*).indent_fmt orelse "", @TypeOf(self.*).indent_fmt orelse "", self.description },
                );
            }
        }.help,
        .allow_abbreviated_long_opts = false,
        .allow_opt_val_no_space = true,
        .opt_val_seps = "=:",
        .short_prefix = null,
        .long_prefix = "-",
    },
    .val_config = .{
        .vals_help_fmt = "{s} ({s}):\t" ++ butter ++ "{s}" ++ zero,
        .set_behavior = .Last,
        .arg_delims = ",;",
    },
});

fn subCommandOrCommand(cmd: []const u8, desc: []const u8, sub_cmds: ?[]const CommandT, opts: ?[]const CommandT.OptionT) CommandT {
    return .{ .name = cmd, .description = desc, .sub_cmds = sub_cmds, .opts = opts };
}

fn boolOption(opt: []const u8, default: bool, desc: []const u8) CommandT.OptionT {
    return .{
        .name = opt,
        .long_name = opt,
        .description = blurple ++ desc ++ zero,
        .val = CommandT.ValueT.ofType(bool, .{
            .name = "",
            .default_val = default,
            .parse_fn = struct {
                pub fn parseBool(arg: []const u8, _: mem.Allocator) !bool {
                    const T = [_][]const u8{ "1", "true", "t", "yes", "y" };
                    const F = [_][]const u8{ "0", "false", "f", "no", "n" };
                    for (T) |str| if (ascii.eqlIgnoreCase(str, arg)) return true;
                    for (F) |str| if (ascii.eqlIgnoreCase(str, arg)) return false;
                    return error.InvalidBooleanValue;
                }
            }.parseBool,
        }),
    };
}

fn containerAndPathOption(opt: []const u8, val: []const u8, desc: []const u8) CommandT.OptionT {
    return .{
        .name = opt,
        .long_name = opt,
        .description = desc,
        .val = CommandT.ValueT.ofType([]const u8, .{
            .name = val ++ "_path",
            .parse_fn = struct {
                pub fn parsePath(arg: []const u8, _: mem.Allocator) ![]const u8 {
                    os.access(arg, os.F_OK) catch |err| {
                        // Windows doesn't make stdin/out/err available via system path,
                        // so this will have to be handled outside Cova
                        if (mem.eql(u8, arg, "-")) return arg;
                        return err;
                    };
                    return arg;
                }
            }.parsePath,
        }),
    };
}

fn deadlineAndPixelOption(opt: []const u8, val: []const u8, desc: []const u8) CommandT.OptionT {
    _ = desc;
    _ = val;
    _ = opt;
}

fn gopOption(opt: []const u8, default: u32, desc: []const u8) CommandT.OptionT {
    _ = desc;
    _ = default;
    _ = opt;
}

const vpxl_cmd = subCommandOrCommand(
    "vpxl",
    "a VP9 encoder by Matt R Bonnette",
    &.{
        subCommandOrCommand("xpsnr", "Calculate XPSNR score between two or more uncompressed inputs.", null, &.{
            containerAndPathOption("mkv", "input", "Path from which uncompressed frames (in the Matroska format) are to be read."),
            containerAndPathOption("y4m", "input", "Path from which uncompressed frames (in the YUV4MPEG2 format) are to be read."),
            containerAndPathOption("yuv", "input", "Path from which uncompressed frames (in plain YUV format) are to be read."),
        }),
        subCommandOrCommand("fssim", "Calculate FastSSIM score between two or more uncompressed inputs.", null, &.{
            containerAndPathOption("mkv", "input", "Path from which uncompressed frames (in the Matroska format) are to be read."),
            containerAndPathOption("y4m", "input", "Path from which uncompressed frames (in the YUV4MPEG2 format) are to be read."),
            containerAndPathOption("yuv", "input", "Path from which uncompressed frames (in plain YUV format) are to be read."),
        }),
    },
    &.{
        containerAndPathOption("mkv", "input",
            \\Path from which uncompressed frames (in the Matroska format) are to be read; mutually
            \\        exclusive with -y4m and -yuv.
        ),
        containerAndPathOption("y4m", "input",
            \\Path from which uncompressed frames (in the YUV4MPEG2 format) are to be read; mutually
            \\        exclusive with -mkv and -yuv.
        ),
        containerAndPathOption("yuv", "input",
            \\Path from which uncompressed frames (in plain YUV format) are to be read; mutually
            \\        exclusive with -mkv and -y4m.
        ),
        containerAndPathOption("webm", "output",
            \\Path to which compressed VP9 frames are to be written; mutually exclusive with -ivf.
        ),
        containerAndPathOption("ivf", "output",
            \\Path to which compressed VP9 frames are to be written; mutually exclusive with -webm.
        ),
        boolOption("resume", true,
            \\Don't be dummy and disable this, this is necessary for thine happiness <3.
        ),
    },
);

pub fn runVPXL(buffered: anytype, ally: mem.Allocator) !void {
    const bw = buffered.writer();
    const vpxl_cli = try vpxl_cmd.init(ally, .{});
    defer vpxl_cli.deinit();

    var arg_it = try cova.ArgIteratorGeneric.init(ally);
    defer arg_it.deinit();

    cova.parseArgs(&arg_it, CommandT, &vpxl_cli, bw, .{ .auto_handle_usage_help = false }) catch |err| switch (err) {
        error.TooManyValues,
        error.UnrecognizedArgument,
        error.UnexpectedArgument,
        error.CouldNotParseOption,
        => {},
        else => return err,
    };
    try vpxl_cli.help(bw);
    try buffered.flush();

    const in_fmt = try vpxl_cli.matchOpts(&.{ "mkv", "y4m", "yuv" }, .{ .logic = .XOR });
    _ = in_fmt;
    const out_fmt = try vpxl_cli.matchOpts(&.{ "webm", "ivf" }, .{ .logic = .XOR });
    _ = out_fmt;

    // Handle in_fmt and out_fmt
    if (builtin.mode == .Debug) try cova.utils.displayCmdInfo(CommandT, &vpxl_cli, ally, bw);
    try buffered.flush();
}

main.zig

const std = @import("std");
const io = @import("std").io;
const cli = @import("cli.zig");
const mem = @import("std").mem;
const heap = @import("std").heap;
const builtin = @import("builtin");

pub fn main() !void {
    // Heapspace Initialization
    const ally: mem.Allocator = ally: {
        if (builtin.mode == .Debug) {
            var gpa = heap.GeneralPurposeAllocator(.{}){};
            var arena = heap.ArenaAllocator.init(gpa.allocator());
            break :ally arena.allocator();
            // arena of gpa
        } else {
            var arena = heap.ArenaAllocator.init(heap.page_allocator);
            var sfa = heap.stackFallback(4 << 20, arena.allocator());
            break :ally sfa.get();
            // stack then arena of page
        }
    };

    // Connect to pipes & run VPXL's CLI
    var stderr = io.getStdErr().writer();
    var bw = io.bufferedWriter(stderr);
    try cli.runVPXL(&bw, ally);
}

However, I am concerned it would be a significant effort to adjust indenting to the command's help function (as I'd have to copy it over/rewrite it from your lib).

p7r0x7 commented 1 year ago

It currently outputs this:

USAGE: vpxl -mkv "input_path ([]const u8)" -y4m "input_path ([]const u8)" -yuv "input_path ([]const u8)" -webm "output_path ([]const u8)" -ivf "output_path ([]const u8)" -resume "(bool)" default: true -usage "usage_flag (bool)" -help "help_flag (bool)" | 'xpsnr' 'fssim' 'usage' 'help' 

HELP:
    COMMAND: vpxl

    DESCRIPTION: a VP9 encoder by Matt R Bonnette

    SUBCOMMANDS:
        xpsnr:  Calculate XPSNR score between two or more uncompressed inputs.
        fssim:  Calculate FastSSIM score between two or more uncompressed inputs.
        usage:  Show the 'vpxl' usage display.
        help:   Show the 'vpxl' help display.

    OPTIONS:
        -mkv "input_path ([]const u8)"
        Path from which uncompressed frames (in the Matroska format) are to be read; mutually
        exclusive with -y4m and -yuv.

        -y4m "input_path ([]const u8)"
        Path from which uncompressed frames (in the YUV4MPEG2 format) are to be read; mutually
        exclusive with -mkv and -yuv.

        -yuv "input_path ([]const u8)"
        Path from which uncompressed frames (in plain YUV format) are to be read; mutually
        exclusive with -mkv and -y4m.

        -webm "output_path ([]const u8)"
        Path to which compressed VP9 frames are to be written; mutually exclusive with -ivf.

        -ivf "output_path ([]const u8)"
        Path to which compressed VP9 frames are to be written; mutually exclusive with -webm.

        -resume "(bool)" default: true
        Don't be dummy and disable this, this is necessary for thine happiness <3.

        -usage "usage_flag (bool)"
        Show the 'vpxl' usage display.

        -help "help_flag (bool)"
        Show the 'vpxl' help display.

But I'm looking for something more like this:

USAGE
        vpxl [-mkv | -y4m | -yuv] [-webm | -ivf] [-resume]
        vpxl [xpsnr | fssim | usage | help]

HELP:
    COMMAND: vpxl

    DESCRIPTION: a VP9 encoder by Matt R Bonnette

SUBCOMMANDS
        xpsnr:  Calculate XPSNR score between two or more uncompressed inputs.
        fssim:  Calculate FastSSIM score between two or more uncompressed inputs.
        usage:  Show the 'vpxl' usage display.
        help:   Show the 'vpxl' help display.

OPTIONS
    -mkv "input_path ([]const u8)"
        Path from which uncompressed frames (in the Matroska format) are to be read; mutually
        exclusive with -y4m and -yuv.

    -y4m "input_path ([]const u8)"
        Path from which uncompressed frames (in the YUV4MPEG2 format) are to be read; mutually
        exclusive with -mkv and -yuv.

    -yuv "input_path ([]const u8)"
        Path from which uncompressed frames (in plain YUV format) are to be read; mutually
        exclusive with -mkv and -y4m.

    -webm "output_path ([]const u8)"
        Path to which compressed VP9 frames are to be written; mutually exclusive with -ivf.

    -ivf "output_path ([]const u8)"
        Path to which compressed VP9 frames are to be written; mutually exclusive with -webm.

    -resume "(bool)" default: true
        Don't be dummy and disable this, this is necessary for thine happiness <3.

    -usage "usage_flag (bool)"
        Show the 'vpxl' usage display.

    -help "help_flag (bool)"
        Show the 'vpxl' help display.
00JCIV00 commented 1 year ago

Yeah, looking at your desired output the only way to make that happen will be custom usage and help functions for your Command Type. That said, they shouldn't be particularly difficult to generate, but I do understand it might be a little tedious up front.

If you'd like help making that output, I can try and do a small example when I get some time.

p7r0x7 commented 1 year ago

If you'd like to, it'd be appreciated.

00JCIV00 commented 1 year ago

@p7r0x7

Apologies for the long delay. Give this a shot and tell me what you think. The only thing I really changed from the formatting you presented was the indentation, but you can can easily fix that if needed.

pub const CommandT = cova.Command.Custom(.{ 
    .global_usage_fn = struct{
        fn usage(self: anytype, writer: anytype, _: mem.Allocator) !void {
            const CmdT = @TypeOf(self.*);
            const OptT = CmdT.OptionT;
            const indent_fmt = CmdT.indent_fmt;
            var no_args = true;
            var pre_sep: []const u8 = "";

            try writer.print("USAGE\n", .{});
            if (self.opts) |opts| {
                no_args = false;
                try writer.print("{s}{s} [", .{ 
                    indent_fmt,
                    self.name,
                });
                for (opts) |opt| {
                    try writer.print("{s} {s}{s} ", .{ 
                        pre_sep,
                        OptT.long_prefix orelse opt.short_prefix,
                        opt.long_name orelse &.{ opt.short_name orelse 0 },
                    });
                    pre_sep = "| ";
                }
                try writer.print("]\n", .{});
            }
            if (self.sub_cmds) |cmds| {
                no_args = false;
                try writer.print("{s}{s} [", .{ 
                    indent_fmt,
                    self.name,
                });
                pre_sep = "";
                for (cmds) |cmd| {
                    try writer.print("{s} {s} ", .{ 
                        pre_sep,
                        cmd.name,
                    });
                    pre_sep = "| ";
                }
                try writer.print("]\n", .{});
            }
            if (no_args) try writer.print("{s}{s}{s}", .{ 
                indent_fmt, 
                indent_fmt,
                self.name,
            });
        }
    }.usage,
    .help_header_fmt = 
        \\HELP
        \\{s}COMMAND: {s}
        \\
        \\{s}DESCRIPTION: {s}
        \\
        \\
    ,
    .global_help_fn = struct{
        fn help(self: anytype, writer: anytype, _: mem.Allocator) !void {
            const CmdT = @TypeOf(self.*);
            const OptT = CmdT.OptionT;
            const indent_fmt = CmdT.indent_fmt;

            try writer.print("{s}\n", .{ self.help_prefix });
            try self.usage(writer);
            try writer.print("\n", .{});
            try writer.print(CmdT.help_header_fmt, .{ 
                indent_fmt, self.name, 
                indent_fmt, self.description 
            });

            if (self.sub_cmds) |cmds| {
                try writer.print("SUBCOMMANDS\n", .{});
                for (cmds) |cmd| {
                    try writer.print("{s}{s}: {s}\n", .{
                        indent_fmt,
                        cmd.name,
                        cmd.description, 
                    });
                }
                try writer.print("\n", .{});
            }
            if (self.opts) |opts| {
                try writer.print("OPTIONS\n", .{});
                for (opts) |opt| {
                    try writer.print(
                        \\{s}{s}{s} "{s} ({s})"
                        \\{s}{s}{s}
                        \\
                        \\
                        , .{
                            indent_fmt, 
                            OptT.long_prefix orelse OptT.short_prefix, opt.long_name orelse "", 
                            opt.val.name(), opt.val.childType(),
                            indent_fmt, indent_fmt,
                            opt.description, 
                        }
                    );
                }
            }
            if (self.vals) |vals| {
                try writer.print("VALUES\n", .{});
                for (vals) |val| {
                    try writer.print("{s}", .{ indent_fmt });
                    try val.usage(writer);
                    try writer.print("\n", .{});
                }
                try writer.print("\n", .{});
            }
        }
    }.help,
};
p7r0x7 commented 1 year ago

Okay, thanks, this has helped; I can close this now that I more fully understand what's going on and can agree that this issue has been resolved (not that I hadn't known that for awhile, but this issue exists such that there's record of the feature being tested).