Is it possible to have subcommands as alternatives using clap?

I have a cli which is essentially this:

myapp [OPTIONS] <INFILE> [OUTFILE]

But the cli I want to have is one that offers three different possible usage patterns:

myapp [OPTIONS] <INFILE> [OUTFILE]
myapp equal <FILE1> <FILE2>
myapp equivalent <FILE1> <FILE2>

I have read clap's docs for subcommands and tried adding one but I get this which isn't what I want at all:

myapp [OPTIONS] <INFILE> [OUTFILE] [SUBCOMMAND]

Is the pattern I describe doable in clap?

You can certainly add subcommands. What specifically isn't working for you? Can you share the code?

The problem I have is that the 3 patterns are alternatives, i.e., the user either uses myapp equal file1 file2 or myapp equivalent file1 file2 or 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.

1 Like

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.

There are definitely programs I've used that work that way. You pass in ./equal instead, for example.

2 Likes

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)

1 Like

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.