Rustでcatコマンドを書く

catと言っても簡易的なもので、与えられた引数のファイルの中身を表示するだけで、オプションとかそんなものはありません。
Rustの勉強用に書いてみました。
というわけで早速コード。

use std::env;
use std::fs::File;
use std::io::{BufReader, BufRead};

fn main() {
    // コマンドライン引数の取得
    let f_name = env::args().nth(1).unwrap();
    // 引数のファイル名を受け取りファイルを開く
    let f = File::open(f_name).unwrap();
    // バッファリングのためにBufReaderを使う
    let f = BufReader::new(f);
    // 一行ずつ取得して出力
    for line in f.lines() {
        println!("{}", line.unwrap());
    }
}

test.txtとか適当なファイルを用意して、実行してみます。

$ rustc cat.rs
$ ./cat test.txt
hello
world

できてますね。

ところで、コードの中に時々出てくるunwrap()とは何でしょう。

unwrap()は列挙型の1つであるOption<T>型やResult<T,E>型の値から、Tの値を取得するために使われます。

Optional<T>型やResult<T,E>型?よくわからないけど何でそんなん使うの?unwrap()使わずに初めから値を返してくれればよくない?」と、思うかもしれませんが、これはエラーハンドリングにおいて重要な役割を果たしています。

例えば、先ほどの簡易catに、引数を与えず実行してみます。

$ ./cat
thread 'main' panicked at 'called `Option::unwrap()` on a `None` value', libcore/option.rs:345:21
note: Run with `RUST_BACKTRACE=1` for a backtrace.

はい、エラーが表示されました。
このとき、もう少し親切なエラーメッセージを表示したいとなった場合、どうすれば良いでしょう?
これは、引数に与えたファイル名が存在しなかった場合も同様です。
「例外処理で書けばいいのかなー」と思うかもしれません。

実は、Rustにはいわゆる例外の仕組みが存在せず、エラーも戻り値で表します。
なので、catchしてthrowする、みたいなことはできません。

そこで、戻り値をOption<T>型やResult<T,E>型で返し、それらをmatch式と呼ばれるものと組み合わせて、エラーハンドリングを実現します。

具体的には以下のような記述になります。

// Option<T>型の場合
match Option<T> {
    Some(T) => {
        // 値があれば処理を続行
    },
    None => {
        // 値がなければエラー処理
    }
}

// Result<T,E>型の場合
match Result<T, E> {
    Ok(T) => {
        // 値があれば処理を続行
    },
    Err(E) => {
        // 値がなければエラー処理
    }
}

では早速、簡易catのmain()を書き直してみましょう。

fn main() {
    match env::args().nth(1) {
        Some(f_name) => {
            match File::open(f_name) {
                Ok(f) => {
                    for line in BufReader::new(f).lines() {
                        match line {
                            Ok(l) => println!("{}", l),
                            Err(e) => {
                                println!("Error: Cannot read a line. {}", e);
                                return;
                            }
                        }
                    }
                }
                Err(e) => {
                    println!("Error: Cannot open the file. {}", e);
                    return;
                }
            }
        },
        None => {
            println!("Error: No args.");
            return;
        }
    }
}
$ rustc cat.rs
$ ./cat test.txt
hello
world

一応、できてますね。引数を与えずに実行してみます。

$ ./cat
Error: No args.

こちらで用意したエラーメッセージが表示されました。
存在しないファイル名も渡してみます。

$ ./cat foo
Error: Cannot open the file. No such file or directory (os error 2)

いい感じ。

このように、unwrap()を使えば簡単に値を取得できるのですが、基本的にはunwrap()を使わず、適切なエラー処理を記述していくのが良いですね。

それにしても、これ、ネスト深くなりすぎてますよね。。。
書き換えましょう。

fn main() {
    let f_name = match env::args().nth(1) {
        Some(f_name) => f_name,
        None => {
            println!("Error: No args.");
            return;
        }
    };

    let f = match File::open(f_name) {
        Ok(f) => f,
        Err(e) => {
            println!("Error: Cannot open the file. {}", e);
            return;
        }
    };

    let reader = BufReader::new(f);
    for line in reader.lines() {
        let l = match line {
            Ok(l) => l,
            Err(e) => {
                println!("Error: Cannot read a line. {}", e);
                return;
            }
        };
        println!("{}", l);
    }
}

少しマシになりました。
というわけで、今後もたまにRust書きます。