Search code examples
rustcommand-line-interfaceclap

How to parse common subcommand arguments with Clap in Rust?


I trying to build cli which should take <command_name> as first argument, <path_to_file> as last argument and options in between, so call in console would look like this:

programm command_one --option True file.txt

I have setup like this:

// ./src/main.rs
use clap::{Args, Parser, Subcommand};

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

#[derive(Args, Debug)]
struct CommandOneArgs {
   file: String,
   #[arg(short, long)]
   option_for_one: Option<String>,
}

#[derive(Args, Debug)]
struct CommandTwoArgs {
   file: String,
   #[arg(short, long)]
   option_for_two: Option<String>,
}


#[derive(Subcommand, Debug)]
enum Commands {
   CmdOne(CommandOneArgs)
   CmdTwo(CommandTwoArgs)
}


fn main() {
   let args = Cli::parse();
   match &args.command {
      Commands::CmdOne(cmd_args) => {println!({:?}, cmd_args)}
      Commands::CmdTwo(cmd_args) => {println!({:?}, cmd_args)}
      _ => {}
   }

But here is the problem which i am failing to solve:
In reality in branches of match i will call some functions with obtained args;
However i need to do preparation common for all commands, e.g. read file from path
So before matching expression i need to extract file attribute:

fn main() {
   let args = Cli::parse();
   /// something like that
   // let file_path = args.command.file;
   // println!("reading from: {}", file_path)
   match &args.command {
      Commands::CmdOne(cmd_args) => {println!({:?}, cmd_args)}
      Commands::CmdTwo(cmd_args) => {println!({:?}, cmd_args)}
      _ => {}
   }

I can not do that in a way like commented.

And I can not add position argument to Cli struct because then interface would look like: programm <POSITIONAL ARG> command_one ...
I have assumptions that I should use generics, but I do not know how.


Solution

  • I'm just beginning with clap as well. While it does not entirely match your issue, I've just came across this Github discussion.

    It would fit your needs if all your subcommands need this parameter (that's my case). The code examples are from my project, but the idea is to use the global method of the Arg struct, which allows an argument to be matched to all child Subcommands:

    #[derive(Parser, Debug)]
    #[command(author, version, about, long_about = None)]
    struct Cli {
        #[command(subcommand)]
        command: Commands,
    
        #[arg(global=true)]  // <-- here
        file: Option<String>
    }
    

    Even though the global help stays the same:

    $ cargo run -- -h
    [...]
    Usage: fed [FILE] <COMMAND>
    [...]
    

    , the subcommand helps are updated:

    $ cargo run -- insert -h
    [...]
    Usage: fed insert -n <N> --line <LINE> [FILE]
    [...]
    

    This probably has limits and it still allows the file argument to be given first, but there might be some way to improve this. I'll update this answer if I find something better in the future. Cheers!