Search code examples
rustclap

Clap - subcommands with possibly shared sets of default values?


I've been going in circles on this for a while and haven't found a good solution:

I've got a bunch of simulations in the same codebase. I'm trying to be an adult and use command line arguments to pick which simulation runs and with what parameters. The issue is that lots of the arguments are shared between different simulations, and some sets are passed to shared components.

For instance, in the below I have three simulations, two of which share the "Threaded" argument subset, another two share the MazeCli argument subset, which would be wanted by the Maze trait. (In the full thing, there are more arguments and combinations.)

use clap::*;

#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Cli {
    #[command(subcommand)]
    sim: Simulation,
}

#[derive(Subcommand, Debug)]
enum Simulation {
    Maze {
        maze_args: MazeCli,
        thread_args: ThreadCli,
    },
    ThreadedMaze {
        maze_args: MazeCli,
    },
    Threaded {
        thread_args: ThreadCli,
    },
}

trait Maze {
    fn new(args: MazeCli) -> Self;
}

#[derive(Args, Debug, Clone)]
struct MazeCli {
    squares: usize,
    openness: f64,
}

#[derive(Args, Debug, Clone)]
struct ThreadCli {
    threads: usize,
}
fn main() {
    let config = Cli::parse();
    println!("{:?}", config);
}

That version fails because it wants the MazeCli and ThreadCli to implement ValueEnum (as far as I can tell - the actual error is long and unhelpful). ValueEnum can't be derived for structs, though. I've tried a few other approaches, but have not gotten anything to compile.

I could do everything as one flat arg list, but then error messages won't tell you what's expected for the specific subcommand you run.

I could manually specify every list, but that's a bunch of boilerplate, especially when default values are factored in.

What's the right way to do this? It it something clap supports at all?

Bonus questions :

  • If this is possible at all, is there any way for specific subcommands to override generic defaults for their specific case (like, if one simulation wanted twice as many threads as the norm by default.)
  • Can I make the default values somehow fall back to a Default trait implementation for that struct?

Solution

  • Use #[command(flatten)]:

    #[derive(Subcommand, Debug)]
    enum Simulation {
        Maze {
            #[command(flatten)]
            maze_args: MazeCli,
            #[command(flatten)]
            thread_args: ThreadCli,
        },
        ThreadedMaze {
            #[command(flatten)]
            maze_args: MazeCli,
        },
        Threaded {
            #[command(flatten)]
            thread_args: ThreadCli,
        },
    }
    

    Can I make the default values somehow fall back to a Default trait implementation for that struct?

    Use default_value_t without a parameter. It defaults to Default::default():

    #[derive(Args, Debug, Clone)]
    struct MazeCli {
        #[arg(long, default_value_t)]
        squares: usize, // Will be optional with a default of 0
        openness: f64,
    }
    

    If this is possible at all, is there any way for specific subcommands to override generic defaults for their specific case (like, if one simulation wanted twice as many threads as the norm by default.)

    I don't think it's possible without using separate structs.