Rust 研究：コマンドライン引数、エラー処理、ソースコード分割、終了コードなど

最近 Rust の勉強をはじめた初心者です。

入門書のサンプルコードを改造してエラー処理などを追加してコマンドプロンプト用のコマンドを作ってみましたのでそのメモを残します。

**!!!コメントを頂いたのでソースコードを修正しました。!!!**

## [Before] お題となるサンプルコード

テキストファイルをオープンして中身を標準出力に表示するサンプルコード。

```rust
// main.rs
use std::fs;

fn main() {
    let content = fs::read_to_string("file.txt").unwrap();
    println!("{}", content);
}
```

これに以下を実装していきます。

* main 関数と基本処理の分離
* コマンドライン引数対応
* エラー処理の追加
* ソースコードの分割
* 終了コード対応


## [AFTER] コマンド化

コマンド仕様を次のように設定した。

### コマンドの書式

```
% preview.exe 表示対象となるテキストファイルのパス
```

### 終了コード

シェルスクリプトやバッチファイルで使用することを想定し、コマンドの「終了コード」を設定する。
エラーの際には標準エラーにエラーメッセージを表示する。

|終了状態|終了コード|メッセージ|
|:--:|:--:|:--|
|正常|0|(なし)|
|引数エラー|100|[USAGE] preview.exe file_path|
|IOエラー|101|[ERROR] IO Error: エラーの理由(ファイルが存在しないとか)|
|空ファイルエラー|102|[ERROR] EMPTY FILE: ファイル名|

### 作成したソースコード

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

use preview::tool::{read_text_file,log};
use preview::def::ErrorCode;

// main は処理結果に応じた ErrorCode を返すことで終了コードを設定する
fn main() -> ErrorCode {

    // 引数チェック
    let args: Vec<String> = env::args().collect();
    if args.len() < 2 {
        print_usage(&args[0]);
        return ErrorCode::ArgError
    }

    // テキストファイルの内容の取得
    let file_path = &args[1];
    let content = match read_text_file(file_path) {
        Ok(content) => content,
        Err(err) => {
            // エラーメッセージを出力
            log(&err);
            return err
        },
    };

    // テキストファイルの内容の表示
    println!("{}", content);

    ErrorCode::Success
}

fn print_usage(exeCommandName: &String) {
    eprintln!("[USAGE] {} file_path", exeCommandName);
}
```

```rust
// lib.rs
pub mod def;
pub mod tool;
```

```rust
use std::process::{Termination,ExitCode};

// ErrorCode: エラーコードを表現する enum 型
pub enum ErrorCode {
    Success,
    ArgError,
    IOError(String),
    EmptyFileError(String),
}

impl ErrorCode {
    // to_value: エラーコードの数値表現を返すメソッド
    pub fn to_value(&self) -> u8 {
        match self {
            ErrorCode::Success => 0,
            ErrorCode::ArgError => 100,
            ErrorCode::IOError(_) => 101,
            ErrorCode::EmptyFileError(_) => 102,
        }
    }

    // to_string: エラーコードの文字列表現を返すメソッド
    pub fn to_string(&self) -> String {
        match self {
            ErrorCode::Success => "Success".to_string(),
            ErrorCode::ArgError => "Argument Error".to_string(),
            ErrorCode::IOError(msg) => format!("IO Error: {}", msg),
            ErrorCode::EmptyFileError(file_path) => format!("EMPTY FILE: {}", file_path),
        }
    }
}

// ErrorCode に Termination トレイトを実装する
impl Termination for ErrorCode {
    fn report(self) -> ExitCode {
        ExitCode::from(self.to_value())
    }
}
```

```rust
// tool.rs
use std::fs;
use crate::def::ErrorCode;

// read_text_file: 指定されたテキストファイルの内容を返す関数
pub fn read_text_file(file_path: &String) -> Result<String, ErrorCode> {
    let content = match fs::read_to_string(file_path) {
        Ok(content) => content,
        Err(err) => return Err(ErrorCode::IOError(format!("{:?}", err))),
    };
    if content.len() < 1 {
        return Err(ErrorCode::EmptyFileError(file_path.to_string()))
    }
    Ok(content)
}

// log: エラーメッセージを標準エラーに表示する関数
pub fn log(err: &ErrorCode) {
    eprintln!("[ERROR] {}", err.to_string())
}
```

ここまでの感想。

* 命名規則は意識的に型名＝キャメルケース／変数名・関数名＝スネイクケースにしてみました。このあたりは適切なルールがありそうな気がします。
* Rust の enum 型は「値」を持たせることができて面白いですね。今回はエラーメッセージの一部を持たせてみました。
* ErrorCode 型の to_value / to_string のところ、なんとなく冗長な気がします。もっと簡潔に実装できるのでは。。。
* 今回は独自にエラー用の enum 型を実装してみましたけど myerror を使う方法はこれから。。。
* Rust のコンパイラのメッセージはなかなかいいですね。メッセージに従ってソースを修正できることが多くて勉強になりました。
* 標準出力・標準エラーに表示するところは中身もよくわからず println! / eprintln! マクロを使用。本当にマクロにお任せでよいのでしょうか？
* コマンドのエラーメッセージは標準エラーに出力するのではなく、ロガーライブラリに出力させたい。。。
* 特にセミコロンの必要な箇所などをコンパイラに怒られながら、Rust の構文って関数型の影響受けてると感じました。
* Rust 入門の本やブログで Result や Option をちゃんと説明せずに慣用句的に unwrap や ? を使ってエラー処理を省略したサンプルコードを提示してるものが多い印象。たとえ面倒でもエラー処理をきっちり勉強させることは大切だと思います。

**コメント対応**

std::process:exit の説明によると

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960096/dcc0ea3b-602e-e2e0-bee6-7aea932be768.png)

【意訳】

> この関数は処理が戻ることなくプロセスを終了させるため、動作中のスタックや他のスレッドのスタック上のデストラクタは実行されないことに注意しましょう。
> きれいにシャットダウンさせる必要があるときには実行すべきデストラクタがもう残留していないことが判明したときのみこの関数を呼び出しましょう。
> あるいは、できれば main 関数から Termination を実装した型 (ExitCode や Result）を返すようにして exit 関数の利用は避けましょう。

https://doc.rust-lang.org/std/process/fn.exit.html

今回の根幹となる std::fs::read_to_string の呼び出しの部分が完了したあとの話なのでほぼ影響はなさそうですが、「慣習」として安易に exit 関数を使用するのはよしましょうということですね。

お作法に基づき std::process:Termination トレイトを ErrorCode に実装し、main 関数は ErrorCode を返すようにしました。

いやー Rust はまさに初見殺しのプログラミング言語ですね。