環境変数を扱う
追加機能として、環境変数を介してユーザーが有効化できる、大文字と小文字を区別しない検索オプションを追加して、minigrep バイナリを改善しましょう。この機能をコマンドラインオプションにして、必要になるたびに毎回ユーザーに指定してもらうこともできますが、代わりに環境変数にすることで、ユーザーは一度だけ環境変数を設定すれば、そのターミナルセッション中のすべての検索で大文字と小文字を区別しないようにできます。
大文字と小文字を区別しない検索のための失敗するテストを書く
まず、環境変数に値があるときに呼び出される新しい search_case_insensitive 関数を minigrep ライブラリに追加します。引き続き TDD のプロセスに従うので、最初のステップもやはり失敗するテストを書くことです。新しい search_case_insensitive 関数用の新しいテストを追加し、2 つのテストの違いを明確にするために、既存のテスト名を one_result から case_sensitive に変更します。これはリスト 12-20 に示されています。
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let mut results = Vec::new();
for line in contents.lines() {
if line.contains(query) {
results.push(line);
}
}
results
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn case_sensitive() {
let query = "duct";
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 も編集したことに注目してください。大文字の D を使った "Duct tape." という新しい行を追加しました。これは、大文字と小文字を区別する方法で検索している場合、クエリ "duct" には一致しないはずです。このように既存のテストを変更することで、すでに実装した大文字と小文字を区別する検索機能を誤って壊していないことを確認しやすくなります。このテストは今の時点で通るはずであり、大文字と小文字を区別しない検索の実装を進める間も通り続けるはずです。
大文字と小文字を区別しない検索用の新しいテストでは、クエリとして "rUsT" を使います。これから追加する search_case_insensitive 関数では、クエリ "rUsT" は大文字の R を含む "Rust:" を含む行に一致し、さらに "Trust me." という行にも一致するはずです。どちらもクエリとは大文字小文字の使い方が異なっていても一致します。これが失敗するテストであり、まだ search_case_insensitive 関数を定義していないため、コンパイルに失敗します。リスト 12-16 の search 関数で行ったのと同様に、常に空のベクタを返す骨組みの実装を追加して、テストがコンパイルし、そして失敗することを確認してみてもかまいません。
search_case_insensitive 関数を実装する
リスト 12-21 に示す search_case_insensitive 関数は、search 関数とほとんど同じになります。唯一の違いは、query と各 line を小文字化することです。そうすることで、入力引数の大文字小文字がどうであっても、行にクエリが含まれているかを調べるときには同じ大文字小文字で比較できます。
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let mut results = Vec::new();
for line in contents.lines() {
if line.contains(query) {
results.push(line);
}
}
results
}
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
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn case_sensitive() {
let query = "duct";
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)
);
}
}
まず、query 文字列を小文字化し、元の query をシャドーイングする同名の新しい変数に保存します。クエリに対して to_lowercase を呼び出す必要があるのは、ユーザーのクエリが "rust"、"RUST"、"Rust"、あるいは "rUsT" のいずれであっても、クエリを "rust" であるかのように扱い、大文字小文字を区別しないようにするためです。to_lowercase は基本的な Unicode を処理しますが、100 パーセント正確というわけではありません。実際のアプリケーションを書いているなら、ここでもう少し手を加えたくなるでしょう。しかし、この節の主題は Unicode ではなく環境変数なので、ここではこのままにしておきます。
ここで query は文字列スライスではなく String になっていることに注意してください。to_lowercase を呼び出すと、既存のデータを参照するのではなく、新しいデータが作られるためです。たとえばクエリが "rUsT" だとしましょう。この文字列スライスには、使える小文字の u や t は含まれていないので、"rust" を含む新しい String を確保する必要があります。いま query を引数として contains メソッドに渡す際には、アンパサンドを付ける必要があります。これは、contains のシグネチャが文字列スライスを受け取るように定義されているためです。
次に、各 line に対しても to_lowercase を呼び出し、すべての文字を小文字にします。line と query の両方を小文字に変換したので、クエリの大文字小文字がどうであっても一致を見つけられるようになります。
この実装がテストを通るか見てみましょう。
$ cargo test
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `test` profile [unoptimized + debuginfo] target(s) in 1.33s
Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)
running 2 tests
test tests::case_insensitive ... ok
test tests::case_sensitive ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/main.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests minigrep
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
すばらしいです! テストは通りました。では次に、新しい search_case_insensitive 関数を run 関数から呼び出しましょう。まず、大文字と小文字を区別する検索と区別しない検索を切り替えるための設定オプションを Config 構造体に追加します。このフィールドを追加すると、まだどこでもこのフィールドを初期化していないため、コンパイラエラーが発生します。
ファイル名: src/main.rs
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{search, search_case_insensitive};
// --snip--
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {err}");
process::exit(1);
});
if let Err(e) = run(config) {
println!("Application error: {e}");
process::exit(1);
}
}
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
Ok(Config { query, file_path })
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
bool 値を保持する ignore_case フィールドを追加しました。次に、run 関数が ignore_case フィールドの値を確認し、その値に応じて search 関数と search_case_insensitive 関数のどちらを呼び出すか決める必要があります。これはリスト 12-22 に示されています。これもまだコンパイルは通りません。
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{search, search_case_insensitive};
// --snip--
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {err}");
process::exit(1);
});
if let Err(e) = run(config) {
println!("Application error: {e}");
process::exit(1);
}
}
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
Ok(Config { query, file_path })
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
最後に、環境変数を確認する必要があります。環境変数を扱うための関数は標準ライブラリの env モジュールにあり、これはすでに src/main.rs の先頭でスコープに入っています。env モジュールの var 関数を使って、IGNORE_CASE という名前の環境変数に何らかの値が設定されているかどうかを確認します。これはリスト 12-23 に示されています。
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{search, search_case_insensitive};
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {err}");
process::exit(1);
});
if let Err(e) = run(config) {
println!("Application error: {e}");
process::exit(1);
}
}
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
ここでは、新しい変数 ignore_case を作成しています。その値を設定するために、env::var 関数を呼び出し、IGNORE_CASE 環境変数の名前を渡します。env::var 関数は Result を返し、環境変数が何らかの値に設定されている場合には、その値を含む成功の Ok バリアントになります。環境変数が設定されていない場合は、Err バリアントを返します。
Result の is_ok メソッドを使って環境変数が設定されているかどうかを確認しています。これは、プログラムが大文字と小文字を区別しない検索を行うべきであることを意味します。IGNORE_CASE 環境変数に何も設定されていなければ、is_ok は false を返し、プログラムは大文字と小文字を区別する検索を実行します。環境変数の 値 自体は気にしておらず、設定されているか未設定かだけが重要なので、unwrap、expect、あるいはこれまでに見てきた Result のほかのメソッドを使うのではなく、is_ok を確認しています。
run 関数がその値を読み取り、リスト 12-22 で実装したように search_case_insensitive と search のどちらを呼ぶかを判断できるように、ignore_case 変数の値を Config インスタンスに渡します。
試してみましょう! まず、環境変数を設定せず、クエリを to にしてプログラムを実行します。これは、すべて小文字の単語 to を含むすべての行にマッチするはずです。
$ cargo run -- to poem.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep to poem.txt`
Are you nobody, too?
How dreary to be somebody!
問題なく動いているようです! では次に、IGNORE_CASE を 1 に設定し、同じクエリ to でプログラムを実行してみましょう。
$ IGNORE_CASE=1 cargo run -- to poem.txt
PowerShell を使っている場合は、環境変数を設定してプログラムを実行する必要があります。
PS> $Env:IGNORE_CASE=1; cargo run -- to poem.txt
これにより、IGNORE_CASE はシェルセッションの残りの間、設定されたままになります。Remove-Item cmdlet を使えば解除できます。
PS> Remove-Item Env:IGNORE_CASE
大文字を含む可能性のある to を含んだ行が得られるはずです。
Are you nobody, too?
How dreary to be somebody!
To tell your name the livelong day
To an admiring bog!
すばらしいことに、To を含む行も得られました! これで minigrep プログラムは、環境変数で制御される大文字と小文字を区別しない検索を実行できるようになりました。これで、コマンドライン引数または環境変数を使って設定されるオプションをどのように管理するかがわかりました。
同じ設定に対して引数 と 環境変数の両方を許可するプログラムもあります。そのような場合、プログラムはどちらか一方を優先するように決めます。もう 1 つ自分でやってみる練習として、コマンドライン引数または環境変数のどちらかで大文字と小文字の区別を制御してみてください。プログラムを、一方では大文字と小文字を区別する設定、もう一方では区別しない設定で実行した場合に、コマンドライン引数と環境変数のどちらを優先するべきかを決めてください。
std::env モジュールには、環境変数を扱うためのさらに多くの便利な機能が含まれています。どのようなものが利用できるかは、そのドキュメントを確認してください。