Giter VIP home page Giter VIP logo

yazap's Introduction

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.

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, invoke the parser, and free associated structures. To add arguments and subcommands, you'll need to use the root command.

To obtain the root command, simply call App.rootCommand(), which returns a pointer to it. This gives you access to the core command of your application.

var myls = app.rootCommand();

Adding Arguments

Once you have obtained the root command, you can proceed to arguments using the provided methods 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', null, &[_][]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', null, &[_][]const u8{
        "always",
        "auto",
        "never",
    }),
});

Adding Subcommands

To create a subcommand, you can use App.createCommand("name", "optional description"). Once you have created a subcommand, you can add its own arguments and subcommands just like the root command then add it to the root command using 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 arguments and subcommands, call App.parseProcess() to start the parsing 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 return a constant pointer to 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

The handling of -h or --help option and the automatic display of usage information are taken care by the library. However, if you need to manually display the help information, there are two functions available: App.displayHelp() and App.displaySubcommandHelp().

  • App.displayHelp() prints the help information for the root command, providing a simple way to display the overall usage and description of the application.

  • On the other hand, App.displaySubcommandHelp() queries the sepecifed subcommand on the command line and displays its specific usage information.

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

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

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();

    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);

    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', null, &[_][]const u8{
        "always",
        "auto",
        "never",
    }));

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

    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.containsArgs()) {
            try app.displaySubcommandHelp();
            return;
        }

        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

yazap's People

Contributors

cdmistman avatar fardragon avatar higuoxing avatar joshua-software-dev avatar karlseguin avatar knnut avatar monomycelium avatar prajwalch avatar signaryk 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  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

yazap's Issues

Sub-commands with no values will never match

Given the following structure:

program foo bar
const matches = try app.parseProcess(); // works fine
const fooMatches = matches.subcommandMatches("foo"); // works fine
const barMatches = fooMatches.subcommandMatches("bar"); // fails

Although fooMatches.subcommand is non-null and .name is equal to bar, subcommandMatches still returns null because bar didn't have any arguments.

This is an issue with subcommandMatches, because it returns ?ArgMatches, even if foo has the bar sub-command, the method returns subcmd.matches which will be null if bar doesn't have any arguments or further sub-commands.

This behavior should be different. The easiest solution is to update this:

yazap/src/Parser.zig

Lines 444 to 446 in 55444b9

if (!takes_value) {
return MatchedSubCommand.init(subcmd.name, null);
}

If the sub-command doesn't take any value, arg_matches should be set to a non-null value instead of null. Probably some empty value.

Latest Zig, won't build

Taking out docs.emit_docs = .emit; from build.zig works, but not sure if that's the right/only solution.

Missing newline when logging `ParseError.CommandSubcommandNotProvided`

Hello,

First of all, thank you for the handy library!

I encountered a really minor thing to improve related to error logging. When the ParseError.CommandSubcommandNotProvided is logged, the \n is missed at the end of the log format in contrast to all other similar places.

try writer.print("The command '{s}' requires a subcommand but none is provided", .{self.getStrValue(.valid_cmd)});

It would be good to add a missing \n so that errors logging style is consistent.

Thanks.

Make a new release with new updates

This is already fixed on main, just make a release with it to be able to be used with zon

/.../build.zig:48:8: error: no field or member function named 'addModule' in 'Build.Step.Compile'
    exe.addModule(name, depend.module(name));
    ~~~^~~~~~~~~~
/usr/lib/zig/std/Build/Step/Compile.zig:1:1: note: struct declared here
const builtin = @import("builtin");
^~~~~
referenced by:
    build: /.../build.zig:28:5
    runBuild__anon_8996: /usr/lib/zig/std/Build.zig:2079:27
    remaining reference traces hidden; use '-freference-trace' to see all reference traces
/home/.../.cache/zig/p/12201e962f43c58af33e657d31402f403abb16a0ede31a3dc5a8f91dfb795ba0db5d/build.zig:3:21: error: root struct of file 'std' has no member named 'build'
pub fn build(b: *std.build.Builder) void {

Both examples fail to compile

Error:

> zig build run
/home/dh/zig/zig-linux-x86_64-0.10.0-dev.4176+6d7b0690a/lib/std/fmt.zig:498:17: error: cannot format optional without a specifier (i.e. {?} or {any})
                @compileError("cannot format optional without a specifier (i.e. {?} or {any})");
                ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
referenced by:
    format__anon_9521: /home/dh/zig/zig-linux-x86_64-0.10.0-dev.4176+6d7b0690a/lib/std/fmt.zig:183:13
    print__anon_9237: /home/dh/zig/zig-linux-x86_64-0.10.0-dev.4176+6d7b0690a/lib/std/io/writer.zig:28:27
    remaining reference traces hidden; use '-freference-trace' to see all reference traces

error: cat...
error: The following command exited with error code 1:
/home/dh/zig/zig-linux-x86_64-0.10.0-dev.4176+6d7b0690a/zig build-exe /home/dh/workspace/zigstuff/test/src/main.zig --cache-dir /home/dh/workspace/zigstuff/test/zig-cache --global-cache-dir /home/dh/.cache/zig --name cat --pkg-begin yazap /home/dh/workspace/zigstuff/test/libs/yazap/src/lib.zig --pkg-end --enable-cache
error: the following build command failed with exit code 1:
/home/dh/workspace/zigstuff/test/zig-cache/o/4cdeb5d65f9fe53c51b10b0e5c9af910/build /home/dh/zig/zig-linux-x86_64-0.10.0-dev.4176+6d7b0690a/zig /home/dh/workspace/zigstuff/test /home/dh/workspace/zigstuff/test/zig-cache /home/dh/.cache/zig run

Zig version:
0.10.0-dev.4176+6d7b0690a

Support passing positional arguments after options

It seems that the parser is confused if it encounters a boolean flag before the positional argument:

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

var gpa = std.heap.GeneralPurposeAllocator(.{
    .safety = true,
}){};

pub fn main() !void {
    const allocator = gpa.allocator();

    var app = yazap.App.init(allocator, "example", null);
    defer app.deinit();

    var root_command = app.rootCommand();

    try root_command.addArg(yazap.flag.boolean("example", null, null));
    try root_command.takesSingleValue("file");

    var args = try app.parseProcess();

    if (args.valueOf("file")) |file| {
        std.debug.print("got file: '{s}'\n", .{ file });
    }
}
$ ./example foo --example
got file: 'foo'
~/tmp/yazap-repro/zig-out/bin (me)
$ ./example --example foo
Unknown Command 'foo'
error: UnknownCommand
/home/me/tmp/yazap-repro/libs/yazap/src/Parser.zig:444:9: 0x25b893 in parseSubCommand (example)
        return Error.UnknownCommand;
        ^
/home/me/tmp/yazap-repro/libs/yazap/src/Parser.zig:109:17: 0x244209 in parse (example)
                try self.parseSubCommand(token.value),
                ^
/home/me/tmp/yazap-repro/libs/yazap/src/App.zig:60:9: 0x21d765 in parseFrom (example)
        return e;
        ^
/home/me/tmp/yazap-repro/libs/yazap/src/App.zig:50:5: 0x2181a0 in parseProcess (example)
    return self.parseFrom(self.process_args.?[1..]);
    ^
/home/me/tmp/yazap-repro/src/main.zig:19:16: 0x217b2e in main (example)
    var args = try app.parseProcess();

Add arbitrary subcommands

Hello there,

first of all, great project!
However i was wondering whether or not it would be a good addition to actually allow for arbitrary subcommands, as an example:

I have created a simple cli-tool that converts the first argument given to the program, like for example so:
./my_binary 1337 which then would parse the 1337 and i would be able to use said value.

I am not sure if this would be in scope of this project, but i think it would greatly benefit this library.
Also; i might be able to implement and contribute it myself, however i am not sure if this is in scope of this project. (hence writing an issue)

`CommandArgumentNotProvided` error when `argRequired` is set to false

Hi, I want to show a help message when no arguments are passed or the "-h" flag is passed.
I set the argRequired option to false, but it returned an error when no arguments passed.

env

  • yazap v0.2.0
  • Zig v0.9.1

Current behavior

...
var parser = Command.new(allocator, "sample");
defer parser.deinit();

var subcmd = Command.new(allocator, "init");
try subcmd.addArg(flag.boolean("help", 'h'));
try subcmd.takesSingleValue("FOOBAR");
subcmd.argRequired(false);
try parser.addSubcommand(subcmd);

var args = try parser.parseProcess();
defer args.deinit();
...
error(zigarg): The command 'sample' requires a value but none is provided

error: CommandArgumentNotProvided
/src/lib/zig-arg/src/parser/Parser.zig:415:13: 0x25401b in .zig-arg.parser.Parser.parseSubCommand (sample)
            return self.err_ctx.err;
            ^
/src/lib/zig-arg/src/parser/Parser.zig:147:28: 0x24ee37 in .zig-arg.parser.Parser.parse (sample
            const subcmd = try self.parseSubCommand(token.value);
                           ^
/src/lib/zig-arg/src/Command.zig:168:9: 0x248e9b in .zig-arg.Command.parseFrom (sample)
        return e;
        ^
/src/lib/zig-arg/src/Command.zig:160:5: 0x243703 in .zig-arg.Command.parseProcess (sample)
    return self.parseFrom(self.process_args.?[1..]);
    ^
/src/src/main.zig:345:16: 0x23cb37 in main (sample)
    var args = try parser.parseProcess();

Expected behavior

no error when "argRequired" is false.

I guess the issue comes from around here. It should take care of this situation and suppress the error when "argRequired" is false.

https://github.com/PrajwalCH/yazap/blob/4153a51dea5f1c3771a85fbff1b9b330988a73db/src/parser/Parser.zig#L413-L415

Automatic help text generation

Hi, thank you for creating this awesome library.

I have one feature request. (it could have already been considered though)
It would be more helpful for users to have automatic help text generation based on configured flags.

Yazap already has a Command.newWithHelpTxt function to set the about field, but currently, it doesn't have any way to output its information except for directly reading about and constructing our own help message by ourselves.

integer constraint

Thanks for the library.

Any chance to add something like singleValueOptionInt(T) which parses the input into an int of type T (making sure both that it's a valid integer and that it's within min(T) and max(T).

`multiValuesOption` results in a `singleValueOption` when provided with only one value

Hi,

I've just encountered some confusing (at least in my opinion) behaviour. When an option is declared as multi-value the resulting matched argument can still be of the .single variant, which makes it inconvenient to use, because you have to check for values using getMultiValues and then if that returns null (and it will, because the argument is .single) getSingleValue.

Reproduction example:

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

const App = yazap.App;
const Arg = yazap.Arg;

pub fn main() anyerror!void {
    var app = App.init(std.heap.page_allocator, "repro", null);
    defer app.deinit();

    var repro = app.rootCommand();

    try repro.addArg(Arg.multiValuesOption("repro", 'r', null, 5));

    const matches = try app.parseProcess();

    if (matches.args.get("repro")) |arg| {
        std.debug.print("Argument is single: {} is many: {}\r\n", .{ arg.isSingle(), arg.isMany() });
    }
}

Running this on zig 0.13.0 and latest yazap commit:

~/Workspace/yazap-repro via ↯ v0.13.0 
❯ zig build run -- --repro one    
Argument is single: true is many: false

~/Workspace/yazap-repro via ↯ v0.13.0 
❯ zig build run -- --repro one two
Argument is single: false is many: true

I'd expect the MatchedArgValue union to be .many in both cases and just return a slice with one element in the first case. I can try fixing this if you're open to PRs. Alternatively, if this is the desired behaviour, then I'd suggest adjusting the example at https://github.com/prajwalch/yazap/blob/main/src/arg_matches.zig#L185 to indicate that it's needed to check for both cases.

make it work on zig `v0.10.1`

Yazap is already tested against v0.9.1 and v0.10.0 but running the zig build test command on v0.10.1 exists unexpectedly with code 5, no any stack traces or any kind of error.

$ zig build test
LLVM Emit Object... error: test...
error: The following command exited with error code 5:
...

But if i remove all the tests and run it again the above issue doesn't occurs. I'm not sure whether it's a issue on compiler or on project itself.

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.