Mutually-exclusive command-line arguments with Clap (or another library)?

I'm designing a CLI for a small tool I'm writing, and I'd like it to support two modes of operation:

  • Online, where it queries an API for JSON data.
  • Cached, where it looks up a local JSON file for the data.

I'd like to be able to specify that on the command line using invocations like

$ frobnicate fetch # Defaults to --online
$ frobnicate --online fetch
$ frobnicate --cached path/to/data.json fetch

However, I can't seem to figure out if Clap (v4) provides a nice way to do that. What I'd like to do would be something like

#[derive(Args)]
enum DataFetchMode {
    /// Fetch the data to frobnicate from the online API.
    #[arg(long, short = 'O')]
    Online,

    /// Fetch the data to frobnicate from a local cache.
    #[arg(long, short = 'C', value_name = "PATH")]
    Cached(PathBuf),
}

However, the analogous example in the "Argument Relations" section of the tutorial parses its mutually exclusive options to a struct:

#[derive(Args)]
#[group(required = true, multiple = false)]
struct Vers {
    /// set version manually
    #[arg(long, value_name = "VER")]
    set_ver: Option<String>,

    /// auto inc major
    #[arg(long)]
    major: bool,

    /// auto inc minor
    #[arg(long)]
    minor: bool,

    /// auto inc patch
    #[arg(long)]
    patch: bool,
}

This is awkward, because it means that all of your logic has to handle the case of Vers { set_ver: Some("1.2.3".to_owned()), major: true, minor: true, patch: bool }, or for my example the case of DataFetchMode { online: true, cached: Some("path/to/data".into()) }.

If Clap doesn't let me parse mutually-exclusive options in a nice way, I am also open to switching CLI parsers. It looks like bpaf lets me do this, for instance, with something like the following? But I'm open to any and all recommendations.

#[derive(Bpaf)]
#[bpaf(options)]
enum DataFetchMode {
    /// Fetch the data to frobnicate from the online API.
    #[bpaf(long, short('O'))]
    Online,

    /// Fetch the data to frobnicate from a local cache.
    #[arg(long, short('C'), argument("PATH"))]
    Cached(PathBuf),
}

Is something like this with Clap what you're looking for?

1 Like

I see, so parse into the ugly type and handle the useful type separately? That could work, although it would be nicer if I didn't have to write two types and especially if I could actually use the DataSource type in the Cli command type itself. I think that's possible using an explicit impl, but that's getting to be a lot of boilerplate…

You are complicating the idea of having two modes (online/offline) represented by separate types. An enum with two variants (Mode::Online, Mode::Cached) more clearly represents the transition between two states.

In this case, though, a simple bool may be sufficient, combined with a sensible default value, as in this toy example on the the playground.

@crumplecup I'm afraid I don't understand this response. My example code does use an enum with two variants, that's what DataFetchMode is. Your link is also to @keyosk's playground example, not something new.

I think I see the error. The link the should point to here.

What about this version?

1 Like

FYI, there is an open feature request for clap to support producing an enum directly:

2 Likes

Well that settles it – and it looks like work on a solution (clap-rs/clap#5700) stalled out because it's tough to implement due to the library internals. So it's either one of the workarounds like @keyosk shared or switching libraries. Thanks, @kpreid!

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.