RustでCLIを作成してGitHub Actionsでリリースするまで


## はじめに

**この記事を書くことになった背景がつらつら書いているので、読み飛ばしていただいても構いません。**

唐突ですが、オライリーを読んでいて以下のようなオライリーの短縮 URL に出会うことは多くないですか？

```shell
https://oreil.ly/XXXXX
```

私は oreil.ly の綴りが覚えられず、またブラウザーを開いて毎回このリンクを打つのが億劫でした。

そこで URL のパスを打ち込むだけで自動でブラウザーが開き URL にアクセスしてくれる CLI ツールを Rust で作成しました。

https://github.com/u-kai/ourl

Rust はクロスコンパイルが可能でかつ、Clap という CLI ライブラリがとても使いやすいので、簡単に CLI ツールを作成することができます。
また、GitHub Actions を使うことで GitHub にコードを Push するだけで複数の OS に対応したバイナリをリリースすることができ、便利です。

**そこで今回は Rust で CLI ツールを作成し、 GitHub でリリースする一連の流れを紹介します。**
(前置き長くてすみません...)

今回作成した ourl(Open(oreil.ly) URL) という CLI ツールを例に進めていきます。

## Rust の CLI ツールを作成する

### 基本準備

まずは以下のコマンドで Rust のプロジェクトを開始します。

```shell
$cargo new PROJECT_NAME
```

基本的には PROJECT_NAME が CLI の名前になります。今回私は ourl という CLI ツールを作成したので、以下のコマンドでプロジェクトを作成しました。

```shell
$cargo new ourl
```

次にライブラリをインストールします。
上述したように今回は Clap というライブラリを使って CLI ツールを作成します。そのため以下の記述を Cargo.toml に追加し、依存関係を追加してください。

```toml
[dependencies]
clap = { version = "4.0.0", features = ["derive"] }
```

features に derive を追加することで、Clap の Parser derive マクロを使うことができます。

### 処理を記述する

#### 構造体の定義

Parser の derive マクロを利用して、以下のように直感的に CLI のオプションを定義することができます。

```rust
use clap::Parser;

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

#[derive(Parser)]
struct Cli {
    #[clap(
        short = 'd',
        long = "domain",
        default_value = "oreil.ly",
        help = r#"The domain name to open in the browser. You can override this by setting the `DEFAULT_SURL_DOMAIN` environment variable."#
    )]
    domain: String,
    #[clap(help = "The URL path to open in the browser.")]
    path: String,
}
```

上の構造体 Cli の domain や path プロパティの上にある#[clap]マクロを使うことで、オプションの定義ができます。

short は char で-を使った短いオプション、long は 文字列で--を使った長いオプションを定義します。
これらを指定しないと、引数として扱われ、上記の例では path は引数として扱われます。

default_value はデフォルト値を設定します。
default_value がないかつ、プロパティの型が Option\<T\> でない場合は実行時に必須のオプションとなります。
今回で言うと引数 path は必須で、オプション domain は指定されない場合は oreil.ly がデフォルト値となります。

help は-h や --help で表示されるヘルプメッセージを設定します。
今回の CLI で-h や --help で表示されるヘルプメッセージは以下のようになります。

```shell

$ ourl -h
Usage: ourl [OPTIONS] <PATH>

Arguments:
  <PATH>  The URL path to open in the browser.

Options:
  -d, --domain <DOMAIN>  The domain name to open in the browser. You can override this by setting the `DEFAULT_SURL_DOMAIN` environment variable. [default: oreil.ly]
  -h, --help             Print help
```

構造体に#[derive(Parser)]をつけることで、Cli::parse()が自動で実装され、これだけで CLI から引数やオプションをパースすることができます。
個人的にめちゃめちゃ便利だなーと思っています。

#### 処理を記述する

構造体を定義できたらその構造体を使った処理を書くだけです。
Cli::parse()メソッドで入力されたコマンドがパースされて構造体が作成されるので、それを使って処理を記述します。

```rust
fn main() {
    let cli = Cli::parse();
    cli.run();
}

impl Cli {
    fn run(&self) {
        open(self.open_url().as_str());
    }
    ...
}
```

#### テストを記述する

実際に CLI が動くかどうかテストを書いておくと安心です。
ただ、CLI は入力を受け取って動作する特性上テストが難しいのです。

そこで Parser を実装している構造体には parse_from メソッドが実装されており、これを使ってテストを書くことができます。

```rust
#[test]
fn open_url_can_specify_domain() {
    let args = vec!["ourl", "Test1", "-d", "example.com"];
    let cli = Cli::parse_from(args);
    assert_eq!(cli.open_url(), "https://example.com/Test1");
}
```

args には標準入力を模擬した文字列のベクターを渡して、Cli::parse_from(args)で構造体を作成出来ます。
これにより標準入力を模擬した構造体を作成できるので、標準入力を行わずにテストを行うことができます。

#### おまけ:OS によって処理を変える

最初に紹介したように Rust はクロスコンパイルが可能です。
また cfg マクロを使うことで、OS によって処理を変えることができます。

cfg マクロは様々な条件を指定してコンパイル時の処理を変えることができます。

よく使うのが #[cfg(test)]だと思っていて、このマクロを使うとテスト実行時以外はコンパイル対象から外れるようになります。

OS に関しても #[cfg(target_os=OS_NAME)]で指定することが出来ます。
今回はブラウザーを開く処理を OS によって変えるために以下のように記述しました。

```rust
#[cfg(target_os = "macos")]
fn open(url: &str) {
    Command::new("open")
        .arg(url)
        .spawn()
        .expect(format!("Failed to open {}", url).as_str());
}

#[cfg(target_os = "linux")]
fn open(url: &str) {
    Command::new("xdg-open")
        .arg(url)
        .spawn()
        .expect(format!("Failed to open {}", url).as_str());
}

#[cfg(target_os = "windows")]
fn open(url: &str) {
    Command::new("cmd")
        .arg("/C")
        .arg("start")
        .arg(url)
        .spawn()
        .expect(format!("Failed to open {}", url).as_str());
}
```

このように同じ関数であっても cfg マクロによって最終的にコンパイルされる処理が変わるため、コンパイルエラーを吐きませんし、OS によって処理を変えることができます。

やはりマクロは最高ですね。(自作は大変ですが。。。)

## GitHub を使ってリリースする

最後に GitHub でリリースする方法を紹介します。
Rust における GitHub Actions のリリースは[こちらの記事](https://qiita.com/okmt765/items/ad3cdfb5850e68edcef0)がとても参考になりました。

actions の設定を一部最新のものに変更したものを私のリポジトリにあげているので、参考にしてください。

個人的に softprops/action-gh-release という actions が GitHub にリリースを簡単にアップロードすることができて感動しています。

おそらく[こちらのファイル](https://github.com/u-kai/ourl/blob/main/.github/workflows/release.yaml)をリポジトリに追加するだけでリリースが可能と思います。
PROJECT_NAME などの環境変数は適宜変更してください。

これで GitHub にコードを Push するだけで複数の OS に対応したバイナリをリリースすることができます。

ぜひ試してみてください！

## 終わりに

今回は Rust で CLI ツールを作成し、 GitHub でリリースする一連の流れを紹介しました。
Rust や Clap の機能を紹介したことで、改めて Rust の強力さを感じました。
また、GitHub Actions による CLI のリリースをとても便利で、CLI の配布にはピッタリだなと感じました。

もしよければ私が作った ourl を使ってみていただけると嬉しいです！(オライリーの読書が捗るはず...)

最後まで読んでいただきありがとうございました。
何か質問やご指摘があればお気軽にコメントしていただけると嬉しいです。

## 参考リンク

https://qiita.com/okmt765/items/ad3cdfb5850e68edcef0