Rust 언어의 clap으로 Cli 앱에서 subcommand 만들기

안녕하세요?

오늘은 지난 시간에 이어 Rust의 clap 크레이트 공부를 계속해 보겠습니다.

지난 시간 강좌는 여기를 누르시면 됩니다.

use clap::{ Parser, Subcommand, Args };

일단 위와 같이 clap으로 Subcommand를 사용하려면 위와 같이 Parser, Subcommand, Args를 불러와야 합니다.


Subcommand란?

subcommand는 요즘 Cli앱에서 자주 쓰이는 형식인데요.

clap-v4-test show --name BTS

위와 같이 실행파일 뒤에 show를 썼는데요.

이 show가 Subcommand이고, show 밑에는 또다시 아규먼트나 옵션이 들어올 수 있습니다.

show 말고 다른 Subcommand도 만들 수 있으니까요?

훨씬 복잡한 Cli앱을 만들 수 있겠죠.

이제 본격적인 Cli 구조체를 설계해 볼까요?

#[derive(Debug, Parser)]
struct Cli {
    #[clap(subcommand)]
    command: MyCommand,
}

일단 우리는 clap이란 매크로로 subcommand를 자동으로 작성하라고 해줍니다.

Cli 구조체에 command라는 "MyCommand"라는 subcommand만 있죠.

그러면 "MyCommand"라는 enum을 만들어야 합니다.

#[derive(Debug, Parser)]
struct Cli {
    #[clap(subcommand)]
    command: MyCommand,
}

#[derive(Subcommand, Debug)]
enum MyCommand {
    Show(ShowArgs),
}

자, 이제 cargo run -- show 처럼 show라는 subcommand를 만들었네요.

이제 show 밑에 아규먼트나 옵션을 넣어 줄 수 있는데요.

enum으로 만든 MyCommand에는 Show(ShowArgs)가 있네요.

그러면 여기서 Show는 subcommand 'show'가 되는 거고,

이 subcommand 'show'를 실행하는 구조체 ShowArgs를 따로 만들어 줘야 합니다.

#[derive(Debug, Parser)]
struct Cli {
    #[clap(subcommand)]
    command: MyCommand,
}

#[derive(Subcommand, Debug)]
enum MyCommand {
    Show(ShowArgs),
}

#[derive(Debug)]
struct ShowArgs {
    name: String,
}

일단 위와 같이 했네요.

위 방식은 ShowArgs가 아규먼트가 되는 겁니다.

➜  clap-v4-test git:(master) ✗ cargo run -- show BTS
   Compiling clap-v4-test v0.1.0 (/Users/cpro95/Codings/Rust/clap-v4-test)
    Finished dev [unoptimized + debuginfo] target(s) in 1.08s
     Running `target/debug/clap-v4-test show BTS`
Cli { command: Show(ShowArgs { name: "BTS" }) }
➜  clap-v4-test git:(master) ✗

위와 같이 실행됩니다.

이제 옵션 방식으로 바꿔볼까요?

use clap::{ Parser, Subcommand, Args };

#[derive(Debug, Parser)]
struct Cli {
    #[clap(subcommand)]
    command: MyCommand,
}

#[derive(Subcommand, Debug)]
enum MyCommand {
    Show(ShowArgs),
}

#[derive(Args, Debug)]
struct ShowArgs {
    #[arg(short, long)]
    name: String,
}

fn main() {
    let cli = Cli::parse();

    println!("{:?}", cli);
}

위와 같이 하면 실행 결과는 아래와 같습니다.

➜  clap-v4-test git:(master) ✗ cargo run -- show --name BTS
    Finished dev [unoptimized + debuginfo] target(s) in 0.06s
     Running `target/debug/clap-v4-test show --name BTS`
Cli { command: Show(ShowArgs { name: "BTS" }) }
➜  clap-v4-test git:(master) ✗ cargo run -- show -n=BTS
    Finished dev [unoptimized + debuginfo] target(s) in 0.06s
     Running `target/debug/clap-v4-test show -n=BTS`
Cli { command: Show(ShowArgs { name: "BTS" }) }
➜  clap-v4-test git:(master) ✗ cargo run -- show -n BTS
    Finished dev [unoptimized + debuginfo] target(s) in 0.06s
     Running `target/debug/clap-v4-test show -n BTS`
Cli { command: Show(ShowArgs { name: "BTS" }) }
➜  clap-v4-test git:(master) ✗

어떤가요?

우리가 지난 시간에 배운 옵션 방식으로 작성되었습니다.

상세 도움말 페이지는 아래와 같이 나오게 됩니다.

➜  clap-v4-test git:(master) ✗ cargo run -- show --help
    Finished dev [unoptimized + debuginfo] target(s) in 0.06s
     Running `target/debug/clap-v4-test show --help`
Usage: clap-v4-test show --name <NAME>

Options:
  -n, --name <NAME>
  -h, --help         Print help
➜  clap-v4-test git:(master) ✗ cargo run -- --help
    Finished dev [unoptimized + debuginfo] target(s) in 0.05s
     Running `target/debug/clap-v4-test --help`
Usage: clap-v4-test <COMMAND>

Commands:
  show
  help  Print this message or the help of the given subcommand(s)

Options:
  -h, --help  Print help
➜  clap-v4-test git:(master) ✗ cargo run -- --help
    Finished dev [unoptimized + debuginfo] target(s) in 0.05s
     Running `target/debug/clap-v4-test --help`
Usage: clap-v4-test <COMMAND>

Commands:
  show
  help  Print this message or the help of the given subcommand(s)

Options:
  -h, --help  Print help
➜  clap-v4-test git:(master) ✗ cargo run -- show --help
    Finished dev [unoptimized + debuginfo] target(s) in 0.17s
     Running `target/debug/clap-v4-test show --help`
Usage: clap-v4-test show --name <NAME>

Options:
  -n, --name <NAME>
  -h, --help         Print help
➜  clap-v4-test git:(master) ✗

어떤가요?

쉽게 clap으로 subcommand를 만들었는데요.

그러면 name 값을 파싱 하는 방법에 대해 알아볼까요?

fn main() {
    let cli = Cli::parse();

    match &cli.command {
        MyCommand::Show(show_args) => println!("name is {}", show_args.name),
    }
}

실행 결과는 아래와 같습니다.

➜  clap-v4-test git:(master) ✗ cargo run -- show --name BTS
    Finished dev [unoptimized + debuginfo] target(s) in 0.06s
     Running `target/debug/clap-v4-test show --name BTS`
name is BTS
➜  clap-v4-test git:(master) ✗

MyCommand::Show가 enum 타입이라 match를 써야 합니다.

그리고 Show(ShowArgs)에 있는 ShowArgs는 show_args라는 변수로 지정하고, 최종적으로 show_args.name이라고 접근하면 됩니다.


다중 Subcommand 만드는 법

Clap에서 Subcommand는 맨 처음 구조체에서 시작하는데요.

구조체 안에 enum이 있고 enum안에 다시 구조체가 오는 방식입니다.

최종적으로는 구조체가 오고요.

use clap::{ Parser, Subcommand, Args };

#[derive(Debug, Parser)]
struct Cli {
    #[clap(subcommand)]
    entity_type: EntityType,
}

#[derive(Debug, Subcommand)]
enum EntityType {
    /// User for create, delete
    User(UserCommand),

    /// Video for create, delete
    Video(VideoCommand),
}

#[derive(Debug, Args)]
struct UserCommand {
    #[clap(subcommand)]
    command: UserSubcommand,
}

#[derive(Debug, Subcommand)]
enum UserSubcommand {
    /// User Create
    Create(CreateUser),

    /// User Delete
    Delete(DeleteUser),
}

#[derive(Debug, Args)]
struct CreateUser {
    /// name of the user
    name: String,

    /// email of the user
    email: String,
}

#[derive(Debug, Args)]
struct DeleteUser {
    /// name of the user
    name: String,
}

#[derive(Debug, Args)]
struct VideoCommand {
    #[clap(subcommand)]
    command: VideoSubCommand,
}

#[derive(Debug, Subcommand)]
enum VideoSubCommand {
    Show(VideoShow),
}

#[derive(Debug, Args)]
struct VideoShow {
    /// id of video
    id: usize,
}

fn main() {
    let cli = Cli::parse();

    println!("{:?}", cli);
}

실행 결과는

➜  clap-v4-test git:(master) ✗ cargo run
   Compiling clap-v4-test v0.1.0 (/Users/cpro95/Codings/Rust/clap-v4-test)
    Finished dev [unoptimized + debuginfo] target(s) in 2.21s
     Running `target/debug/clap-v4-test`
Usage: clap-v4-test <COMMAND>

Commands:
  user   User for create, delete
  video  Video for create, delete
  help   Print this message or the help of the given subcommand(s)

Options:
  -h, --help  Print help
➜  clap-v4-test git:(master) ✗ cargo run -- --help
    Finished dev [unoptimized + debuginfo] target(s) in 0.06s
     Running `target/debug/clap-v4-test --help`
Usage: clap-v4-test <COMMAND>

Commands:
  user   User for create, delete
  video  Video for create, delete
  help   Print this message or the help of the given subcommand(s)

Options:
  -h, --help  Print help



➜  clap-v4-test git:(master) ✗ cargo run -- user --help
    Finished dev [unoptimized + debuginfo] target(s) in 0.06s
     Running `target/debug/clap-v4-test user --help`
User for create, delete

Usage: clap-v4-test user <COMMAND>

Commands:
  create  User Create
  delete  User Delete
  help    Print this message or the help of the given subcommand(s)

Options:
  -h, --help  Print help
➜  clap-v4-test git:(master) ✗

user, video라는 SubCommand가 있고, user 밑에는 다시 create, delete란 Subcommand가 있습니다.

➜  clap-v4-test git:(master) ✗ cargo run -- user create --help
    Finished dev [unoptimized + debuginfo] target(s) in 0.18s
     Running `target/debug/clap-v4-test user create --help`
User Create

Usage: clap-v4-test user create <NAME> <EMAIL>

Arguments:
  <NAME>   name of the user
  <EMAIL>  email of the user

Options:
  -h, --help  Print help
➜  clap-v4-test git:(master) ✗

위와 같이 user creaate 밑에는 아규먼트 방식으로 name과 이메일이 보이네요.

  clap-v4-test git:(master) ✗ cargo run -- user create BTS bts@hybe.com
    Finished dev [unoptimized + debuginfo] target(s) in 0.17s
     Running `target/debug/clap-v4-test user create BTS 'bts@hybe.com'`
Cli { entity_type: User(UserCommand { command: Create(CreateUser { name: "BTS", email: "bts@hybe.com" }) }) }
➜  clap-v4-test git:(master) ✗

어떤가요?

아주 복잡한 2단계 Subcommand도 만들 수 있게 되었네요.

그럼.