Rustでコマンドラインツールを作ってみる

# 概要

Rustを色々触ってコマンドラインツールを作ってみる。
[Rust公式ドキュメント](https://www.rust-lang.org/ja/)

私は **helpman** というコマンドを作ってみました。現在、***v1.0.4***。
**help** や **man** コマンドは情報量が多いので、独自のマニュアルを新規作成、編集、閲覧できるものを作りました。コマンドの詳細は後日載せます。

[Git Release](https://github.com/tomo1227/helpman/releases/tag/v1.0.4)
[Crate.io](https://crates.io/crates/helpman)

# インストール

私の環境はMac(Intel)なので以下のコマンドを叩く。(Linuxも同じ)

```:terminal
% curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
```

Windowsは [rustup-init.exe](https://static.rust-lang.org/rustup/dist/i686-pc-windows-gnu/rustup-init.exe) をインストールする。以降はMacでの説明のみ。

`~/.cargo/bin`配下にツールがインストールされる。
[rustup](https://github.com/rust-lang/rustup/blob/master/README.md), [cargo](https://doc.rust-lang.org/cargo/index.html), [rustc](https://doc.rust-lang.org/rustc/index.html)などがある。

```:terminal
% ls ~/.cargo/bin/
cargo*         cargo-fmt*     clippy-driver* rust-gdb*      rust-lldb*     rustdoc*       rustup*
cargo-clippy*  cargo-miri*    rls*           rust-gdbgui*   rustc*         rustfmt*
```

### バージョン確認

```
% source "~/.cargo/env"
% rustc --version          
rustc 1.66.0 (69f9c33d7 2022-12-12)
% cargo --version
cargo 1.66.0 (d65d197ad 2022-11-15)
```

### 更新

`rustup update`でできる。

### アンインストール
アンインストールは`rustup self uninstall`で実行できる。

# Hello World

早速、Hello worldしてみる。
まず, **main.rs** を以下のように作る。

```rust:main.rs
fn main() {
    println!("Hello World!");
}
```

そして、実行

```:terminal
% rustc main.rs
% ./main       
Hello World!
```

# Cargo

CargoはRustのビルドシステム兼パッケージマネージャ。

```:terminal
% cargo new hello_cargo
     Created binary (application) `hello_cargo` package
% tree .
.
└── hello_cargo
    ├── Cargo.toml
    └── src
        └── main.rs
```

## 主なコマンド

### cargo build

ビルドして実行してみる。

```:terminal
% cargo build
   Compiling hello_cargo v0.1.0 
   (/Users/***/rust/cargo/hello_cargo)
    Finished dev [unoptimized + debuginfo] target(s) in 1.31s
% ./target/debug/hello_cargo
Hello, world!
```

リリース時は`cargo build --release`とすると、最適化し、target/releaseに実行ファイルが生成される。

### cargo run

`cargo run`を使うとこれらを一発でコンパイルできる。

```:terminal
% cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
     Running `target/debug/hello_cargo`
Hello, world!
```

### cargo check

`cargo check`をすると実行できるかを瞬時にチェックする。
実行ファイルを生成しないので、`cargo build` よりも高速。

```
% cargo check
    Checking hello_cargo v0.1.0 (/Users/Tomoki/workspace/rust/cargo/hello_cargo)
    Finished dev [unoptimized + debuginfo] target(s) in 0.56s
```

### cargo add

Cargo.tomlに自動で追記してくれる

```diff_toml:Cargo.toml
[dependencies]
+ anyhow = "1.0.68"
```

これは以下のように叩くだけでいい。

```:terminal
% cargo add anyhow@0.1.41
```

## フォーマッター

`rustfmt` と `cargo-fmt`を使う。(元から入ってる)
以下のように叩くと自動でフォーマットされる。

```:terminal
cargo fmt
```

## rustfix

自動でwarningを修正してくれる。

```
% cargo fix
```

## Linter

リンターでは`Clippy`というものがある。

```:terminal
% cargo clippy
```

# 型

Rustの文字列の型には **&str** と **String** がある。
Rustでは `&[T]` はsliceを表すので、**&str**はu8のsliceである。
それに対して、 **String** はu8のVectorである。
ちなみにStringから&strにするには&をつければいい。

型を出力する関数`print_type()`を以下に載せておく。

```
fn print_type<T>(_: T) {
    println!("{}", std::any::type_name::<T>());
}
```

# コマンドラインツール

```
% cargo new grrs
```

引数は標準ライブラリの`std::env::args()`を使用する。
引数がないと`expect()`に渡したものが出力される。

```rust:src/main.js
fn main() {
    let arg1 = std::env::args().nth(1).expect("no arg1 given");
    let arg2 = std::env::args().nth(2).expect("no arg2 given");
    println!("arg1 : {}, arg2 : {}", arg1, arg2);
}
```

## 実行結果

```
grrs % cargo run a b
arg1 : a, arg2 : b
% cargo run a
thread 'main' panicked at 'no arg2 given', src/main.rs:3:40
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
% cargo run
thread 'main' panicked at 'no arg1 given', src/main.rs:2:43
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
```

:::note warn
環境変数はRUST_BACKTRACEは以下のように設定できる
`export RUST_BACKTRACE=1`
fullも設定できる
`export RUST_BACKTRACE=full`
:::


## 構造体を使う

```rust:src/main.rs
use std::env;

struct User {
    name: String,
    age: u8,
}

fn main() {
    let arg1 = env::args().nth(1).expect("nameがありません");
    let arg2 = env::args().nth(2).expect("ageがありません");
    let args = User {
        name: arg1,
        age: arg2.parse().unwrap() 
    };
    println!(
        "name is {}, age is {}",
        args.name, args.age
    )
}
```

出力すると

```:terminal
% cargo run Taro 18
name is Taro, age is 18
```

### [derive(Debug)]

#[derive(Debug)]を使うと構造体を`fmt::Debug`で出力する

```rust:src/main.rs
use std::env;

#[derive(Debug)]
struct User {
    name: String,
    age: u8,
}

fn main() {
    let arg1 = env::args().nth(1).expect("nameがありません");
    let arg2 = env::args().nth(2).expect("ageがありません");
    let args = User {
        name: arg1,
        age: arg2.parse().unwrap()
    };
    println!("「:?」を使った場合  {:?}", args);
    println!("「:#?」を使った場合 is {:#?}", args);
    println!("「1:?」 : {1:?}, 「0:?」 : {0:?}", args.name, args.age);
}
```

出力すると

```:terminal
 % cargo run Taro 18
「:?」を使った場合  User { name: "Taro", age: 18 }
「:#?」を使った場合 is User {
    name: "Taro",
    age: 18,
}
「1:?」 : 18, 「0:?」 : "Taro"
```

##clap

Cargo.tomlにclapを追加する。

```diff_toml:Cargo.toml
[package]
name = "grrs"
version = "0.1.0"
edition = "2021"

+ [dependencies]
+ clap = { version = "4.0", features = ["derive"] }
```

以下のようなfoo.textを用意。
```text:foo.txt
abcd
1234
pqrs
9876
```

main.rsを以下のように編集する。

```rust:src/main.rs
#![allow(unused)]
use clap::Parser;

#[derive(Parser)]
struct Cli {
    text: String,
    file: std::path::PathBuf,
}

fn main() {
    let args = Cli::parse();
    let content = std::fs::read_to_string(&args.file).expect("could not read file");
    println!("input : {}", args.text);
    println!("file contents  : {}", content);
}
```

実行するとエラーがいい感じに出力される。
引数は--の後に入力する。--の代わりに main,もしくは省略も可。
ただし、--helpの時は省略不可。


```:terminal
% cargo run                
    Finished dev [unoptimized + debuginfo] target(s) in 0.17s
     Running `target/debug/grrs`
error: The following required arguments were not provided:
  <TEXT>
  <FILE>

Usage: grrs <TEXT> <FILE>

For more information try '--help'
% cargo run -- --help
    Finished dev [unoptimized + debuginfo] target(s) in 0.08s
     Running `target/debug/grrs --help`
Usage: grrs <TEXT> <FILE>

Arguments:
  <TEXT>  
  <FILE>  

Options:
  -h, --help  Print help information
% cargo run -- hoge foo.txt
    Finished dev [unoptimized + debuginfo] target(s) in 0.08s
     Running `target/debug/grrs hoge foo.txt`
input : hoge
file contents  : abcd
1234
pqrs
9876
```

## ファイルを読み込む

std::fs::read_to_string()でファイルを読み込み、１つ目の引数を含む行を出力している。

```rust:src/main.rs
#![allow(unused)]
use clap::Parser;

#[derive(Parser)]
struct Cli {
    text: String,
    file: std::path::PathBuf,
}

fn main() {
    let args = Cli::parse();
    let content = std::fs::read_to_string(&args.file).expect("could not read file");

    for line in content.lines() {
        // textを含む行を出力
        if line.contains(&args.text) {
            println!("{}", line);
        }
    }
}
```

### 実行結果

```:terminal
% cargo run -- 98 foo.txt
    Finished dev [unoptimized + debuginfo] target(s) in 0.09s
     Running `target/debug/grrs 98 foo.txt`
9876
```

## match & unwrap & expect

fileが見つからなかった時は **unwrap** で **panic** を起こすか
**match** で明示的にエラー処理を行う。
**expect** よりパフォーマンスが優れている。 

### matchを使った場合

```diff_rust:src/main.rs
#![allow(unused)]
use clap::Parser;

#[derive(Parser)]
struct Cli {
    text: String,
    file: std::path::PathBuf,
}

fn main() {
    let args = Cli::parse();
-   let content = std::fs::read_to_string(&args.file).expect("could not read file");
+   let result = std::fs::read_to_string(&args.file);
+   let content = match result {
+       Ok(content) => content,
+       Err(error) => {
+           panic!("エラー内容 : {}", error);
+       }
+   };
    println!("{}", content);
}
```
#### 実行結果

```:terminal
% cargo run -- a a.txt
    Finished dev [unoptimized + debuginfo] target(s) in 0.82s
     Running `target/debug/grrs a a.txt`
thread 'main' panicked at 'エラー内容 : No such file or directory (os error 2)', src/main.rs:21:13
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
```

### unwrap

unwrapだと以下のように簡潔にかける。

```diff_rust:src/main.rs
//***省略***
fn main() {
    let args = Cli::parse();
+   let content = std::fs::read_to_string(&args.file).unwrap();
+   println!("{}", content);
}
```

#### 実行結果

```:terminal
% cargo run a a.txt
    Finished dev [unoptimized + debuginfo] target(s) in 1.04s
     Running `target/debug/grrs a a.txt`
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:12:55
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
```


panic!を起こさずにエラーを出力することも可能。

```rust:src/main.rs
//***省略***

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let args = Cli::parse();
    let result = std::fs::read_to_string(&args.file);
    let content = match result {
        Ok(content) => content,
        Err(error) => {
            return Err(error.into());
        }
    };
    println!("{}", content);
    Ok(())
}
```

#### 実行結果

```:terminal
% cargo run a a.txt
    Finished dev [unoptimized + debuginfo] target(s) in 1.39s
     Running `target/debug/grrs a a.txt`
Error: Os { code: 2, kind: NotFound, message: "No such file or directory" }
```

:::note alert
`Ok(())`を`Ok(());`としてはいけません。
戻り値のある関数はreturnを明示していない最終行で`;`はつけてはいけない
:::

### 「?」 を使ったらもっと簡潔にかける

```rust:src/main.rs
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let args = Cli::parse();
    let content = std::fs::read_to_string(&args.file)?;
    println!("{}", content);
    Ok(())
}
```
#### 実行結果

```:terminal
% cargo run a a.txt
    Finished dev [unoptimized + debuginfo] target(s) in 1.52s
     Running `target/debug/grrs a a.txt`
Error: Os { code: 2, kind: NotFound, message: "No such file or directory" }
```

### 「?」を使ったときのエラーのカスタマイズ

```rust:src/main.rs
// ***省略***
#[derive(Debug)]
struct CustomError(String);

fn main() -> Result<(), CustomError> {
    let args = Cli::parse();
    let content = std::fs::read_to_string(&args.file)
        .map_err(|err| CustomError(format!("エラー内容: `{}`: {}", args.file.display(), err)))?;

    println!("{}", content);
    Ok(())
}
```

#### 実行結果

```:terminal
% cargo run a a.txt
   Compiling grrs v0.1.0 (/Users/Tomoki/workspace/rust/cargo/grrs)
    Finished dev [unoptimized + debuginfo] target(s) in 1.42s
     Running `target/debug/grrs a a.txt`
Error: CustomError("エラー内容: `a.txt`: No such file or directory (os error 2)")
```

##  anyhow

anyhowを使うともっとスマートにかける。
まず、**Cargo.toml**に追加する。

```diff_toml:Cargo.toml
[dependencies]
clap = { version = "4.0", features = ["derive"] }
+ anyhow = "1.0"
```

以下のように **main.rs**　を修正。

```diff_rust:src/main.rs
#![allow(unused)]
+ use anyhow::{Context, Result};
use clap::Parser;

// ***省略***

#[derive(Debug)]
struct CustomError(String);
+ fn main() -> Result<()> {
    let args = Cli::parse();
+   let content = std::fs::read_to_string(&args.file)
+       .with_context(|| format!("エラー内容 : {}が見つかりません", args.file.display()))?;

    println!("{}", content);
    Ok(())
}
```

#### 実行結果

```:terminal
% cargo run a a.txt
    Finished dev [unoptimized + debuginfo] target(s) in 0.82s
     Running `target/debug/grrs a a.txt`
Error: エラー内容 : a.txtが見つかりません

Caused by:
    No such file or directory (os error 2)
```

## output

### eprintln!

`println!`は標準出力、`eprintln!`は標準エラー出力である。

### 高速な標準出力

#### 普通のprintln!

```rust
fn main() {
    for _ in 0..10_000_000 {
        println!("");
    }
}
```

速度を計測すると **3.97s**

```:terminal
% time cargo run > /dev/null
    Finished dev [unoptimized + debuginfo] target(s) in 0.08s
     Running `target/debug/grrs`
cargo run > /dev/null  3.97s user 3.18s system 97% cpu 7.358 total
```


#### writeln!

```rust:src/main.rs
use std::io::{stdout, Write};

fn main() {
    let out = stdout();
    let mut out = out.lock();
    for _ in 0..10_000_000 {
        writeln!(out, "").unwrap();
    }
}
```

速度は **3.72s** となり少し速くなった。

```:terminal
% time cargo run > /dev/null
    Finished dev [unoptimized + debuginfo] target(s) in 0.08s
     Running `target/debug/grrs`
cargo run > /dev/null  3.72s user 3.12s system 97% cpu 7.008 total
```

#### write!

`writeln!` の代わりに `write!` を使う。

```rust:src/main.rs
use std::io::{stdout, Write};

fn main() {
    let out = stdout();
    let mut out = out.lock();
    for _ in 0..10_000_000 {
        write!(out, "").unwrap();
    }
}
```

速度は **0.53s** と数倍速くなった。

```:terminal
% time cargo run > /dev/null
    Finished dev [unoptimized + debuginfo] target(s) in 0.08s
     Running `target/debug/grrs`
cargo run > /dev/null  0.53s user 0.03s system 77% cpu 0.732 total
```

#### バッファリング

デフォルトではバッファリングされないので、バッファリングしてあげる。

```rust:src/main.rs
#![allow(unused)]
use std::io::{stdout, BufWriter, Write};

fn main() {
    let stdout = stdout();
    let mut handle = BufWriter::new(stdout); // optional: wrap that handle in a buffer
    for _ in 0..10_000_000 {
        writeln!(handle, "foo");
    }
}
```

速度は **0.94s**

```:terminal
% time cargo run > /dev/null
    Finished dev [unoptimized + debuginfo] target(s) in 0.08s
     Running `target/debug/grrs`
cargo run > /dev/null  0.94s user 0.03s system 85% cpu 1.143 total
```

またバッファリングは以下のようにも書ける。

```rust:src/main.rs
use std::io::{stdout, Write, BufWriter};

fn main() {
    let out = stdout();
    let mut out = BufWriter::new(out.lock());
    for _ in 0..10_000_000 {
        writeln!(out, "").unwrap();
    }
}
```

こちらも速度は **0.94s** となる。

```:terminal
% time cargo run > /dev/null
    Finished dev [unoptimized + debuginfo] target(s) in 0.08s
     Running `target/debug/grrs`
cargo run > /dev/null  0.94s user 0.04s system 85% cpu 1.138 total
```

## Progress bar

Cargo.tomlに **indicatif** を追記。

```diff_toml:Cargo.toml
[dependencies]
+ indicatif = {version = "*", features = ["rayon"]}
```

main.rsを以下のように編集。

```rust:src/main.rs
#![allow(unused)]
use indicatif::ProgressBar;

fn main() {
    let pb = ProgressBar::new(100_000_000);
    for i in 0..100_000_000 {
        pb.inc(1);
    }
    pb.finish_with_message("done");
}
```

これを実行するとプログレスバーが出てくる。

```:terminal
% cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.12s
     Running `target/debug/grrs`
█████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 11674248/100000000
```

## Logging

`Cargo.toml`に **env_logger** と **log** を追記する。

```diff_toml:Cargo.toml
[dependencies]
+ env_logger = "0.9.0"
+ log = "0.4"
```

`src/bin/output-log.rs`を作成

```src/bin/output-log.rs
use log::{info, warn};
use env_logger;

fn main() {
    env_logger::init();
    info!("starting up");
    warn!("oops, nothing implemented!");
}
```

#### 実行結果

log levelは `error`, `warn`, `info`, `debug`, `trace`がある。

```:terminal
% env RUST_LOG=info cargo run --bin output-log
    Finished dev [unoptimized + debuginfo] target(s) in 1.15s
     Running `target/debug/log`
[2023-01-02T08:20:40Z INFO  log] starting up
[2023-01-02T08:20:40Z WARN  log] oops, nothing implemented!
```

# クロスコンパイラ

クロスコンパイラの比較については [Rust でバイナリを配布する](https://zenn.dev/coord_e/articles/portable-binary-in-rust) がよくまとまっていた。

## cross

[公式ドキュメント](https://github.com/cross-rs/cross)

まずはインストールする。

```:terminal
cargo install cross --git https://github.com/cross-rs/cros
```

以下の2つのコマンドのどちらかを叩くと対象の環境のリストが表示される。

```:terminal
rustc --print target-list
rustup target list
```

例えば

```:terminal
// Linuxの場合
% cross build --target x86_64-unknown-linux-gnu
// Windowsの場合
% cross build --target x86_64-pc-windows-gnu
// Mac (m1の場合)
$ rustup target add aarch64-apple-darwin               
$ cross build --target aarch64-apple-darwin
```

## CentOS7

centOS7はglibcが2.28ではなく、2.27を使用しているので、上記の方法だとエラーが出る。
`cross`でも以下のようにCross.tomlに追加したらいけるらしい。公式ドキュメントに記載。

```toml:Cross.toml
[target.x86_64-unknown-linux-gnu]
image = "ghcr.io/cross-rs/x86_64-unknown-linux-gnu:main-centos"
```

今回は`cargo zigbuild`を使う。
事前に **zig** をインストールしておかなければならない。

```:terminal
% rustup target add x86_64-unknown-linux-gnu
% brew install zig
% cargo zigbuild --target=x86_64-unknown-linux-gnu.2.17
```

# バイナリ配布方法

## Git Release

一番手っ取り早い。
しかし、配布方法が基本的に`.tar.gz`か`.zip`なので解凍する必要がある。
また、Passを通す必要があるので、面倒である。

## brew

homebrew (tap)
tapを利用せずにインストールさせたい場合は、 homebrew-core に対してpull requestを送って取り込んでもらうことで可能になる。 [いくつかのルール](https://docs.brew.sh/Acceptable-Formulae) を満たす必要がある。

## その他

代表的なパッケージマネージャを挙げておく。

* rpm
* yum
* dpkg

# 参考文献

コマンドツールを作る際にお世話になった記事。

1. [Rust公式ドキュメント](https://www.rust-lang.org/ja/)
2.  [The Rust Programming Language 日本語版
](https://doc.rust-jp.rs/book-ja/)
3.  [Git-clippy](https://github.com/rust-lang/rust-clippy)
4.  [エラーが出たときの RUST_BACKTRACE=1 を設定する](https://www.suzu6.net/posts/294-rust-backtrace/)
5.  [&strとStringを理解しようと思ったらsliceやmutを理解できてないことに気づいた話](https://qiita.com/yagince/items/e7474839246ced595f7a)
6.  [Rustのデータ型](https://qiita.com/tonesuke/items/733fdb2fa96a6f8374f8)  
7. [命名](https://sinkuu.github.io/api-guidelines/naming.html) 
8. [rust String &str の変換と、文字列 数値 の変換](https://qiita.com/smicle/items/29a4d5d1d14ad7f77f60)
9. [Rustの所有権について](https://blog.foresta.me/posts/rust_ownership/)
9. [expect()よりunwrap_or_else()を使うべき場合](https://qiita.com/garkimasera/items/f39d2900f20c90d13259)
9. [Rust のエラーハンドリングはシンタックスシュガーが豊富で完全に初見殺しなので自信を持って使えるように整理してみたら完全に理解した](https://qiita.com/nirasan/items/321e7cc42e0e0f238254#unwrap-%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89)
9. [Rustの行末セミコロンはいるのかいらないのか](https://www.saluteweb.net/~oss_rustsemicolon.html)
9. [Rustのコマンドライン解析ライブラリ『Clap』 〜導入編〜](https://qiita.com/emonuh/items/41f7bba5283c732b0209)
9. [Rustで手軽にCLIツールを作れるclapを軽く紹介する](https://qiita.com/Tadahiro_Yamamura/items/4ae32347fb4be07ea642#app%E3%81%A8parser%E3%81%AE%E3%81%A9%E3%81%A1%E3%82%89%E3%82%92%E4%BD%BF%E3%81%86%E3%81%B9%E3%81%8D%E3%81%8B)
9. [Rust で derive(Debug) を安心して使うために秘匿情報をマスクする](https://qiita.com/jwhaco/items/924dc8c488c98fd412f6)
9. [Rustで高速な標準出力](drive.google.com/drive/u/2/folders/1nq8vnhamvH3jAsPM--OwSjjb2UVsocOZ)
9. [Rust：logでログ出力を行う](https://qiita.com/fujitayy/items/590145c0f4b4e7d06de7)
9. [Rust のエラーハンドリングはシンタックスシュガーが豊富で完全に初見殺しなので自信を持って使えるように整理してみたら完全に理解した](https://qiita.com/nirasan/items/321e7cc42e0e0f238254)
9. [RustのCLI作成チュートリアルやっている中のメモ書き](https://zenn.dev/anchor_cable/scraps/1a237e288f046d)
9. [clap v3 のdriveベースのパースのサンプル](https://igaguri.hatenablog.com/entry/2022/01/18/175758)
9. [Rust における `From<T>` とか `Into<T>` とかの考え方](https://qiita.com/hadashiA/items/d0c34a4ba74564337d2f)
9. [std::any::type_name はバッチリ型名を返してくれるわけではなさそう](https://rnitta.hatenablog.com/entry/2020/03/21/103850)
9. [Rust入門](https://zenn.dev/mebiusbox/books/22d4c1ed9b0003/viewer/6d5875)
9. [Rust を始める時に少しだけ読み書きしやすくなる Result と Option の話](https://eng-blog.iij.ad.jp/archives/11405)
9. [Rust でバイナリを配布する](https://zenn.dev/coord_e/articles/portable-binary-in-rust#musl-%E3%81%A7%E9%9D%99%E7%9A%84%E3%83%AA%E3%83%B3%E3%82%AF)
9. [RustでWindows⇔Linuxをクロスコンパイルする](https://own-search-and-study.xyz/2020/04/19/rust-windows-linux-cross-compile/)
9. [Rust でバイナリを配布する](https://zenn.dev/coord_e/articles/portable-binary-in-rust)