Работа с переменными окружения
Мы улучшим бинарный файл 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 обработает
базовый Unicode, он не будет точен на 100 процентов. Если бы мы писали
настоящее приложение, здесь стоило бы сделать немного больше работы, но этот
раздел о переменных окружения, а не о Unicode, поэтому здесь мы оставим все как
есть.
Обратите внимание, что теперь 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};
// --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(())
}
Мы добавили поле 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(())
}
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 содержит намного больше полезных возможностей для работы с
переменными окружения: посмотрите его документацию, чтобы узнать, что
доступно.