Переменные окружения
Улучшим бинарник minigrep, добавив ещё одну возможность: поиск без учёта
регистра, который пользователь может включить через переменную окружения.
Можно было бы сделать это параметром командной строки, который пользователю
приходилось бы указывать каждый раз. Но если сделать это переменной
окружения, пользователь сможет задать её один раз — и в этом сеансе терминала
все поиски будут без учёта регистра.
Пишем проваливающийся тест для поиска без учёта регистра
Сначала добавим в библиотеку minigrep новую функцию
search_case_insensitive, которая будет вызываться, если у переменной
окружения есть значение. Мы продолжаем следовать TDD, так что первым шагом
снова пишем проваливающийся тест. Добавим новый тест для новой функции
search_case_insensitive, а старый тест переименуем из 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 в старом тесте мы тоже поменяли: добавили строку
"Duct tape." с заглавной D, которая не должна совпадать с запросом
"duct" при поиске с учётом регистра. Такое изменение старого теста помогает
убедиться, что мы случайно не сломаем уже реализованный поиск с учётом
регистра. Этот тест должен проходить сейчас и продолжать проходить, пока мы
работаем над поиском без учёта регистра.
Новый тест для поиска без учёта регистра использует запрос "rUsT". В
функции search_case_insensitive, которую мы сейчас напишем, запрос "rUsT"
должен совпасть и со строкой "Rust:" с заглавной R, и со строкой
"Trust me.", хотя регистр у обеих отличается от запроса. Это наш
проваливающийся тест, и он не скомпилируется, потому что функцию
search_case_insensitive мы ещё не определили. Если хотите, добавьте
заготовку, которая всегда возвращает пустой вектор, — как мы делали для
функции search в листинге 12-16, — чтобы увидеть, как тест компилируется и
проваливается.
Реализуем функцию search_case_insensitive
Функция search_case_insensitive из листинга 12-21 почти такая же, как
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)
);
}
}
search_case_insensitive приводит запрос и строку к нижнему регистру, прежде чем сравнивать ихСначала приводим строку query к нижнему регистру и сохраняем в новую
переменную с тем же именем, затеняя исходную query. to_lowercase нужен
затем, чтобы любой вариант запроса — "rust", "RUST", "Rust" или
"rUsT" — обрабатывался одинаково, как "rust", то есть без учёта
регистра. to_lowercase справляется с базовым Юникодом, но не
идеально. В настоящем приложении здесь пришлось бы поработать ещё, но этот
раздел о переменных окружения, а не о Юникоде, так что на этом остановимся.
Обратите внимание: query теперь не строковый срез, а String, потому что
вызов to_lowercase создаёт новые данные, а не ссылается на существующие.
Скажем, если запрос — "rUsT", в этом строковом срезе нет строчных u и t,
которые можно было бы использовать, поэтому приходится выделить память под
новую String со значением "rust". А когда мы теперь передаём query
аргументом в метод contains, нужно добавить амперсанд: сигнатура contains
принимает строковый срез.
Затем добавляем вызов to_lowercase для каждой строки line, чтобы привести
к нижнему регистру все её символы. Теперь, когда и 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};
// --пропуск--
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(())
}
Мы добавили поле 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};
// --пропуск--
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(())
}
search или search_case_insensitive в зависимости от значения config.ignore_caseНаконец, нужно проверить переменную окружения. Функции для работы с
переменными окружения находятся в модуле env стандартной библиотеки, который
уже подключён в начале src/main.rs. С помощью функции var из модуля env
проверим, задано ли какое-нибудь значение у переменной окружения
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Здесь мы создаём новую переменную ignore_case. Чтобы задать её значение,
вызываем функцию env::var и передаём ей имя переменной окружения
IGNORE_CASE. Функция env::var возвращает Result: если переменная
окружения задана (с любым значением), это будет успешный вариант Ok со
значением переменной. Если не задана — вариант Err.
С помощью метода is_ok у Result мы проверяем, задана ли переменная
окружения, — если задана, программа должна искать без учёта регистра. Если
IGNORE_CASE не задана, is_ok вернёт false, и программа будет искать
с учётом регистра. Значение переменной окружения нас не интересует — только
то, задана она или нет. Поэтому мы проверяем is_ok, а не используем
unwrap, expect или другие уже знакомые методы Result.
Значение переменной ignore_case мы передаём в экземпляр Config, чтобы
функция run могла его прочитать и решить, вызывать search_case_insensitive
или search, как мы реализовали в листинге 12-22.
Попробуем! Сначала запустим программу без переменной окружения с запросом
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:
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 умеет искать без учёта
регистра, а включается это переменной окружения. Теперь вы знаете, как
управлять настройками и через аргументы командной строки, и через переменные
окружения.
Некоторые программы позволяют задавать одну и ту же настройку и аргументами, и переменными окружения. В таких случаях программа решает, что из них имеет приоритет. В качестве самостоятельного упражнения попробуйте управлять учётом регистра и через аргумент командной строки, и через переменную окружения. Решите, что должно иметь приоритет, если программу запустили с аргументом «учитывать регистр» и переменной «не учитывать» (или наоборот).
В модуле std::env есть ещё много полезного для работы с переменными
окружения — загляните в его документацию и посмотрите, что там доступно.