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

⚓ Rust    📅 2025-09-22    👤 surdeus    👁️ 7      

surdeus

Warning

This post was published 40 days ago. The information described in this article may have changed.

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),
}

8 posts - 4 participants

Read full topic

🏷️ Rust_feed