GithubHelp home page GithubHelp logo

00jciv00 / cova Goto Github PK

View Code? Open in Web Editor NEW
87.0 4.0 3.0 68.65 MB

Commands, Options, Values, Arguments. A simple yet robust cross-platform command line argument parsing library for Zig.

Home Page: https://00jciv00.github.io/cova/

License: MIT License

Zig 100.00%
clap cli zig terminal zig-package ziglang command-line

cova's Introduction

cova_icon_v2 1

Commands Options Values Arguments

A simple yet robust cross-platform command line argument parsing library for Zig.

Static Badge Static Badge GitHub commit activity Static Badge


Overview

Cova is based on the idea that Arguments will fall into one of three types: Commands, Options, or Values. These Types are assembled into a single Command struct which is then used to parse argument tokens. Whether you're looking for simple argument parsing or want to create something as complex as the ip or git commands, Cova makes it easy.

Get Started Quickly!

Features

  • Comptime Setup. Runtime Use.
    • Cova is designed to have Argument Types set up at compile time so they can be validated during each compilation, thus providing you with immediate feedback.
    • Once validated, Argument Types are initialized to memory for runtime use where end user argument tokens are parsed then made ready to be analyzed by your code.
  • Build-time Bonuses! Cova also provides a simple build step to generate Help Docs, Tab Completion Scripts, and Argument Templates at build-time!
  • Simple Design:
    • All argument tokens are parsed to Argument Types: Commands, Options, or Values.
      • Options = Flags and Values = Positional Arguments
    • These Argument Types can be created from or converted to your Structs, Unions, and Functions along with their corresponding Fields and Parameters.
    • This design allows for infinitely nestable Commands, Options, and Values in a way that's simple to parse, analyze, and use in your projects.
  • Multiplatform. Tested across common architectures of Linux, Mac, and Windows.
  • Granular, Robust Customization:
    • POSIX Compliant by default, with plenty of ways to configure to whatever standard you'd like.
      • Ex: command --option option_string "standalone value" subcmd -i 42 --bool
    • Cova offers deep customization through the Argument Types and several Config Structs. These customizations all provide simple and predictable defaults, allowing you to only configure what you need.
  • And much more!

Usage

Cova makes it easy to set up your Argument Types at comptime and use the input provided by your end users at runtime!

Comptime Setup

There are two main ways to set up your Argument Types. You can either convert existing Zig Types within your project or create them manually. You can even mix and match these techniques to get the best of both!

Code Example
const std = @import("std");
const cova = @import("cova");
pub const CommandT = cova.Command.Base();
pub const OptionT = CommandT.OptionT;
pub const ValueT = CommandT.ValueT;

// The root Command for your program.
pub const setup_cmd: CommandT = .{
    .name = "basic-app",
    .description = "A basic user management application designed to highlight key features of the Cova library.",
    .cmd_groups = &.{ "INTERACT", "VIEW" },
    .sub_cmds = &.{
        // A Sub Command created from converting a Struct named `User`.
        // Usage Ex: `basic-app new -f Bruce -l Wayne -a 40 -p "555 555 5555" -A " 1007 Mountain Drive, Gotham" true`
        CommandT.from(User, .{
            .cmd_name = "new",
            .cmd_description = "Add a new user.",
            .cmd_group = "INTERACT",
            .sub_descriptions = &.{
                .{ "is_admin", "Add this user as an admin?" },
                .{ "first_name", "User's First Name." }, 
                .{ "last_name", "User's Last Name." },
                .{ "age", "User's Age." },
                .{ "phone", "User's Phone #." },
                .{ "address", "User's Address." },
            },
        }),
        // A Sub Command created from a Function named `open`.
        // Usage Ex: `basic-app open users.csv`
        CommandT.from(@TypeOf(open), .{
            .cmd_name = "open",
            .cmd_description = "Open or create a users file.",
            .cmd_group = "INTERACT",
        }),
        // A manually created Sub Command, same as the root `setup_cmd`.
        // Usage Ex: `basic-app clean` or `basic-app delete --file users.csv`
        CommandT{
            .name = "clean",
            .description = "Clean (delete) the default users file (users.csv) and persistent variable file (.ba_persist).",
            .alias_names = &.{ "delete", "wipe" },
            .cmd_group = "INTERACT",
            .opts = &.{
                OptionT{
                    .name = "clean_file",
                    .description = "Specify a single file to be cleaned (deleted) instead of the defaults.",
                    .alias_long_names = &.{ "delete_file" },
                    .short_name = 'f',
                    .long_name = "file",
                    .val = ValueT.ofType([]const u8, .{
                        .name = "clean_file",
                        .description = "The file to be cleaned.",
                        .alias_child_type = "filepath",
                        .valid_fn = cova.Value.ValidationFns.validFilepath,
                    }),
                },
            },
        },
    }
};
// Continue to Runtime Use...

Runtime Use

Once Cova has parsed input from your end users it puts that data into the Command you set up. You can call various methods on the Command to use that data however you need.

Code Example
// ...continued from the Comptime Setup.
pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{ .verbose_log = builtin.mode == .Debug }){};
    const alloc = gpa.allocator();

    // Initializing the `setup_cmd` with an allocator will make it available for Runtime use.
    const main_cmd = try setup_cmd.init(alloc, .{}); 
    defer main_cmd.deinit();

    // Parsing
    var args_iter = try cova.ArgIteratorGeneric.init(alloc);
    defer args_iter.deinit();
    const stdout = std.io.getStdOut().writer();

    cova.parseArgs(&args_iter, CommandT, &main_cmd, stdout, .{}) catch |err| switch (err) {
        error.UsageHelpCalled,
        error.TooManyValues,
        error.UnrecognizedArgument,
        error.UnexpectedArgument,
        error.CouldNotParseOption => {},
        else => return err,
    };

    // Analysis (Using the data.)
    if (builtin.mode == .Debug) try cova.utils.displayCmdInfo(CommandT, &main_cmd, alloc, &stdout);
    
    // Glossing over some project variables here.

    // Convert a Command back into a Struct.
    if (main_cmd.matchSubCmd("new")) |new_cmd| {
        var new_user = try new_cmd.to(User, .{});
        new_user._id = getNextID();
        try users.append(new_user);
        try stdout.print("Added:\n{s}\n", .{ new_user });
    }
    // Convert a Command back into a Function and call it.
    if (main_cmd.matchSubCmd("open")) |open_cmd| {
        user_file = try open_cmd.callAs(open, null, std.fs.File);
    }
    // Get the provided sub Command and check an Option from that sub Command.
    if (main_cmd.matchSubCmd("clean")) |clean_cmd| cleanCmd: {
        if ((try clean_cmd.getOpts(.{})).get("clean_file")) |clean_opt| {
            if (clean_opt.val.isSet()) {
                const filename = try clean_opt.val.getAs([]const u8);
                try delete(filename);
                break :cleanCmd;
            }
        }
        try delete("users.csv");
        try delete(".ba_persist");
    }
}

More Examples

  • basic-app: Where the above examples come from.
  • covademo: This is the testbed for Cova, but its a good demo of virtually every feature in the library.

Build-time Bonuses

Cova's simple Meta Doc Generator build step lets you quickly and easily generate documents in the following formats based on the Commands you set up at comptime:

  • Help Docs:
    • Manpages
    • Markdown
  • Tab Completion Scripts:
    • Bash
    • Zsh
    • Powershell
  • Argument Templates:
    • JSON
    • KDL
Code Example
// Within 'build.zig'
pub fn build(b: *std.Build) void {
    // Set up your build variables as normal.

    const cova_dep = b.dependency("cova", .{ .target = target, .optimize = optimize });
    const cova_mod = cova_dep.module("cova");

    // Set up your exe step as you normally would.

    const cova_gen = @import("cova").addCovaDocGenStep(b, cova_dep, exe, .{
        .kinds = &.{ .all },
        .version = "0.10.0",
        .ver_date = "06 APR 2024",
        .author = "00JCIV00",
        .copyright = "MIT License",
    });
    const meta_doc_gen = b.step("gen-meta", "Generate Meta Docs using Cova");
    meta_doc_gen.dependOn(&cova_gen.step);
}

Demo

cova_demo

Alternatives

cova's People

Contributors

00jciv00 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

cova's Issues

Create Enums from Commands

Add SubCommandsEnum() method to Command.Custom to create enums from Commands that can be used for Argument Type analysis by the library user after parsing.

v0.7.0-beta Pre-Public Release Goals

  • Implement basic Argument Parsing for Commands, Options, and Values.
  • Advanced Parsing:
    • Handle '=' instead of ' ' between an Option and its Value.
    • Handle the same Option given multiple times. (Currently takes last value.)
    • Handle no space ' ' between a Short Option and its Value.
    • Abbreviated Long Option parsing (i.e. '--long' working for '--long-opt').
  • Parsing Customization:
    • Mandate Values be filled.
    • Custom prefixes for Options.
    • Custom separator between Options and Values.
    • Choose behavior for having the same option given multiple times.
    • Choose whether or not to skip the first Argument (the executable's name).
  • Setup Features:
    • Set up the build.zig and build.zig.zon for install and use in other codebases.
    • Proper library tests.
    • Initialization Custom() methods for Commands and Options.
      • Setup in Comptime. Use in Runtime.
      • Validate unique sub Commands, Options, and Values.
      • Generate Usage/Help sub Commands.
      • Generate Usage/Help Options.
      • User formatting options for Usage/Help messages.
    • Generate Commands from a struct and vice versa.
      • Compatible nullable fields become Options.
      • Compatible non-nullable fields become Values.

Option Termination Configuration.

/// Enable Option Termination using `--` per the POSIX standard (or whatever symbol is chosen for Option long names).
enable_opt_termination: bool = true,

a) This comment is ambiguous if you don't already understand the POSIX standard here.
b) As discussed elsewhere, this option terminator could be made configurable to support any string.

Auto-handle Usage/Help messages during parsing

The cova.ParseConfig struct and cova.parseArgs() function should allow library users to handle Usage/Help messages during parsing automatically instead of manually analyzing and handling those calls after parsing.

Further format configuration options?

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!

Implement Initial Tab-Completion

Tab-completion scripts should be generated by the Cova library at comptime for Bash, Zsh, and PowerShell at a minimum. These initial scripts will only handle static completion of Argument Types which are all known at comptime.

Add Option Parsing Termination

Per the POSIX standard, Options (and Commands?) should no longer be parsed after -- is given by itself. Instead, all arguments after that should be treated as Values.

The termination symbol should be configurable via cova.ParseConfig, but default to the symbol used for long name Options.

Generate Command Tree in an easily parsable format.

Per talks with @matu3ba:

  • The list should be easily parsable for all shell languages for selection by the user as shell script (for now) with listing most relevant items first.
  • The idea is to keep it compatible to shell scripts for completion, but also enable usage outside of shell scripts.

The easiest way to do this up front will be JSON, but other formats should be explored down the road.

Revamp Value.zig Argument Type

In order to make Values more consistent with Commands and Options, they should be given a Value.Config struct and a Value.Custom() function that wraps the Value .Generic union and Value.Typed structs.

Additionally, with this change, Value.Generic can be customized and generated at comptime to include more Value.Typed types as the library user sees fit.

Improve Library Testing

Every public function in the library should be tested either individually or in a group similar to how the tests in src/cova work. Additionally this testing should be conducted across multiple targets.

  • Stratify tests into a separate file (or series of files)?
  • Group common/related function tests?
  • Use GitHub actions to test across multiple targets and provide that info directly on the README.

What's going wrong here? std.fmt usage error

const std = @import("std");
const cova = @import("cova");
const os = @import("std").os;
const mem = @import("std").mem;
const ascii = @import("std").ascii;
const builtin = @import("builtin");
pub usingnamespace @import("cova"); // Forward namespaces from the original module

const blue = "\x1b[34m";
const yell = "\x1b[93m";
const zero = "\x1b[0m";

/// Cova configuration type identity
pub const CommandT = cova.Command.Custom(.{
    .subcmds_help_fmt = "{s}:\t" ++ blue ++ "{s}" ++ zero,
    .val_config = .{
        .vals_help_fmt = "{s} ({s}):\t" ++ blue ++ "{s}" ++ zero,
        .set_behavior = .Last,
        .arg_delims = ",;",
    },
    .opt_config = .{
        .help_fn = struct {
            fn help(self: anytype, writer: anytype) !void {
                try self.usage(writer);
                try writer.print(
                    "\n{?s}{?s}{s}",
                    .{ CommandT.indent_fmt, CommandT.indent_fmt, self.description },
                );
            }
        }.help,
        .usage_fmt = "{c}{?c}{s}{?s} " ++ yell ++ "\"{s}({s})\"" ++ zero,
        .allow_abbreviated_long_opts = false,
        .allow_opt_val_no_space = true,
        .opt_val_seps = "=:",
        .short_prefix = null,
        .long_prefix = "-",
    },
    .indent_fmt = "    ",
});

///
pub const setup_cmd: CommandT = .{
    .name = "vpxl",
    .description = "a VP9 encoder by Matt R Bonnette",
    .opts = &.{
        pathOption("mkv", "input_path", ""),
        pathOption("y4m", "input_path", ""),
        pathOption("yuv", "input_path", ""),
        pathOption("webm", "output_path", ""),
        pathOption("ivf", "output_path", ""),
        boolOption("resume", "don't be dummy and disable this, this is necessary for thine happiness <3"),
    },
};

pub fn boolOption(comptime name: []const u8, comptime description: []const u8) CommandT.OptionT {
    return .{
        .name = name,
        .long_name = name,
        .description = description,
        .val = CommandT.ValueT.ofType(bool, .{ .name = "", .parse_fn = struct {
            pub fn parseBool(arg: []const u8) !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 }),
    };
}

pub fn pathOption(comptime name: []const u8, comptime val: []const u8, comptime description: []const u8) CommandT.OptionT {
    return .{
        .name = name,
        .long_name = name,
        .description = description,
        .val = CommandT.ValueT.ofType([]const u8, .{ .name = val ++ " ", .parse_fn = struct {
            pub fn parsePath(arg: []const u8) ![]const u8 {
                os.access(arg, os.F_OK) catch |err| {
                    // No way to return a path to stdin/out/err on Windows, so this will have to be handled outside Cova
                    if (mem.eql(u8, arg, "-")) return arg;
                    return err;
                };
                return arg;
            }
        }.parsePath }),
    };
}
p7r0x7@Peroxide-2 ~/D/vpxl> zig build run -- -ivf=-l -y4m:- -resume=1 -help
zig build-exe vpxl Debug native: error: the following command failed with 1 compilation errors:
/Users/p7r0x7/zig/zig build-exe /Users/p7r0x7/Documents/vpxl/src/main.zig -I/opt/homebrew/Cellar/zimg/3.0.5/include -L/opt/homebrew/Cellar/zimg/3.0.5/lib -lzimg --cache-dir /Users/p7r0x7/Documents/vpxl/zig-cache --global-cache-dir /Users/p7r0x7/.cache/zig --name vpxl --mod cova::/Users/p7r0x7/.cache/zig/p/1220ba88327fe4535cd026593b3bb6393f1f89f9ff6c67a71fa9587660fa3f96e11b/src/cova.zig --deps cova --listen=- 
Build Summary: 0/5 steps succeeded; 1 failed (disable with --summary none)
run transitive failure
└─ run vpxl transitive failure
   ├─ zig build-exe vpxl Debug native 1 errors
   └─ install transitive failure
      └─ install vpxl transitive failure
         └─ zig build-exe vpxl Debug native (reused)
/Users/p7r0x7/zig/lib/std/fmt.zig:459:5: error: invalid format string 's' for type 'Value.Custom(.{.set_behavior = .Last, .arg_delims = ",;", .custom_types = .{  }, .use_custom_bit_width_range = false, .min_int_bit_width = 1, .max_int_bit_width = 256, .help_fn = null, .usage_fn = null, .indent_fmt = "    ", .vals_usage_fmt = "\"{s} ({s})\"", .vals_help_fmt = "{s} ({s}):\t\x1b[34m{s}\x1b[0m"})'
    @compileError("invalid format string '" ++ fmt ++ "' for type '" ++ @typeName(@TypeOf(value)) ++ "'");
    ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
referenced by:
    formatType__anon_6139: /Users/p7r0x7/zig/lib/std/fmt.zig:566:53
    format__anon_6127: /Users/p7r0x7/zig/lib/std/fmt.zig:184:23
    remaining reference traces hidden; use '-freference-trace' to see all reference traces
p7r0x7@Peroxide-2 ~/D/vpxl [2]>

Possible upgrade?

I noticed you're using std.mem in your library. Have you thought about hand rolling it? I think this would help add a uniqueness to your code base that does not exist in other libraries. Essentially, make your code so you have no imports from the std library. Everything from scratch. Think about it.

Allow Fields with default values to be Options

When converting a Struct or Union, Fields with default values should be allowed to be Options. This should be configurable via the Command.Config with an allow_default_val_opts Field.

Rename Callback Functions

Rename callback functions (and potentially other fields) that can be set via both a Config Struct and directly on an instance of an Argument Type:

  1. No prefix: This is reserved for fields set directly on the instance of an Argument Type.
  2. child_type_ prefix: This will be for fields in a Config Struct that are set based on a specific Child Type (Options and Values only).
  3. global_ prefix: This will be for fields in a Config Struct that are set for ALL instances of the corresponding Argument Type. Think of it as changing the default behavior for that Argument Type.

When being processed the order of precedence will be as listed above ending in the default behavior of Cova.

The intent here is to make the usage of these callback functions and fields a bit more clear without needing to refer to the Docs as often.

Value type aliases.

I think dropping the requirement that the input and return values of parse_fn share the same type would lead to a more efficient/flexible CLI framework, but it would increase the code size.

I can show it's benefit here:

const std = @import("std");
const cova = @import("cova");
const os = @import("std").os;
const mem = @import("std").mem;
const ascii = @import("std").ascii;
const builtin = @import("builtin");
pub usingnamespace @import("cova"); // Forward namespaces from the original module

const blue = "\x1b[34m";
const yell = "\x1b[93m";
const zero = "\x1b[0m";

/// Cova configuration type identity
pub const CommandT = cova.Command.Custom(.{
    .subcmds_help_fmt = "{s}:\t" ++ blue ++ "{s}" ++ zero,
    .val_config = .{
        .vals_help_fmt = "{s} ({s}):\t" ++ blue ++ "{s}" ++ zero,
        .set_behavior = .Last,
        .arg_delims = ",;",
    },
    .opt_config = .{
        .help_fn = struct {
            fn help(self: anytype, writer: anytype) !void {
                try self.usage(writer);
                try writer.print(
                    "\n{?s}{?s}{s}",
                    .{ CommandT.indent_fmt, CommandT.indent_fmt, self.description },
                );
            }
        }.help,
        .usage_fmt = "{c}{?c}{s}{?s} " ++ yell ++ "\"{s}({s})\"" ++ zero,
        .allow_abbreviated_long_opts = false,
        .allow_opt_val_no_space = true,
        .opt_val_seps = "=:",
        .short_prefix = null,
        .long_prefix = "-",
    },
    .indent_fmt = "    ",
});

///
pub const setup_cmd: CommandT = .{
    .name = "vpxl",
    .description = "a VP9 encoder by Matt R Bonnette",
    .opts = &.{
        pathOption("mkv", "input_path", ""),
        pathOption("y4m", "input_path", ""),
        pathOption("yuv", "input_path", ""),
        pathOption("webm", "output_path", ""),
        pathOption("ivf", "output_path", ""),
        boolOption("resume", "don't be dummy and disable this, this is necessary for thine happiness <3"),
    },
};

pub fn boolOption(comptime name: []const u8, comptime description: []const u8) CommandT.OptionT {
    return .{
        .name = name,
        .long_name = name,
        .description = description,
        .val = CommandT.ValueT.ofType(bool, .{ .name = "", .parse_fn = struct {
            pub fn parseBool(arg: []const u8) !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 }),
    };
}

pub fn pathOption(comptime name: []const u8, comptime val: []const u8, comptime description: []const u8) CommandT.OptionT {
    return .{
        .name = name,
        .long_name = name,
        .description = description,
        .val = CommandT.ValueT.ofType([]const u8, .{ .name = val ++ " ", .parse_fn = struct {
            pub fn parsePath(arg: []const u8) ![]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 }),
    };
}

where specifically parsePath() cannot perform all parsing actions due to this type limitation. I think adjustment could literally support any kind of value parsing from the commandline to the program directly! (reducing the CLI code bleeding into in the meat of your program)

Let me know what you think (and I promise in no way am I suggesting your project wasn't already wonderful, I'm just excited to see what I could do with this)

build.zig API changes for 0.13.0-dev.211+6a65561e3

Zig master no longer supports this syntax and requires b.path("path/relative/to/build.zig"), which returns a std.Build.LazyPath.

/Users/p7r0x7/.cache/zig/p/1220b4820dfc4510ad9018d4e9f0dabd1f5b4d90162dd32d213d655514c06d61347f/build.zig:14:33: error: no field named 'path' in union 'Build.LazyPath'
        .root_source_file = .{ .path = "src/cova.zig" },
                                ^~~~
/Users/p7r0x7/zig/lib/std/Build.zig:2133:22: note: union declared here
pub const LazyPath = union(enum) {
                     ^~~~~

Add `tokenize()` Function

This function should allow library users to tokenize argument strings into arguments. By default, it should follow the POSIX standard and effectively mimic getopt or getopt_long. If useful, customization can be added via a TokenizeConfig struct.

Default help doesn't adapt to cova.Command.Custom.FromConfig setting

I set .attempt_short_opts = false, when converting from a struct with cova.Command.Custom.from(). The resulting help output shows -null as the short option instead of excluding it from the help output. This happens on both 0.9.1 and current main branch. See here:

        Temperature:
            -null,--temperature <temperature (f16)>
            The 'temperature' Option of type '?f16'.

I did just realize that -u and -h are set to enabled by default, maybe the fact that they have short options but the others don't is where the formatting goes wrong?

Expanding valid boolean value types.

I believe that boolean flags should support any of 1, true, "yes", 0, false, or "no" as values, just as the character between flags and their values can be any of "", " ", or "=".

What do you think?

Command Config option to Mandate a Sub Command be used

Command.Config should include an option that mandates a Sub Command be called if one is available. This will not include Usage/Help Commands and will be ignored if the corresponding Usage/Help Options are called as well.

Completions Parsing Function

A new function, cova.genCompletions(), should generate completions for provided argument tokens based on a Command. Very similar to cova.parseArgs().

This can be used to generate completions in custom CLIs or in conjunction with Tab Completion Shell scripts.

Add config item to, instead of POSIX flagging, support single dash longform flags only

... instead of having the option to concatenate shortened flags behind a single dash.

This is useful for certain project designs, but not necessary for all, so POSIX is definitely the better default.

I'm currently writing a video encoder and I'm using your project as part of the frontend; if you've used ffmpeg you know why single-dashed longform flags are the way to go.

For clarity, I'm referring to something such as the following:

$ ffmpeg -i in.mkv -f yuv4mpegpipe - | vpxl -y4m=- -crf=20 -best -tune=butteraugli -ivf=out.ivf

Thank you for your consideration.

Add ability to group arguments together

Would it be possible to add a "help heading" flag that would allow for grouping different arguments together in autogenerated man pages? For instance, the example below would allow for logical separation of different arguments based on their functionality within my cli program.

Client Settings:
  -T, --timeout <SECONDS>
          Number of seconds before a client's request times out

          [default: 7]

  -r, --redirects
          Allow a client to follow redirects

  -H, --headers <HEADERS>
          Allow a client to specify HTTP headers


  -a, --user-agent <USER_AGENT>
          Allow a client to specify a User-Agent

          [default: "Mozilla/5.0 (Macintosh; Intel Mac OS X x.y; rv:42.0) Gecko/20100101 Firefox/42.0)"]

  -i, --insecure
          Disables TLS certificate validation in the client

Response filters:
  -s, --status-codes <STATUS_CODE>...
          Status Codes to include (allow list) (default: 200 204 301 302 307 308 401 403 405)

  -e, --exclude-status-codes <STATUS_CODE>...
          Status Codes to exclude (returns all status codes except the ones passed)

Hope this example makes sense. Keep up the great work. Love the library.

Allow Options to be required during parsing

After looking through several other CLI tools, it seems that what Cova calls Options are often not optional. While they should still be optional by default the following two additions will allow for greater flexibility:

  1. Add a mandatory field to a Options.
  2. Add an opt_group_mandatory field to Commands.

Found `std.mem.indexOf()` for you.

/// Find the Index of a Scalar or Slice `needle` within a Slice `haystack`. (Why is this not in std.mem?!?!? Did I miss it?)
pub fn indexOfEql(comptime T: type, haystack: []const T, needle: T) ?usize {
    switch (@typeInfo(T)) {
        .Pointer => |ptr| {
            for (haystack, 0..) |hay, idx| if (mem.eql(ptr.child, hay, needle)) return idx;
            return null;
        },
        inline else => return mem.indexOfScalar(T, haystack, needle),
    }
}

std.mem.indexOf points to std.mem.indexOfPos:

/// Uses Boyer-Moore-Horspool algorithm on large inputs; `indexOfPosLinear` on small inputs.
pub fn indexOfPos(comptime T: type, haystack: []const T, start_index: usize, needle: []const T) ?usize {
    if (needle.len > haystack.len) return null;
    if (needle.len < 2) {
        if (needle.len == 0) return start_index;
        // indexOfScalarPos is significantly faster than indexOfPosLinear

        return indexOfScalarPos(T, haystack, start_index, needle[0]);
    }

    if (!meta.trait.hasUniqueRepresentation(T) or haystack.len < 52 or needle.len <= 4)
        return indexOfPosLinear(T, haystack, start_index, needle);

    const haystack_bytes = sliceAsBytes(haystack);
    const needle_bytes = sliceAsBytes(needle);

    var skip_table: [256]usize = undefined;
    boyerMooreHorspoolPreprocess(needle_bytes, skip_table[0..]);

    var i: usize = start_index * @sizeOf(T);
    while (i <= haystack_bytes.len - needle_bytes.len) {
        if (i % @sizeOf(T) == 0 and mem.eql(u8, haystack_bytes[i .. i + needle_bytes.len], needle_bytes)) {
            return @divExact(i, @sizeOf(T));
        }
        i += skip_table[haystack_bytes[i + needle_bytes.len - 1]];
    }

    return null;
}

Create Commands from a Help Message

Introduce a new function for Commands called fromHelpMessage() (or similar) that allows users to create Commands from a string that's setup like a Help Message.

If possible, this Help Message should also be used as the basis for the Command's Help Format.

Implement `optimize()` Function

This will combine noFormats() and the new void technique used with allow_arg_indices.

It will also implement the void technique on other Argument fields.

Global command option to set the default format strings to "".

I use custom callbacks for everything, so I don't need these strings to exist in my binaries.
This chaos could be mitigated with a global command option to set the default format strings to "".

/// Cova configuration type identity
const CommandT = cova.Command.Custom(.{
    .cmd_alias_fmt = "",
    .help_header_fmt = "",
    .subcmd_alias_fmt = "",
    .subcmds_help_fmt = "",
    .subcmds_usage_fmt = "",
    .subcmds_help_title_fmt = "",
    .vals_help_title_fmt = "",
    .opts_help_title_fmt = "",
    .usage_header_fmt = "",
    .group_title_fmt = "",
    .group_sep_fmt = "",

    .indent_fmt = "    ",
    .global_help_prefix = "",
    .global_case_sensitive = false,
    .global_vals_mandatory = false,
    .global_sub_cmds_mandatory = false,
    .global_usage_fn = printers.commandUsage,
    .global_help_fn = printers.commandHelp,
    .opt_config = .{
        .help_fmt = "",
        .usage_fmt = "",
        .global_usage_fn = printers.optionUsage,
        .global_help_fn = printers.optionHelp,
        .allow_abbreviated_long_opts = false,
        .allow_opt_val_no_space = true,
        .opt_val_seps = "=:",
        .short_prefix = null,
        .long_prefix = "-",
    },
    .val_config = .{
        .help_fmt = "",
        .usage_fmt = "",
        .global_usage_fn = printers.valueUsage,
        .global_help_fn = printers.valueHelp,
        .global_set_behavior = .Last,
        .add_base_floats = false,
        .add_base_ints = false,
        .use_slim_base = true,
        .max_children = 1,
    },
});

Revamp Config Structs

Config options should align better with the Argument Type they affect instead of being crammed into cova.ParseConfig.

Mutually exclusive options in a mandatory set.

How might I implement mutually exclusive options within a set that must be passed at runtime? For example, every command to the program must include an input and an output option, but there are more than one to select for each kind.

I'm implementing an encoder than supports -mkv, -y4m, or -yuv inputs and -webm or -ivf outputs.

Allow Command Aliases

Command Aliases will allow for alternate strings to be used instead of a Command's Name during parsing.

  • Add as Command.Custom.aliases.
  • Validated against sub-Commands and their respective aliases.

Add Option Aliases

Option Aliases should apply to Option Long Names, enabling alternatives similar to Command Aliases.

Review the use of `const` and `@constCast` throughout the library

The use of @constCast is generally discouraged, but it's used throughout the library in the Command, Option, and Value types. There two main reasons for this:

  1. A desire to use const as a sort of equivalent to val in Kotlin, wherein the data can be made immutable to external users but still be mutated internally.
  2. A lack of understanding of how const works. Namely, the fact that it applies to memory directly instead of the data or Type. Because of this, the compiler can make assumptions about const data and even choose to move it to the rodata section of a program.

While the library ostensibly works fine in its current state, this is an item that's worth reviewing for completeness down the line.

I don't know what I'm doing wrong here and the error message is not helpful:

const std = @import("std");
const cova = @import("cova");
const io = @import("std").io;
const os = @import("std").os;
const mem = @import("std").mem;
const ascii = @import("std").ascii;
pub usingnamespace @import("cova"); // Forward namespaces from the original module

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 = .{
        .help_fn = struct {
            fn help(self: anytype, writer: anytype, _: mem.Allocator) !void {
                try self.usage(writer);
                try writer.print("\n{?s}{?s}{s}", .{
                    @TypeOf(self.*).indent_fmt,
                    @TypeOf(self.*).indent_fmt,
                    self.description,
                });
            }
        }.help,
        .usage_fn = struct {
            fn usage(self: anytype, writer: anytype, _: mem.Allocator) !void {
                const long_prefix = @TypeOf(self.*).long_prefix;
                try writer.print(
                    "{s}{?s} {s}\"{s}({s})\"{s} {s}",
                    .{ long_prefix, self.long_name, butter, self.val.name(), @TypeOf(self.*).val.valType(), zero, zero },
                );
            }
        }.usage,
        .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 = ",;",
    },
});

///
pub const vpxl_cmd = command(
    "vpxl",
    "a VP9 encoder by Matt R Bonnette", 
    &.{
        command("xpsnr", "calculate XPSNR score", null, null),
        command("fssim", "calculate FastSSIM score", null, null),
    },
    &.{ 
        pathOption("mkv", "input_path", ""),
        pathOption("y4m", "input_path", ""),
        pathOption("yuv", "input_path", ""),
        pathOption("webm", "output_path", ""),
        pathOption("ivf", "output_path", ""),
        boolOption("resume", true, "Don't be dummy and disable this, this is necessary for thine happiness <3."),
    },
);

fn command(comptime name: []const u8, comptime description: []const u8, comptime sub_cmds: ?[]CommandT, comptime opts: ?[]CommandT.OptionT) CommandT {
    return .{ .name = name, .description = blurple ++ description ++ zero, .sub_cmds = sub_cmds, .opts = opts };
}

fn boolOption(comptime name: []const u8, comptime default: ?bool, comptime description: []const u8) CommandT.OptionT {
    return .{
        .name = name,
        .long_name = name,
        .description = blurple ++ description ++ 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 pathOption(comptime name: []const u8, comptime val: []const u8, comptime description: []const u8) CommandT.OptionT {
    return .{
        .name = name,
        .long_name = name,
        .description = blurple ++ description ++ zero,
        .val = CommandT.ValueT.ofType([]const u8, .{
            .name = val ++ " ",
            .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,
        }),
    };
}
p7r0x7@Peroxide-2 ~/D/vpxl [2]> zig build -Drelease=true run -- -help
zig build-exe vpxl ReleaseSafe native: error: the following command failed with 1 compilation errors:
/Users/p7r0x7/zig/zig build-exe /Users/p7r0x7/Documents/vpxl/src/main.zig -I/opt/homebrew/Cellar/zimg/3.0.5/include -L/opt/homebrew/Cellar/zimg/3.0.5/lib -lzimg -OReleaseSafe --cache-dir /Users/p7r0x7/Documents/vpxl/zig-cache --global-cache-dir /Users/p7r0x7/.cache/zig --name vpxl --mod cova::/Users/p7r0x7/.cache/zig/p/122030184bbcae053f930edf0f30dd8482c4a7b6149e2fa20cb46e3a1214b6e354bf/src/cova.zig --deps cova --listen=- 
Build Summary: 0/5 steps succeeded; 1 failed (disable with --summary none)
run transitive failure
└─ run vpxl transitive failure
   ├─ zig build-exe vpxl ReleaseSafe native 1 errors
   └─ install transitive failure
      └─ install vpxl transitive failure
         └─ zig build-exe vpxl ReleaseSafe native (reused)
src/cova.zig:54:5: error: expected type '?[]Command.Custom(.{.opt_config = .{.val_config = .{.set_behavior = (enum), .arg_delims = .{ ... }, .custom_types = .{ ... }, .custom_parse_fns = null, .use_custom_bit_width_range = false, .min_int_bit_width = 1, .max_int_bit_width = 256, .help_fn = null, .usage_fn = null, .indent_fmt = null, .vals_usage_fmt = .{ ... }, .vals_help_fmt = .{ ... }}, .help_fn = (function 'help'), .usage_fn = (function 'usage'), .indent_fmt = null, .help_fmt = null, .usage_fmt = "[{c}{?c},{s}{?s} \"{s} ({s})\"]", .short_prefix = null, .long_prefix = "-", .allow_opt_val_no_space = true, .opt_val_seps = "=:", .allow_abbreviated_long_opts = false}, .val_config = .{.set_behavior = .Last, .arg_delims = ",;", .custom_types = .{  }, .custom_parse_fns = null, .use_custom_bit_width_range = false, .min_int_bit_width = 1, .max_int_bit_width = 256, .help_fn = null, .usage_fn = null, .indent_fmt = null, .vals_usage_fmt = "\"{s} ({s})\"", .vals_help_fmt = "{s} ({s}):\t\x1b[38;5;230m{s}\x1b[0m"}, .global_help_prefix = "", .help_fn = null, .usage_fn = null, .indent_fmt = "    ", .group_title_fmt = " {s}|{s}|\n", .group_sep_fmt = "{s}{s}\n", .help_header_fmt = "HELP:\n{s}COMMAND: {s}\n\n{s}DESCRIPTION: {s}\n\n", .cmd_alias_fmt = "{s}ALIAS(ES): {s}\n\n", .usage_header_fmt = "USAGE: {s} ", .subcmds_help_title_fmt = "{s}SUBCOMMANDS:\n", .opts_help_title_fmt = "{s}OPTIONS:\n", .vals_help_title_fmt = "{s}VALUES:\n", .subcmds_help_fmt = "{s}:\t\x1b[38;5;230m{s}\x1b[0m", .subcmds_usage_fmt = "'{s}'", .subcmd_alias_fmt = "[alias(es): {s}]", .max_args = 25, .sub_cmds_mandatory = true, .vals_mandatory = true})', found '*const [2]Command.Custom(.{.opt_config = .{.val_config = .{.set_behavior = (enum), .arg_delims = .{ ... }, .custom_types = .{ ... }, .custom_parse_fns = null, .use_custom_bit_width_range = false, .min_int_bit_width = 1, .max_int_bit_width = 256, .help_fn = null, .usage_fn = null, .indent_fmt = null, .vals_usage_fmt = .{ ... }, .vals_help_fmt = .{ ... }}, .help_fn = (function 'help'), .usage_fn = (function 'usage'), .indent_fmt = null, .help_fmt = null, .usage_fmt = "[{c}{?c},{s}{?s} \"{s} ({s})\"]", .short_prefix = null, .long_prefix = "-", .allow_opt_val_no_space = true, .opt_val_seps = "=:", .allow_abbreviated_long_opts = false}, .val_config = .{.set_behavior = .Last, .arg_delims = ",;", .custom_types = .{  }, .custom_parse_fns = null, .use_custom_bit_width_range = false, .min_int_bit_width = 1, .max_int_bit_width = 256, .help_fn = null, .usage_fn = null, .indent_fmt = null, .vals_usage_fmt = "\"{s} ({s})\"", .vals_help_fmt = "{s} ({s}):\t\x1b[38;5;230m{s}\x1b[0m"}, .global_help_prefix = "", .help_fn = null, .usage_fn = null, .indent_fmt = "    ", .group_title_fmt = " {s}|{s}|\n", .group_sep_fmt = "{s}{s}\n", .help_header_fmt = "HELP:\n{s}COMMAND: {s}\n\n{s}DESCRIPTION: {s}\n\n", .cmd_alias_fmt = "{s}ALIAS(ES): {s}\n\n", .usage_header_fmt = "USAGE: {s} ", .subcmds_help_title_fmt = "{s}SUBCOMMANDS:\n", .opts_help_title_fmt = "{s}OPTIONS:\n", .vals_help_title_fmt = "{s}VALUES:\n", .subcmds_help_fmt = "{s}:\t\x1b[38;5;230m{s}\x1b[0m", .subcmds_usage_fmt = "'{s}'", .subcmd_alias_fmt = "[alias(es): {s}]", .max_args = 25, .sub_cmds_mandatory = true, .vals_mandatory = true})'
    &.{
    ^
src/cova.zig:68:92: note: parameter type declared here
fn command(comptime name: []const u8, comptime description: []const u8, comptime sub_cmds: ?[]CommandT, comptime opts: ?[]CommandT.OptionT) CommandT {
                                                                                           ^~~~~~~~~~~
referenced by:
    main: src/main.zig:12:32
    callMain: /Users/p7r0x7/zig/lib/std/start.zig:583:32
    remaining reference traces hidden; use '-freference-trace' to see all reference traces
p7r0x7@Peroxide-2 ~/D/vpxl [2]>

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.