00JCIV00/cova

Mutually exclusive options in a mandatory set.

p7r0x7 opened this issue · 8 comments

p7r0x7 commented

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.

Would it be possible to work these like this?

encoder -input y4m -output webm

If so, you can use the parse_fn field of the inner Value to parse the Option directly to an enum. I actually have a Builder fn for exactly that available here.

If that's not possible/desirable, I would use exclusive or (XOR) logic with the getOpts() method of your main Command during analysis. So something like this:

const opts = try main_cmd.getOpts();
defer alloc.free(getOpts); // not needed with the preferred Arena Allocator, but it's good practice I think.

// List of possible names for the Input Option
const xor_in_opts: []const []const u8 = .{ "exclusive", "input", "opts" };
// Run XOR on the given Input Option against the list above
const xor_in_opt = getXorIn: {
    var in_opt: ?OptionT = null;
    var xor_in_flag = false;
    for (opts) |opt| {
        if (cova.utils.indexOfEql([]const u8, xor_in_opts, opt)) |_| {
            if (xor_in_flag) {
                xor_in_flag = false;
                break;
            }
            xor_in_flag = true;
            in_opt = opt;
        }
    }
    if (!xor_in_flag) {
        log.err("The Input Option must be exclusively one of: {s}\n", .{ xor_in_opts });
        return error.NonExclusiveInput;
    }
    break :getXorIn in_opt.?;
};

// Handle xor_in_opt
// Repeat for xor_out_opt

I wrote that up here so it's untested, but it should be the general idea. Admittedly, I could probably make that a smoother process somehow. Maybe wrap it into checkExclusiveOpts()/matchExclusiveOpts() methods? Let me know what you think.

p7r0x7 commented

The goal was to a) reduce the number of required options as much as possible (as things get out of hand quickly when it comes to encoding parameters) and b) eliminate the need for passed path to have any specific format or have any specific extension

This allows the user to arbitrarily specify filenames as they like without it affecting the format the command line application interprets the read or written data.

In other words, the following all work and aren't unclear at all and don't require any additional flags to specify anything:

-y4m=/dev/stdin (format: y4m, input path: /dev/stdin)
-mkv=- (format: mkv, input path: /dev/stdin)
-ivf=- (format: ivf, output path: /dev/stdout)
-webm=file.mkv (format: webm, output path: file.mkv)

I think that all makes sense to me. If I'm understanding correctly, point a rules out my first solution, but I believe my second solution using getOpts() would work. As is, it's a little verbose, but I have an idea for checkOpts() and matchOpts() methods that should make it easier. These methods will return a bool or an []OptionT, respectively, based on a list of Option names and either AND, OR, or, XOR logic as defined by the user (with the default being OR).

For your case it would look something like:

const in_fmt = (try main_cmd.matchOpts(&.{ "valid", "input", "formats" }, .{ .logic = .XOR }))[0];
const out_fmt = (try main_cmd.matchOpts(&.{ "valid", "output", "formats" }, .{ .logic = .XOR }))[0];

// Handle in_fmt and out_fmt 

I'll have this integrated and pushed to the working v0.9.0 branch soon.

p7r0x7 commented

Yes, my only constraint is external usage, not internal handling of the mutual-exclusivity, so it would appear that your solution would work just fine (though, I don't completely understand what your code is doing at a glance).

No worries on trying to understand that code. It was pseudo-code and a little flawed, but the intent was to analyze the Options set by the end user to ensure only 1 from the provided list was set (XOR logic). I've now rolled that into the new checkOpts() and matchOpts() methods in Command.Custom so you should be able to do the following during analysis:

const in_fmt = (main_cmd.matchOpts(&.{ "valid", "input", "formats" }, .{ .logic = .XOR }) catch |err| switch (err) { 
    error.CheckOptionLogicFailXOR => { // Handle the case where the user provides more than one input format  },
    else => return err,
})[0];
const out_fmt = (main_cmd.matchOpts(&.{ "valid", "output", "formats" }, .{ .logic = .XOR }) catch |err| switch (err) { 
    error.CheckOptionLogicFailXOR => { // Handle the case where the user provides more than one output format  },
    else => return err,
})[0];

// Handle in_fmt and out_fmt 

You can also use AND or OR logic if you want to make sure only specific Options from a given list are set as well.

Let me know if that works for you. If not, I'm here to answer any questions and/or tweak the code a little as needed.

p7r0x7 commented

I believe you've created a library thorough enough for me to create external categories of values, and when I have, I'll share my code because I believe option categories are an important feature Cova should internally support.

I believe you've created a library thorough enough for me to create external categories of values, and when I have, I'll share my code because I believe option categories are an important feature Cova should internally support.

This was also brought up in issue #26 by @GobiasSomeCoffeeCo. It's part of the v0.9.0 milestone to implement for sure. I'd love to see your take on the feature as well!

p7r0x7 commented

Closing this here as I don't plan to revisit this for awhile.