環境変数を取り扱う
おまけの機能を追加してminigrep
を改善します: 環境変数でユーザがオンにできる大文字小文字無視の検索用のオプションです。
この機能をコマンドラインオプションにして、適用したい度にユーザが入力しなければならないようにすることもできますが、
代わりに環境変数を使用します。そうすることでユーザは1回環境変数をセットすれば、そのターミナルセッションの間は、
大文字小文字無視の検索を行うことができるようになるわけです。
大文字小文字を区別しないsearch
関数用に失敗するテストを書く
環境変数がオンの場合に呼び出すsearch_case_insensitive
関数を新しく追加したいです。テスト駆動開発の過程に従い続けるので、
最初の手順は、今回も失敗するテストを書くことです。新しいsearch_case_insensitive
関数用の新規テストを追加し、
古いテストをone_result
からcase_sensitive
に名前変更して、二つのテストの差異を明確化します。
リスト12-20に示したようにですね。
ファイル名: src/lib.rs
# #![allow(unused_variables)] #fn main() { #[cfg(test)] mod test { use super::*; #[test] fn case_sensitive() { let query = "duct"; // Rust // 安全かつ高速で生産的 // 三つを選んで // ガムテープ let contents = "\ Rust: safe, fast, productive. Pick three. Duct tape."; assert_eq!( vec!["safe, fast, productive."], search(query, contents) ); } #[test] fn case_insensitive() { let query = "rUsT"; // (最後の行のみ) // 私を信じて let contents = "\ Rust: safe, fast, productive. Pick three. Trust me."; assert_eq!( vec!["Rust:", "Trust me."], search_case_insensitive(query, contents) ); } } #}
古いテストのcontents
も変更していることに注意してください。大文字小文字を区別する検索を行う際に、
"duct"
というクエリに合致しないはずの大文字Dを使用した"Duct tape"
(ガムテープ)という新しい行を追加しました。
このように古いテストを変更することで、既に実装済みの大文字小文字を区別する検索機能を誤って壊してしまわないことを保証する助けになります。
このテストはもう通り、大文字小文字を区別しない検索に取り掛かっても通り続けるはずです。
大文字小文字を区別しない検索の新しいテストは、クエリに"rUsT"を使用しています。
追加直前のsearch_case_insensitive
関数では、"rUsT"というクエリは、
両方ともクエリとは大文字小文字が異なるのに、大文字Rの"Rust:"を含む行と、
“Trust me.”
という行にもマッチするはずです。これが失敗するテストであり、まだsearch_case_insensitive
関数を定義していないので、
コンパイルは失敗するでしょう。リスト12-16のsearch
関数で行ったのと同様に空のベクタを常に返すような仮実装を追加し、テストがコンパイルされるものの、失敗する様をご自由に確認してください。
search_case_insensitive
関数を実装する
search_case_insensitive
関数は、リスト12-21に示しましたが、search
関数とほぼ同じです。
唯一の違いは、query
と各line
を小文字化していることなので、入力引数の大文字小文字によらず、
行がクエリを含んでいるか確認する際には、同じになるわけです。
ファイル名: src/lib.rs
# #![allow(unused_variables)] #fn main() { pub fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { let query = query.to_lowercase(); let mut results = Vec::new(); for line in contents.lines() { if line.to_lowercase().contains(&query) { results.push(line); } } results } #}
まず、query
文字列を小文字化し、同じ名前の覆い隠された変数に保存します。ユーザのクエリが"rust"
や"RUST"
、
"Rust"
、"rUsT"
などだったりしても、"rust"
であり、大文字小文字を区別しないかのようにクエリを扱えるように、
to_lowercase
をクエリに対して呼び出すことは必須です。
query
は最早、文字列スライスではなくString
であることに注意してください。というのも、
to_lowercase
を呼び出すと、既存のデータを参照するというよりも、新しいデータを作成するからです。
例として、クエリは"rUsT"
だとしましょう: その文字列スライスは、小文字のu
やt
を使えるように含んでいないので、
"rust"
を含む新しいString
のメモリを確保しなければならないのです。今、contains
メソッドに引数としてquery
を渡すと、
アンド記号を追加する必要があります。contains
のシグニチャは、文字列スライスを取るよう定義されているからです。
次に、各line
がquery
を含むか確かめる前にto_lowercase
の呼び出しを追加し、全文字を小文字化しています。
今やline
とquery
を小文字に変換したので、クエリが大文字であろうと小文字であろうとマッチを検索するでしょう。
この実装がテストを通過するか確認しましょう:
running 2 tests
test test::case_insensitive ... ok
test test::case_sensitive ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
素晴らしい!どちらも通りました。では、run
関数から新しいsearch_case_insensitive
関数を呼び出しましょう。
1番目に大文字小文字の区別を切り替えられるよう、Config
構造体に設定オプションを追加します。
まだどこでも、このフィールドの初期化をしていないので、追加するとコンパイルエラーが起きます:
ファイル名: src/lib.rs
# #![allow(unused_variables)] #fn main() { pub struct Config { pub query: String, pub filename: String, pub case_sensitive: bool, } #}
論理値を持つcase_sensitive
フィールドを追加したことに注意してください。次に、run
関数に、
case_sensitive
フィールドの値を確認し、search
関数かsearch_case_insensitive
関数を呼ぶかを決定するのに使ってもらう必要があります。
リスト12-22のようにですね。それでも、これはまだコンパイルできないことに注意してください。
ファイル名: src/lib.rs
# #![allow(unused_variables)] #fn main() { # use std::error::Error; # use std::fs::File; # use std::io::prelude::*; # # fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { # vec![] # } # # pub fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { # vec![] # } # # pub struct Config { # query: String, # filename: String, # case_sensitive: bool, # } # pub fn run(config: Config) -> Result<(), Box<Error>> { let mut f = File::open(config.filename)?; let mut contents = String::new(); f.read_to_string(&mut contents)?; let results = if config.case_sensitive { search(&config.query, &contents) } else { search_case_insensitive(&config.query, &contents) }; for line in results { println!("{}", line); } Ok(()) } #}
最後に、環境変数を確認する必要があります。環境変数を扱う関数は、標準ライブラリのenv
モジュールにあるので、
use std::env;
行でsrc/lib.rsの冒頭でそのモジュールをスコープに持ってくる必要があります。そして、
env
モジュールからvar
関数を使用してCASE_INSENSITIVE
という環境変数のチェックを行います。
リスト12-23のようにですね。
ファイル名: src/lib.rs
# #![allow(unused_variables)] #fn main() { use std::env; # struct Config { # query: String, # filename: String, # case_sensitive: bool, # } // --snip-- impl Config { pub fn new(args: &[String]) -> Result<Config, &'static str> { if args.len() < 3 { return Err("not enough arguments"); } let query = args[1].clone(); let filename = args[2].clone(); let case_sensitive = env::var("CASE_INSENSITIVE").is_err(); Ok(Config { query, filename, case_sensitive }) } } #}
ここで、case_sensitive
という新しい変数を生成しています。その値をセットするために、
env::var
関数を呼び出し、CASE_INSENSITIVE
環境変数の名前を渡しています。env::var
関数は、
環境変数がセットされていたら、環境変数の値を含むOk
列挙子の成功値になるResult
を返します。
環境変数がセットされていなければ、Err
列挙子を返すでしょう。
Result
のis_err
メソッドを使用して、エラーでありゆえに、セットされていないことを確認しています。
これは大文字小文字を区別する検索をすべきことを意味します。CASE_INSENSITIVE
環境変数が何かにセットされていれば、
is_err
はfalseを返し、プログラムは大文字小文字を区別しない検索を実行するでしょう。環境変数の値はどうでもよく、
セットされているかどうかだけ気にするので、unwrap
やexpect
あるいは、他のここまで見かけたResult
のメソッドではなく、
is_err
をチェックしています。
case_sensitive
変数の値をConfig
インスタンスに渡しているので、リスト12-22で実装したように、
run
関数はその値を読み取り、search
かsearch_case_insensitive
を呼び出すか決定できるのです。
試行してみましょう!まず、環境変数をセットせずにクエリはto
でプログラムを実行し、
この時は全て小文字で"to"という言葉を含むあらゆる行が合致するはずです。
$ cargo run to poem.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/minigrep to poem.txt`
Are you nobody, too?
How dreary to be somebody!
まだ機能しているようです!では、CASE_INSENSITIVE
を1にしつつ、同じクエリのto
でプログラムを実行しましょう。
PowerShellを使用しているなら、1コマンドではなく、2コマンドで環境変数をセットし、プログラムを実行する必要があるでしょう:
$ $env:CASE_INSENSITIVE=1
$ cargo run to poem.txt
大文字も含む可能性のある"to"を含有する行が得られるはずです:
$ CASE_INSENSITIVE=1 cargo run to poem.txt
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/minigrep to poem.txt`
Are you nobody, too?
How dreary to be somebody!
To tell your name the livelong day
To an admiring bog!
素晴らしい、"To"を含む行も出てきましたね!minigrep
プログラムはこれで、
環境変数によって制御できる大文字小文字を区別しない検索も行えるようになりました。もうコマンドライン引数か、
環境変数を使ってオプションを管理する方法も知りましたね。
引数と環境変数で同じ設定を行うことができるプログラムもあります。そのような場合、 プログラムはどちらが優先されるか決定します。自身の別の鍛錬として、コマンドライン引数か、 環境変数で大文字小文字の区別を制御できるようにしてみてください。 片方は大文字小文字を区別するようにセットされ、もう片方は区別しないようにセットしてプログラムが実行された時に、 コマンドライン引数と環境変数のどちらの優先度が高くなるかを決めてください。
std::env
モジュールは、環境変数を扱うもっと多くの有用な機能を有しています:
ドキュメンテーションを確認して、何が利用可能か確かめてください。