コマンドライン引数を受け付ける

いつものように、cargo newで新しいプロジェクトを作りましょう。プロジェクトをminigrepと名付けて、 既に自分のシステムに存在するかもしれないgrepツールと区別しましょう。

$ cargo new minigrep
     Created binary (application) `minigrep` project
$ cd minigrep

最初の仕事は、minigrepを二つの引数を受け付けるようにすることです: ファイルパスと検索する文字列ですね。 つまり、cargo run、以降の引数がcargoに対するものではなく自分のプログラムに対するものであることを示すハイフン2個、 検索文字列、そして検索を行うファイルへのパスを渡して、プログラムを実行できるようになりたいということです。 こんな感じにね:

$ cargo run -- searchstring example-filename.txt

今現在は、cargo newで生成されたプログラムは、与えた引数を処理できません。 crates.ioに存在する既存のライブラリには、 コマンドライン引数を受け付けるプログラムを書く手助けをしてくれるものもありますが、ちょうどこの概念を学んでいる最中なので、 この能力を自分で実装しましょう。

引数の値を読み取る

minigrepが渡したコマンドライン引数の値を読み取れるようにするために、 Rustの標準ライブラリで提供されているstd::env::args関数が必要になります。 この関数は、minigrepに渡されたコマンドライン引数のイテレータを返します。 イテレータについては第13章で詳しく講義します。 とりあえずイテレータに関しては、2つの詳細のみ知っていればいいです: イテレータは一連の値を生成することと、イテレータに対してcollectメソッドを呼び出し、 イテレータが生成する要素全部を含むベクタなどのコレクションに変えられることです。

リスト12-1のコードを使用することで、minigrepプログラムに渡されたあらゆるコマンドライン引数を読み取り、 それからその値をベクタに集めることができます。

ファイル名: src/main.rs

use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();
    dbg!(args);
}

リスト12-1: コマンドライン引数をベクタに集結させ、出力する

まず、std::envモジュールをuse文でスコープに導入したので、args関数が使用できます。 std::env::args関数は、2レベルモジュールがネストされていることに注目してください。 第7章で議論したように、希望の関数が2モジュール以上ネストされている場合、 関数ではなく親モジュールをスコープに導入しています。そうすることで、 std::envから別の関数も容易に使用することができます。また、 use std::env::argsを追記し、関数をargsとするだけで呼び出すのに比べて曖昧でもありません。 というのも、argsは現在のモジュールに定義されている関数と容易に見間違えられるかもしれないからです。

args関数と不正なユニコード

引数のどれかが不正なユニコードを含んでいたら、std::env::argsはパニックすることに注意してください。 プログラムが不正なユニコードを含む引数を受け付ける必要があるなら、代わりにstd::env::args_osを使用してください。 この関数は、String値ではなく、OsString値を生成するイテレータを返します。ここでは、 簡潔性のためにstd::env::argsを使うことを選択しました。 なぜなら、OsString値はプラットフォームごとに異なり、String値に比べて取り扱いが煩雑だからです。

mainの最初の行でenv::argsを呼び出し、そして即座にcollectを使用して、 イテレータをイテレータが生成する値全てを含むベクタに変換しています。 collect関数を使用して多くの種類のコレクションを生成することができるので、 argsの型を明示的に注釈して文字列のベクタが欲しいのだと指定しています。Rustにおいて、 型を注釈しなければならない頻度は非常に少ないのですが、collectはよく確かに注釈が必要になる一つの関数なのです。 コンパイラには、あなたが欲しているコレクションの種類が推論できないからです。

最後に、デバッグマクロを使用してベクタを出力しています。 まずは引数なしでコードを走らせてみて、それから引数二つで試してみましょう:

$ cargo run
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished dev [unoptimized + debuginfo] target(s) in 0.61s
     Running `target/debug/minigrep`
[src/main.rs:5:5] args = [
    "target/debug/minigrep",
]
$ cargo run -- needle haystack
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished dev [unoptimized + debuginfo] target(s) in 1.57s
     Running `target/debug/minigrep needle haystack`
[src/main.rs:5:5] args = [
    "target/debug/minigrep",
    "needle",
    "haystack",
]

ベクタの最初の値は"target/debug/minigrep"であることに注目してください。これはバイナリの名前です。 これはCの引数リストの振る舞いと合致し、実行時に呼び出された名前をプログラムに使わせてくれるわけです。 メッセージで出力したり、プログラムを起動するのに使用されたコマンドラインエイリアスによってプログラムの振る舞いを変えたい場合に、 プログラム名にアクセスするのにしばしば便利です。ですが、この章の目的には、これを無視し、必要な二つの引数のみを保存します。

引数の値を変数に保存する

現時点でプログラムはコマンドライン引数として指定された値にアクセスできます。 さて、プログラムの残りを通して使用できるように、二つの引数の値を変数に保存する必要があります。 それをしているのがリスト12-2です。

ファイル名: src/main.rs

use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();

    let query = &args[1];
    let file_path = &args[2];

    //       "{}を検索します"
    println!("Searching for {}", query);
    //       "ファイル{}の中で"
    println!("In file {}", file_path);
}

リスト12-2: クエリ引数とファイルパス引数を保持する変数を生成

ベクタを出力した時に確認したように、プログラム名がベクタの最初の値、args[0]を占めているので、 引数を添え字1から始めます。minigrepが取る最初の引数は、検索する文字列なので、 最初の引数への参照を変数queryに置きました。2番目の引数はファイルパスでしょうから、 2番目の引数への参照は変数file_pathに置きました。

一時的にこれらの変数の値を出力して、コードが意図通りに動いていることを証明しています。 再度このプログラムをtestsample.txtという引数で実行しましょう:

$ cargo run -- test sample.txt
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished dev [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/minigrep test sample.txt`
Searching for test
In file sample.txt

素晴らしい、プログラムは動作しています!必要な引数の値が、正しい変数に保存されています。後ほど、 何らかのエラー処理を加えて、ユーザが引数を提供しなかった場合など、可能性のある特定のエラー状況に対処します; 今は、そのような状況はないものとし、代わりにファイル読み取り能力を追加することに取り組みます。