/cova

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

Primary LanguageZigMIT LicenseMIT

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.

Table of Contents

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.
  • 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.
  • 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 Types and Structs all provide simple and predictable defaults, allowing library users to only configure what they 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.

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

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 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 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 Command, same as the parent `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 the 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.

// ...continued from the Comptime Setup.
pub fn main() !void {
    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer arena.deinit();
    const alloc = arena.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 users_mal.append(alloc, new_user);
        var user_buf: [512]u8 = .{ 0 } ** 512;
        try user_file.writer().print("{s}\n", .{ try new_user.to(user_buf[0..]) });
        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

  • Simpler Example
  • 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.

Demo

cova_demo

Versions & Milestones

Documentation

Install

Package Manager

  1. Find the latest v#.#.# commit here.
  2. Copy the full SHA for the commit.
  3. Add the dependency to build.zig.zon:
zig fetch --save "https://github.com/00JCIV00/cova/archive/<GIT COMMIT SHA FROM STEP 2 HERE>.tar.gz"
  1. Add the dependency and module to build.zig:
// Cova Dependency
const cova_dep = b.dependency("cova", .{ .target = target });
// Cova Module
const cova_mod = cova_dep.module("cova");
// Executable
const exe = b.addExecutable(.{
    .name = "cova_example",
    .root_source_file = .{ .path = "src/main.zig" },
    .target = target,
    .optimize = optimize,
});
// Add the Cova Module to the Executable
exe.root_module.addImport("cova", cova_mod);

Package Manager - Alternative

Note, this method makes Cova easier to update by simply re-running zig fetch --save https://github.com/00JCIV00/cova/archive/[BRANCH].tar.gz. However, it can lead to non-reproducible builds because the url will always point to the newest commit of the provided branch. Details can be found in this discussion.

  1. Choose a branch to stay in sync with.
  • main is the latest stable branch.
  • The highest v#.#.# is the development branch.
  1. Add the dependency to build.zig.zon:
zig fetch --save https://github.com/00JCIV00/cova/archive/[BRANCH FROM STEP 1].tar.gz
  1. Continue from Step 4 above.

Build the Basic-App Demo from source

  1. Use the latest Zig (v0.12) for your system. Available here.
  2. Run the following in whichever directory you'd like to install to:
git clone https://github.com/00JCIV00/cova.git
cd cova
zig build basic-app
  1. Try it out!
cd bin
./basic-app help

Alternatives