3

Situation

I want to build a more complex CLI tool. For the purposes of this question, let's say I want to build my own implementation of an AWS Cli tool.

I want to split up the logic in the different services (like EC2, S3, sns, etc) and be able to execute on subcommands.

$ aws ec2 describe-instances

$ aws ec2 start-instances --instance-ids i-1348636c

$ aws s3 <Command> [<Arg> ...]

Complication

To properly split up the business logic, I want to distribute the subcommands over multiple files.

.
├── Cargo.lock
├── Cargo.toml
└── src
    ├── main.rs
    ├── S3.rs
    ├── Ec2.rs
    └── etc.rs

I want every service (S3, EC2, etc) to be in control of their own commands and arguments and this would mean that the structure of the subcommands needs to be distributed to the rs files of each the service (S3.rs, Ec2.rs, etc).

Question

What would be the most idiomatic way to create the struct for the args in rust/clap? I prefer to utilize the #[derive] macro as much as possible, because it looks like clap is recommending this.

Caesar
  • 6,733
  • 4
  • 38
  • 44
Arend-Jan
  • 53
  • 4

2 Answers2

3

For each level of commands, you need one enum that lists all subcommands, e.g.:

// main.rs
mod ec2;
mod s3;

#[derive(Parser)]
enum Cli {
  S3(s3::Command),
  EC2(ec2::Command),
}

Each of the command modules can then have a separate struct that defines further options or subcommands.

// ec2.rs
#[derive(Parser)]
pub(crate) struct Command {
  #[clap(long)]
  vpc: String,
  #[clap(subcommand)]
  subcommand: Subcommand
}
#[derive(Parser)]
enum Subcommand {
  Instance(instance::Command),
  FooBar(…)
}

You can find a real-world example of this strategy in wasmer.

Caesar
  • 6,733
  • 4
  • 38
  • 44
  • 2
    Anyone who is having a hard time understanding complex Clap usage, I highly recommend watching this 5 min tutorial https://www.youtube.com/watch?v=fD9ptABVQbI (not sponsored or affiliated in any way) – Pavindu Dec 07 '22 at 06:14
0

The main.rs is the default executable file in the rust program. If you want to distribute a library you would typically have different executables inside the bin folder.

I would recommend moving the service files ec2.rs and s3.rs to src/bin folder which makes them executables. You can now run these files from the command line/terminal directly with command cargo run --bin s3.

src/bin/ec2.rs 
src/bin/s3.rs

Further, for the subcommands, you need to specify the subcommand in an Enum and provide these commands to the clap macro #[clap(subcommand)]

File src/bin/ec2.rs

Struct Cli {
  #[clap(subcommand)]
  command: Command,
}

enum Command {
  StartInstance { arg1: String, arg2: String }
  DescribeInstance { arg1: String }   
}

File src/bin/ec2.rs

let args = Cli.parse();

let command = args.command;
// match on the command and extract args for further processing.
Ashish Singh
  • 739
  • 3
  • 8
  • 21
  • I do believe @OP wants a single binary as an entrypoint. (This can be realized the same the way `git` or `cargo` commands allow calling other binaries when you invoke `cargo audit`, e.g. But is that accounted for in your suggestion?) – Caesar Aug 15 '22 at 07:49
  • @Caesar your correct. For personal use I wouldn't mind having multiple bin's, but this cli will be used by other people (potentially only having access to an executable). So it makes a lot more sense to propagate it all into one binary. – Arend-Jan Aug 15 '22 at 08:04
  • Okay got it. I am modifying the answer to reflect the solution in a bit. – Ashish Singh Aug 15 '22 at 08:11
  • 1
    Caesar's solution is correct. – Ashish Singh Aug 15 '22 at 09:20