Closed p7r0x7 closed 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!
I was thinking specifically about how the help and usage menu structure printout not being very customizable.
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
andvals_usage_fmt
should've been moved under theValue.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!
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.
Help and usage are subcommands that are both default and fairly uncustomizable, that's what I meant to ask for adjustments on. To clarify. 🤗
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!
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
.
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()
andusage()
to be set via the Configs. The second allows for completely customizedhelp()
and/orusage()
functions to be set inCommand.Config
similar to theparse_fn
andvalid_fn
fields inValue.Typed
.
How exciting!
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
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 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.
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.
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
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
},
});
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.
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
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?
I'll let you know if it doesn't!
I'm sorry, how do you write an option usage_fn?
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.
So this is what I've come up with so far, and it seems to work well:
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();
}
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).
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.
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.
If you'd like to, it'd be appreciated.
@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,
};
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).
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!