Skip to content

πŸ”§ The ultimate Zig library for seamless command line argument parsing.

License

Notifications You must be signed in to change notification settings

prajwalch/yazap

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

test pages-build-deployment

Yazap

Note

This branch targets the master branch of zig. See supported versions table.

The ultimate zig library for seamless command-line parsing. Effortlessly handles options, subcommands, and custom arguments with ease.

Inspired by clap-rs and andrewrk/ziglang: src-self-hosted/arg.zig

Supported versions table

yazap Zig
main master
0.5.1 0.12.0, 0.12.1 and 0.13.0
<= 0.5.0 Not supported to any

Key Features:

  • Options (short and long):

    • Providing values with =, space, or no space (-f=value, -f value, -fvalue).
    • Supports delimiter-separated values with = or without space (-f=v1,v2,v3, -fv1:v2:v3).
    • Chaining multiple short boolean options (-abc).
    • Providing values and delimiter-separated values for multiple chained options using = (-abc=val, -abc=v1,v2,v3).
    • Specifying an option multiple times (-a 1 -a 2 -a 3).
  • Positional arguments:

    • Supports positional arguments alongside options for more flexible command-line inputs. For example:
      • command <positional_arg>
      • command <arg1> <arg2> <arg3>
  • Nested subcommands:

    • Organize commands with nested subcommands for a structured command-line interface. For example:
      • command subcommand
      • command subcommand subsubcommand
  • Automatic help handling and generation

  • Custom Argument definition:

    • Define custom Argument types for specific application requirements.

Limitations:

  • Does not support delimiter-separated values using space (-f v1,v2,v3).
  • Does not support providing value and delimiter-separated values for multiple chained options using space (-abc value, -abc v1,v2,v3).

Installing

  1. Run the following command:
zig fetch --save git+https://github.com/prajwalch/yazap
  1. Add the following to build.zig:
const yazap = b.dependency("yazap", .{});
exe.root_module.addImport("yazap", yazap.module("yazap"));

Documentation

For detailed and comprehensive documentation, please visit this link.

Warning

The documentation site is currently broken, in the meantime check out the source code.

Building and Running Examples

The examples can be found here. To build all of them, run the following command on your terminal:

$ zig build examples

After the compilation finishes, you can run each example by executing the corresponding binary:

$ ./zig-out/bin/example_name

To view the usage and available options for each example, you can use -h or --help flag:

$ ./zig-out/bin/example_name --help

Usage

Initializing Yazap

To begin using yazap, the first step is to create an instance of App by calling App.init(allocator, "Your app name", "optional description"). This function internally creates a root command for your application.

var app = App.init(allocator, "myls", "My custom ls");
defer app.deinit();

Obtaining the Root Command

The App itself does not provide any methods for adding arguments to your command. Its main purpose is to initialize the library, to invoke the parser with necessary arguments, and to deinitilize the library.

To add arguments and subcommands, acquire the root command by calling App.rootCommand(). This gives you access to the core command of your application by returning a pointer to it.

var myls = app.rootCommand();

Adding Arguments

Once you have obtained the root command, you can proceed to add arguments and subcommands using the methods available in the Command. For a complete list of available methods, refer to the Command API documentation.

try myls.addArg(Arg.positional("FILE", null, null));
try myls.addArg(Arg.booleanOption("all", 'a', "Don't ignore the hidden directories"));
try myls.addArg(Arg.booleanOption("recursive", 'R', "List subdirectories recursively"));
try myls.addArg(Arg.booleanOption("one-line", '1', null));
try myls.addArg(Arg.booleanOption("size", 's', null));
try myls.addArg(Arg.booleanOption("version", null, null));

try myls.addArg(Arg.singleValueOption("ignore", 'I', null));
try myls.addArg(Arg.singleValueOption("hide", null, null));

try myls.addArg(Arg.singleValueOptionWithValidValues(
    "color",
    'C',
    "Colorize the output",
    &[_][]const u8{ "always", "auto", "never" }
));

Alternatively, you can add multiple arguments in a single function call using Command.addArgs():

try myls.addArgs(&[_]Arg {
    Arg.positional("FILE", null, null),
    Arg.booleanOption("all", 'a', "Don't ignore the hidden directories"),
    Arg.booleanOption("recursive", 'R', "List subdirectories recursively"),
    Arg.booleanOption("one-line", '1', null),
    Arg.booleanOption("size", 's', null),
    Arg.booleanOption("version", null, null),

    Arg.singleValueOption("ignore", 'I', null),
    Arg.singleValueOption("hide", null, null),

    Arg.singleValueOptionWithValidValues(
        "color",
        'C',
        "Colorize the output",
        &[_][]const u8{ "always", "auto", "never" }
    ),
});

Note that for any option that accepts value, you can set its value placeholder to display in the help message. If you don't set the placeholder, the option name will be displayed by default.

var ignore_opt = Arg.singleValueOption("ignore", 'I', null);
ignore_opt.setValuePlaceholder("PATTERN");

var hide_opt = Arg.singleValueOption("hide", null, null);
hide_opt.setValuesPlaceholder("PATTERN");

var color_opt = Arg.singleValueOptionWithValidValues(
    "color",
    'C',
    "Colorize the output",
    &[_][]const u8{ "always", "auto", "never" }
);
color_opt.setValuePlaceholder("WHEN");

try myls.addArgs(&[_]Arg{ ignore_opt, hide_opt, color_opt });

Adding Subcommands

To create a subcommand, use App.createCommand("name", "optional description") then you can add its own arguments and subcommands just like the root command. After you finish adding arguments, add it as a root subcommand by calling Command.addSubcommand().

var update_cmd = app.createCommand("update", "Update the app or check for new updates");
try update_cmd.addArg(Arg.booleanOption("check-only", null, "Only check for new update"));
try update_cmd.addArg(Arg.singleValueOptionWithValidValues(
    "branch",
    'b',
    "Branch to update",
    &[_][]const u8{ "stable", "nightly", "beta" }
));

try myls.addSubcommand(update_cmd);

Parsing Arguments

Once you have finished adding all the arguments and subcommands, call App.parseProcess() to start parsing the arguments given to the current process. This function internally utilizes std.process.argsAlloc to obtain the raw arguments. Alternatively, you can use App.parseFrom() and pass your own raw arguments, which can be useful during testing. Both functions returns ArgMatches.

const matches = try app.parseProcess();

if (matches.containsArg("version")) {
    log.info("v0.1.0", .{});
    return;
}

if (matches.getSingleValue("FILE")) |f| {
    log.info("List contents of {f}");
    return;
}

if (matches.subcommandMatches("update")) |update_cmd_matches| {
    if (update_cmd_matches.containsArg("check-only")) {
        std.log.info("Check and report new update", .{});
        return;
    }

    if (update_cmd_matches.getSingleValue("branch")) |branch| {
        std.log.info("Branch to update: {s}", .{branch});
        return;
    }
    return;
}

if (matches.containsArg("all")) {
    log.info("show all", .{});
    return;
}

if (matches.containsArg("recursive")) {
    log.info("show recursive", .{});
    return;
}

if (matches.getSingleValue("ignore")) |pattern| {
    log.info("ignore pattern = {s}", .{pattern});
    return;
}

if (matches.containsArg("color")) {
    const when = matches.getSingleValue("color").?;

    log.info("color={s}", .{when});
    return;
}

Handling Help

-h and --h flag is globally available to all the commands and subcommands and handled automatically when they are passed to command line. However, if you need to manually display the help message there are currently two ways to do it.

1. By invoking App.displayHelp() and App.displaySubcommandHelp().

App.displayHelp() displays the help message for the root command and other hand App.displaySubcommandHelp() displays the help message for the active subcommand.

For e.x.: if gh auth login were passed then App.displayHelp() would display the help for gh and App.displaySubcommandHelp() display the help for login.

Example:

if (!matches.containsArgs()) {
    try app.displayHelp();
    return;
}

if (matches.subcommandMatches("update")) |update_cmd_matches| {
    if (!update_cmd_matches.containsArgs()) {
        try app.displaySubcommandHelp();
        return;
    }
}

2. By setting .help_on_empty_args property to the command.

The .help_on_empty_args property which when set to a command, it instructs the handler to display the help message for that particular command when arguments are not provided. It behaves exactly like the code shown at the example above.

Example:

var app = App.init(allocator, "myls", "My custom ls");
defer app.deinit();

var myls = app.rootCommand();
myls.setProperty(.help_on_empty_args);

var update_cmd = app.createCommand("update", "Update the app or check for new updates");
update_cmd.setProperty(.help_on_empty_args);

try myls.addSubcommand(update_cmd);

const matches = try myls.parseProcess();

// --snip--

Putting it All Together

const std = @import("std");
const yazap = @import("yazap");

const allocator = std.heap.page_allocator;
const log = std.log;
const App = yazap.App;
const Arg = yazap.Arg;

pub fn main() anyerror!void {
    var app = App.init(allocator, "myls", "My custom ls");
    defer app.deinit();

    var myls = app.rootCommand();
    myls.setProperty(.help_on_empty_args);
    
    try myls.addArgs(&[_]Arg {
        Arg.positional("FILE", null, null),
        Arg.booleanOption("all", 'a', "Don't ignore the hidden directories"),
        Arg.booleanOption("recursive", 'R', "List subdirectories recursively"),
        Arg.booleanOption("one-line", '1', null),
        Arg.booleanOption("size", 's', null),
        Arg.booleanOption("version", null, null),
    });
    
    var ignore_opt = Arg.singleValueOption("ignore", 'I', null);
    ignore_opt.setValuePlaceholder("PATTERN");

    var hide_opt = Arg.singleValueOption("hide", null, null);
    hide_opt.setValuesPlaceholder("PATTERN");

    var color_opt = Arg.singleValueOptionWithValidValues(
        "color",
        'C',
        "Colorize the output",
        &[_][]const u8{ "always", "auto", "never" }
    );
    color_opt.setValuePlaceholder("WHEN");

    try myls.addArgs(&[_]Arg{ ignore_opt, hide_opt, color_opt });

    // Update subcommand.
    var update_cmd = app.createCommand("update", "Update the app or check for new updates");
    update_cmd.setProperty(.help_on_empty_args);

    try update_cmd.addArg(Arg.booleanOption("check-only", null, "Only check for new update"));
    try update_cmd.addArg(Arg.singleValueOptionWithValidValues(
        "branch",
        'b',
        "Branch to update",
        &[_][]const u8{ "stable", "nightly", "beta" }
    ));

    try myls.addSubcommand(update_cmd);

    // Get the parse result.
    const matches = try app.parseProcess();

    if (matches.containsArg("version")) {
        log.info("v0.1.0", .{});
        return;
    }

    if (matches.getSingleValue("FILE")) |f| {
        log.info("List contents of {f}");
        return;
    }

    if (matches.subcommandMatches("update")) |update_cmd_matches| {
        if (update_cmd_matches.containsArg("check-only")) {
            std.log.info("Check and report new update", .{});
            return;
        }

        if (update_cmd_matches.getSingleValue("branch")) |branch| {
            std.log.info("Branch to update: {s}", .{branch});
            return;
        }
        return;
    }

    if (matches.containsArg("all")) {
        log.info("show all", .{});
        return;
    }

    if (matches.containsArg("recursive")) {
        log.info("show recursive", .{});
        return;
    }

    if (matches.getSingleValue("ignore")) |pattern| {
        log.info("ignore pattern = {s}", .{pattern});
        return;
    }

    if (matches.containsArg("color")) {
        const when = matches.getSingleValue("color").?;

        log.info("color={s}", .{when});
        return;
    }
}

Alternate Parsers