The problem I have is that the 3 patterns are alternatives, i.e., the user either uses myapp equal file1 file2ormyapp equivalent file1 file2or doesn't use a subcommand, e.g., myapp -i8 file -.
The subcommand appears to be an added option rather than an alternative.
So really what I need is to add subcommands that can be used instead of the normal options (that don't involve any subcommand).
Here's an example of what doesn't work:
use clap::{AppSettings, Parser, Subcommand};
use std::path::PathBuf;
fn main() {
let config = Config::parse();
dbg!(config);
}
#[derive(Parser, Debug)]
#[clap(global_setting(AppSettings::DeriveDisplayOrder))]
#[clap(version, about = "test app")]
struct Config {
/// Indent (0-8 spaces or 9 to use a tab; ignored if -c|--compact used)
#[clap(short, long, default_value_t=2,
value_parser=clap::value_parser!(u8).range(0..=9))]
indent: u8,
/// Required infile
#[clap(value_parser)]
infile: PathBuf,
/// Optional outfile; use - to write to stdout or = to overwrite
/// infile
#[clap(value_parser)]
outfile: Option<PathBuf>,
/// Want this as an _alternative_ to all the others
#[clap(subcommand)]
equal: Equal,
}
#[derive(Subcommand, Debug)]
enum Equal {
/// Compare two files for equality ignoring insignificant whitespace
Equal {
/// Required file1
#[clap(value_parser)]
file1: PathBuf,
/// Required file2
#[clap(value_parser)]
file2: PathBuf,
},
}
If I then do myapp equal file1 file2 I get an error that I didn't supply INFILE. But of course that's not what I want because the equal subcommand is meant as an alternative to the regular usage.
The standard way to do subcommands with clap is to have them be exclusive (all variants of an enum). Then you don't have to do any funny tricks to do what you want. Playground
use clap::{AppSettings, Args, Parser, Subcommand};
use std::path::PathBuf;
fn main() {
let config = Subcommands::parse_from(["binname", "equal", "one", "two"]);
dbg!(config);
let config = Subcommands::parse_from(["binname", "main", "input"]);
dbg!(config);
}
#[derive(Args, Debug)]
#[clap(global_setting(AppSettings::DeriveDisplayOrder))]
#[clap(version, about = "test app")]
struct Config {
/// Indent (0-8 spaces or 9 to use a tab; ignored if -c|--compact used)
#[clap(short, long, default_value_t=2,
value_parser=clap::value_parser!(u8).range(0..=9))]
indent: u8,
/// Required infile
#[clap(value_parser)]
infile: PathBuf,
/// Optional outfile; use - to write to stdout or = to overwrite
/// infile
#[clap(value_parser)]
outfile: Option<PathBuf>,
}
#[derive(Parser, Debug)]
enum Subcommands {
Main(Config),
/// Compare two files for equality ignoring insignificant whitespace
Equal {
/// Required file1
#[clap(value_parser)]
file1: PathBuf,
/// Required file2
#[clap(value_parser)]
file2: PathBuf,
},
}
In general it's not possible to unambiguously parse a default subcommand (what if the infile arg to the main command is named "equal"?). This issue comment suggests it's possible to do what you want under some circumstances, but I don't think your command structure will work with that particularly well.
You could also try to parse the subcommands and then parse the main command if the subcommand parsing fails. But it's gonna be tricky to make that work reliably I think.
Yes, of course you're right about name conflicts between a subcommand and a filename.
If it was only for unix I could use soft links to give the exe different names; but since it is cross-platform I guess I'll have to switch to an all-subcommand cli. And using aliases this isn't as bad as I thought.
Thanks for your help.
The other common solution is for there to be a sentinel, usually --, that explicitly marks the boundary between options and filenames. (And can be omitted if there are no filename/option conflicts)