CLI Parser branching subcommand

For the pasts weeks I’ve been working in a CLI Parser. https://codeberg.org/SillyProgramming/Zark

I do not have a problem making not conditional subocmand and positionals.
But what I would want is if the first subcommand is X then it could be A and B or Y C and D.
I had some ideas with unions enums with have to be transform but the lib to add the needed fields like flags, and sequencials subcommands and positional
Any body has an other idea for acomplishing this?

1 Like

Some examples of how you want to invoke the CLI would be useful. But from what I understood, different commands can interact with other different commands. The way I usually handle this is:

var option_a: ?OptionA = null;
var option_b: ?OptionB = null;

while(next_option()) |n| switch(n){
    .A => |a| {option_a = a;},
    .B => |b| {option_b = b;},
};

// Now you do whatever logic validation you want
if(option_a != null and option_b == null)
  @panic("When A is present, you must supply B");
1 Like

I started doing this parser because I was making an app where I did every thing manually, so I tried to use a lib for this, but I did not found one which had branching subcommand I liked.

    std.debug.print(
        "Usage <-coverage> [SUBCOMAND]\n" ++
            "   Subcommand:\n" ++
            "     run <TARGET> <SUBCOMMAND>\n" ++
            "       Run the test on the [TARGET]. The [TARGET] is either a '*.yt' file or \n" ++
            "       folder with '*.yt' files. The default [TARGET] is '.yt'.\n" ++
            "       Subcommand\n" ++
            "         lexer\n" ++
            "         parser\n" ++
            "         check\n" ++
            "         ir\n" ++
            "         build\n" ++
            "         inter\n" ++
            "         run\n" ++
            "         all\n" ++
            "     update [SUBSUBCOMMAND]\n" ++
            "       SUBSUBCOMMAND\n" ++
            "         input <TARGET>\n" ++
            "           TARGET: Must be a '.yt' file\n" ++
            "         output <TARGET> <SUBCOMAND>\n" ++
            "           Must be a folder with .yt files or a .yt file\n" ++
            "           Subcommand\n" ++
            "              lexer\n" ++
            "              parser\n" ++
            "              check\n" ++
            "              ir\n" ++
            "              build\n" ++
            "              inter\n" ++
            "              run\n" ++
            "              all\n" ++
            "     help\n",
        .{},
    );

As you can see there is 2 main branches update and run then inside that there is simply a sequencial commands or positionals.
I want to declare this somehow plus indicate flag inside the branching subcomand.
All the libs I saw, had the option to add subcommands and I as the user check if it is valid according to the ones before and the same with the flags.

I want to declare it like the flag group ideally but I do not find how (The only way I thought it would be possible with a creating the union enums, the problem with this aproch is that I cannot reuse the declarations)

If I’m understanding you correctly, you just want subcommands to be associated to/validated by the command preceding them. Assuming that’s correct, my library cova intrinsically handles this use case.

Even if you don’t want to use the library directly, you might be able to find something useful in my approach to solving this problem.

1 Like

I tried reading it, I’ll have to read the docs slowly (some day next week). It does not seem that simple

1 Like

Hello! I also got this implemented in my library! In essence, when you parse a command you can treat it as a tagged union, and according to what the first command is, change what the parser should look for.

The thing you have to implement first is to detect how many different command the user can enter, and then make them into a tagged union. Like this structure from this example

const def = .{
    .flags = .{ Flag("verbose", "v", "Print more" ) },
    .commands = .{
        .entry = .{
            .commands = .{
            .start = entry_start,
            .status = .{},
            .stop = .{},

            }
        },
        .project = .{
            .commands = .{
                .create = project_create,
                .rename = project_rename,
            }
        },
    }
};

becomes this enum with some comptime magic

const CommandUnion = enum {
    entry,
    project,
};

and i do it with a code like this in src/gnu_parse.zig, line 146

const CommandUnion = comptime find_cmd: {
  const fields = @typeInfo(ReArgs).@"struct".fields;
  for (fields) |f| {
      if (std.mem.eql(u8, f.name, "cmd")) {
          break :find_cmd f.type;
      }
  }
  unreachable; // if @hasField(definition, "commands") this is unreachable
};

Of course this is inside a function which explores the user input, and then changes what shoud it parse according to the union the command took. The idea is that inside of a subcommand you have the same problem you were solving up until now, which you just call the same function again:

  1. if next argument is a command, you recall the function with the remining commands
  2. if its an argument, a flag or an option, you call your actual parsing logic, as you know there will be no more commands (at least this is what i assume)

This makes the code the user has to write like the following one, from the same example:

switch (arguments.cmd) {
    .entry => |entry_cmd| {
        switch (entry_cmd.cmd) {
            .start => |start_args| {
                try stdout.print("'entry start' detected!\n", .{});
                if (start_args.projectid) |pid| {
                    try stdout.print("Detected pid: {d}\n", .{pid});
                } else {
                    try stdout.writeAll("No pid detected\n");
                }
            },
            .stop => try stdout.writeAll("'entry stop' detected!\n"),
            .status => try stdout.writeAll("'entry status' detected!\n"),
        }
    },
    .project => |project_cmd| {
        switch (project_cmd.cmd) {
            .create => |create_args| {
                 try stdout.print("Creating project: {s}\n", .{create_args.description});
            },
            .rename => |rename_args| {
                 try stdout.print("Renaming ID {d} to {s}\n", .{rename_args.projectid, rename_args.name});
            }
        } 
    }
}

Idk if you are taking advantage of comptime execution to generate code that parses the exact user definition. If you are taking a more traditional approach, maybe this is not what you were looking for and I am sorry haha

1 Like