Mutually-exclusive command-line arguments with Clap (or another library)?
⚓ Rust 📅 2025-09-22 👤 surdeus 👁️ 7I'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
🏷️ Rust_feed