66
38

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Rustで手軽にCLIツールを作れるclapを軽く紹介する

Last updated at Posted at 2022-01-11

はじめに

rustでCLIツールを作ろうとした際に最初に候補にあがるライブラリがclapだろう。実際、Rust CLI working groupがまとめているリポジトリでは、Useful cratesの章でclapが紹介されている。

ただこの記事を執筆している時点での最新バージョンは3.0.5なのに対し、ググって出てくる記事で紹介されているコードがバージョン2.xのものだったりする。3.0がリリースされたのはこの記事を書いた数日前でした。clapは2.xから3.0に上がるタイミングでいくつかのマクロが廃止された他、deprecatedになった実装が多数存在する。また新機能として構造体を定義するとパーサーを生成してくれる機能が追加され、これがめっぽう使い易いのでぜひ使って頂きたいのもある。

ネット上のコードをとりあえずコピペしても動作しない場合があるため、「とりあえず使ってみたい」という人にとって現状は大変不便な状況である。本記事ではこの状況をふまえ、最新版で動作するサンプルコードを提示したい。

clapとはどんなライブラリなのか?

clapはコマンドラインから渡された引数を解析するためのライブラリ。使用感はNode.jsにおけるyargsに近く、メソッドチェーンによりツールのインターフェースを定義することができる。だがNode.jsと違い、Rustではclap::Parserトレイトを継承したstructを定義することで宣言的にオプション引数などを指定することもできるようになっている。筆者的にはこちらの書き方の方が記述がすっきりするので好き。本記事では両方の書き方を紹介する。

執筆時点の環境

  • OS: Ubuntu 20.04.3 LTS
  • rustc: 1.56.1 (59eed8a2a 2021-11-01)
  • cargo: 1.56.0 (4ed5d137b 2021-10-04)
  • clap: 3.0.5
  • clap_derive: 3.0.5

まずはいちばん単純な定義方法から

Cargo.tomlの指定には特に変わったところはない。

Cargo.toml
# ... 前略 ...

[dependencies]
clap = "3.0.5"

このとき、main.rsの記述は下記の通りとなる。

main.rs
use clap::{App, Arg};

fn main() {
    // 任意のオプション(デフォルト値の指定も可能)
    let optional_opt: Arg = Arg::new("name")  // 入力値を取得する際の識別子を指定
        .short('n')                           // オプション(短)に使用する文字を指定
        .long("name")                         // オプション(長)に使用する文字列を指定
        .default_value("Alice");              // デフォルト値を指定(※暗黙的にtakes_value(true)が指定される)

    // 必須のオプション
    let required_opt: Arg = Arg::new("count")
        .short('c')
        .long("count")
        .takes_value(true)                    // オプションが値を要求するかどうかを設定
        .required(true);

    // 位置引数
    let positional_arg: Arg = Arg::new("message")
        .required(true);

    // 上記で定義したオプションや引数は下記のようにAppに渡す
    let app: App = App::new("My Application")
        .author("Author's name")
        .version("v1.0.0")
        .about("Application short description.")
        .arg(optional_opt)
        .arg(required_opt)
        .arg(positional_arg);
    
    // Appを設定し終わったら下記のようにしてコマンドラインからのデータを解析することができる
    // ArgMatchesを取得する関数には"try_"で始まるものとそうでないものがある。
    // "try_"が付いていない方は渡されたデータが不正だった場合にプロセスが終了するため、
    // "try_"系を使った方が安全ではある。
    // ただほとんどのCLIツールは引数指定が間違っていた場合はエラーとヘルプを出力すると思うが、
    // そういった挙動はclap側で実装されているため、あまり神経質になる必要はなさそう。
    // とりあえずこのサンプルでは簡単なエラーハンドリングを例示しておく。
    match app.try_get_matches() {
        Ok(m) => {
            let name = m.value_of("name").unwrap();
            let count = m.value_of("count").unwrap().parse::<i32>().unwrap();
            let message = m.value_of("message").unwrap();
            for _ in 0..count {
                println!("{}: {}", name, message);
            }
        },
        Err(e) => {
            println!("{}", e);
        }
    }
}

derive featureを利用して見通しを良くする

上記でわかる通り、基本的にclapはArgAppを利用してインターフェースを定義していく。すこしオプションの数を増やすと一気にコードの見通しが悪くなってしまう点には注意が必要だ。そこでfeatureとして定義されているclap_deriveを使用する。まずはCargo.tomlを以下の通り変更する。

Cargo.toml
  [dependencies]
- clap = "3.0.5"
+ clap = { version = "3.0.5", features = ["derive"] }

これでclap::Parserトレイトが使用できるようになる。Parserを使用すると上記のコードは下記のように変更できる。コードで記述していたものが宣言的にかけるようになり、メインの処理がかなり見通しがよくなったことがわかるだろう。また整数が渡される前提だったパラメーターcountの文字列→整数への変換処理が必要なくなったことにも注目して頂きたい。

main.rs
use clap::Parser;

// Parserを継承した構造体はArgの代わりに使用することが可能。
#[derive(Parser)]
#[clap(
    name = "My Application",
    author = "Author's name",
    version = "v1.0.0",
    about = "Application short description."
)]
struct AppArg {
    // 任意のオプション
    #[clap(short, long)]
    name: Option<String>,

    // 必須のオプション
    #[clap(short = 'c', long = "count")]
    count: i32,

    // 位置引数
    message: String,
}

fn main() {
    let arg: AppArg = AppArg::parse();
    for _ in 0..arg.count {
        println!(
            "{}: {}",
            arg.name.clone().unwrap_or(String::from("Alice")),
            arg.message
        );
    }
}

ちなみに、上記コードでは「任意項目にしたい場合はOptionで型を指定すればよい」と示すためにわざとデフォルト値を宣言しなかったが、当然これも可能である。

main.rs
  struct AppArg {
      // 任意のオプション
-     #[clap(short, long)]
-     name: Option<String>,
+     #[clap(short, long, default_value = "Alice")]
+     name: String,

サブコマンドを定義する

サブコマンドの定義方法は、App::new()を使用するパターンとParserを使うパターンのどちらにも用意されている。

main.rs
use clap::{App, Arg};

fn main() {
    let subcommand1 = App::new("echo").arg(Arg::new("message").required(true));
    let subcommand2 = App::new("greet").arg(Arg::new("name").required(true));
    let app = App::new("Application")
        .subcommand(subcommand1)
        .subcommand(subcommand2);
    match app.get_matches().subcommand() {
        Some(("echo", sub_m)) => {
            println!("{}", sub_m.value_of("message").unwrap());
        }
        Some(("greet", sub_m)) => {
            println!("hello, {}", sub_m.value_of("name").unwrap());
        }
        _ => {}
    }
}
main.rs
// ここでuseしているのはSubcommandトレイトであって、SubCommand構造体ではない点に注意。
// 名前の"C"が大文字かどうかしか違いがなく、大変間違えやすい。
// SubCommand構造体はバージョン3.0からdeprecatedされており、今後廃止予定である。
use clap::{Parser, Subcommand};

#[derive(Parser)]
struct AppArg {
    #[clap(subcommand)]
    action: Action,
}

#[derive(Subcommand)]
enum Action {
    Echo { message: String },
    Greet { name: String },
}

fn main() {
    let cli = AppArg::parse();
    match cli.action {
        Action::Echo { message } => {
            println!("{}", message);
        },
        Action::Greet { name } => {
            println!("hello, {}", name);
        }
    }
}

AppとParserのどちらを使うべきか

個人的には処理とインターフェースの定義を分離しやすいParserを使うパターンの方が好みだが、細かい制御はApp::new()の方が得意である。例えば、2つのオプションの内一方のみを指定してもらいたい場合の制御はParserパターンの方が記述量が増えてしまうため不利である。

66
38
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
66
38

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?