-
Notifications
You must be signed in to change notification settings - Fork 6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Further format configuration options? #27
Comments
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 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 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. |
Please let me know when the API change is pushed somewhere! |
- Moved Value Usage/Help formatters to `Value.Config`. - Added `usage()` and `help()` messages to `Value.Custom`. - Updated `Command.Custom` with these changes. - Updated the docs with these changes. - Closes #27
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 If you have more specific configurations in mind let me know! |
- Moved several hardcoded strings from `help()` and `usage()` to `Config` in `Command.Custom`. - Updated `Value` and `Option` with these changes. - Updated Docs with these changes. - #27
- Added `help_fn` and `usage_fn` fields to `Command.Config` to allow for custom Usage/Help functions that will override the default implementations. - Updated the Docs to reflect the new fields. - Closes #27
@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 |
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! |
@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 If you do remove the auto-generated help/usage, be sure to also adjust your Doing this should completely remove all of the help/usage stuff that's auto-generated. |
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 When it is implemented, you'll be able to do exactly what you're looking for I think. |
- Added `help_fn` and `usage_fn` to corresponding Configs. - #27
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
},
}); |
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. |
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 .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 |
So this is what I've come up with so far, and it seems to work well: cli.zigconst 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.zigconst 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:
But I'm looking for something more like this:
|
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. |
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!
The text was updated successfully, but these errors were encountered: