Rustでheadコマンドを作ってみる

はじめに

この記事はAizu Advent Calendar 2017の19日目の記事です。

adventar.org

2月以降ブログは全く書いてなくて、約10ヶ月ぶりです。natumnです。
最近はアルバイトでフロントエンドをかき、個人的にバックエンドやシステムプログラムをかくみたいな生活を送っています。


そこで今ちょうど、「ふつうのLinuxプログラミング(第二版)」という本を読んでるのですが、本の中に「headコマンドを作る」という章がありました。
そのままC言語で写経してもあまり面白くないので、自分が今勉強中であるRustでheadコマンドを実装していきたいと思います。

環境

cargoバージョン 0.19.0
Rustバージョン 1.18.0

目標

今回は以下のような仕様を満たすコマンドを作っていきたいと思います。

  • 第一引数はrhead
  • nオプションとその後ろの数字で行数指定
  • ファイル名指定

例)

$ rhead -n 5 cat.txt

プログラムは

github.com

にあります。

それでは、作っていきましょう!

コマンドラインの解析を行う

まずコマンドラインの解析だけを行います。
ツールは公式から提供されているgetoptsというcrateを利用します。

doc.rust-lang.org

まずは rhead、-nオプションとそれに続く行数、-rファイル名の3つを取得します。
以下のように書きます。

    // argsを取得
    let args: Vec<String> = env::args().collect();
    let program = args[0].clone();
    let filename = Some(args[4].clone());

    // optionの設定
    let mut opts = Options::new();
    opts.optopt("n", "", "set number of lines", "LINES");
    opts.optflag("h", "help", "print this help menu");

    // optionの解析
    let matches = match opts.parse(&args[1..]) {
        Ok(m) => m,
        Err(f) => panic!(f.to_string()),
    };
    if matches.opt_present("h") {
        print_usage(&program, &opts);
    }
    let opt = matches.opt_str("n");
    let cmd = if !matches.free.is_empty() {
        matches.free[0].clone()
    } else {
        print_usage(&program, &opts);
        return;
    };

    // cmd, opt, filenameをprintする関数
    do_work(&cmd, opt, filename);

fn do_work(inp: &str, lines: Option<String>, filename: Option<String>) {
    println!("{}", inp);
    match lines {
        Some(x) => println!("{}", x),
        None => println!("No lines"),
    }
    match filename {
        Some(x) => println!("{}", x),
        None => println!("No File"),
    }
}

fn print_usage(program: &str, opts: &Options) {
    let brief = format!("Usage: {} FILE [options]", program);
    print!("{}", opts.usage(&brief));
    process::exit(0);
}

コードの流れとしては

まず、はじめにコマンドライン引数に渡される値を取得します。
その後、getopsを利用してoptionの設定を行っています。(例えば、-nがきたらその後ろの値をLINESとして取得しています)
そして、それぞれの引数の解析をおこない、その結果をdo_work関数に渡します。
do_work関数では情報であるコマンド名、行数、ファイル名が出力されます。


出力例)

f:id:nktafuse:20171219135659p:plain


head関数を作る

次にhead関数を作っていきます。
head関数は取得した行数とファイル名を元にファイル内容を出力します。

コードは以下のようになります。

fn head(lines: i32, filename: String) -> i32 {
    // ファイル読み込み
    let string = String::from(filename);
    let path = Path::new(&string);
    let mut file = BufReader::new(File::open(&path).unwrap());

    // ファイルが空かチェック
    if lines <= 0 {
        return 0;
    } else {
        let mut buf = String::new();
        let mut count = 0;
        let mut done = false;

        // 選択した行数かファイルの最後まで行ごとに出力
        while !done {
            file.read_line(&mut buf).expect(
                "reading from BufReader won't fail",
            );
            print!("{}", buf);
            count += 1;
            if lines == count || buf == "" {
                done = true;
            }
            buf.clear();
        }
        return 0;
    }
}

このコードではまず、ファイル名からファイルのバイト列を取得して、それを一行ごとに print! してます。
バイト列の取得にはBufReaderトレイトを利用します。


そして、カウントが指定の行数に達するかファイルが最終行までいったらループを抜けるようになっています。
今回の場合、file.read_line()はBufReadトレイトを満たしたトレイトでかつ、引数ではバイトのスライスでなければ利用できませんので、注意が必要です。
自分はここへんの知識が曖昧で結構時間を溶かしました。

そして、最後にmain関数のもとあったdo_work関数をhead関数に合うように変更してcargoで実行すると、以下のような実行結果が得られます。


出力例)

f:id:nktafuse:20171219180403p:plain



なんとか仕様をみたすプログラムができました!

まとめ

  • getoptsでオプション解析できる。
  • トレイトや型の条件を満たす実装をしているかの把握は重要。

おわりに

Aizu Advent Calendarの投稿に間に合ってよかった...
簡単でヘルプなどの機能を追加していないのですが以上になります。
比較的簡単なプログラムなのに意外と時間かかった... traitや所有権とかまだ慣れない。

私自身Rustを最近学び始めたので、なかなかRustの機能を生かした書き方ができていないかもしれません。
上記のコード中にある、行数分の出力する処理の部分など所々いい書き方が思い浮かびませんでした...
これから書いていって、知見を増やしていきたい。


もし、「ここをこう書いたら良くなる」などアドバイスありましたら、お願いします...!


では、これで!読んでくれた方、ありがとうございました!