Робота зі Змінними Середовища
Ми покращимо minigrep
, додавши екстра функціонал: можливість нечутливого до регістру пошуку, який користувач може увімкнути використавши змінну середовища. Ми могли б зробити цю особливість опцією командного рядку та вимагати, щоб користувачі вводили її кожного разу, коли вони хотіли б її застосувати, але, замість цього, зробивши її змінною середовища, ми дозволяємо нашим користувачам встановлювати змінну середовища одноразово і мати будь-який пошук в цій сесії терміналу нечутливим до регістру.
Написання Провального Тесту для Нечутливої до Регістру Функції search
Спочатку ми додаємо нову функцію search_case_insensitive
, яка буде викликатися, коли змінна середовища має якесь значення. Ми продовжимо дотримуватися процесу TDD, так що, знову, перший крок це написати провальний тест. Ми додамо новий тест новій функції search_case_insensitive
та перейменуємо наш старий тест із one_result
в case_sensitive
для уточнення відмінностей між двома тестами, як показано в Блоці Коду 12-20.
Файл: src/lib.rs
use std::error::Error;
use std::fs;
pub struct Config {
pub query: String,
pub file_path: String,
}
impl Config {
pub 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 })
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
for line in search(&config.query, &contents) {
println!("{line}");
}
Ok(())
}
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)
);
}
}
Зверніть увагу, що ми також відредагували зміст
старого тесту. Ми додали новий рядок із текстом "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
малими літерами, тому незважаючи на регістр вхідних аргументів, вони будуть однакового регістру коли ми будемо перевіряти, чи містить рядок запит.
Файл: src/lib.rs
use std::error::Error;
use std::fs;
pub struct Config {
pub query: String,
pub file_path: String,
}
impl Config {
pub 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 })
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
for line in search(&config.query, &contents) {
println!("{line}");
}
Ok(())
}
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
та зберігаємо в затіненій змінній з такою ж назвою. Виклик 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 [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/lib.rs
use std::error::Error;
use std::fs;
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
pub 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 })
}
}
pub 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(())
}
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)
);
}
}
Ми додали поле ignore_case
, яке містить Boolean. Далі, нам потрібно, щоб функція run
перевіряла значення поля ignore_case
та використовувала це, щоб вирішити, чи викликати функцію search
чи функцію search_case_insensitive
, як показано в Блоці Коду 12-22. Проте, це ще не буде компілюватися.
Файл: src/lib.rs
use std::error::Error;
use std::fs;
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
pub 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 })
}
}
pub 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(())
}
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)
);
}
}
Наостанок, нам потрібно перевірити змінну середовища. Функції для роботи зі змінними середовища є в модулі env
стандартної бібліотеки, тому ми внесемо цей модуль в область видимості зверху файлу src/lib.rs. Потім ми використаємо функцію var
з модуля env
для перевірки наявності значення в змінній середовища з ім'ям IGNORE_CASE
, як показано в Блоці коду 12-23.
Файл: src/lib.rs
use std::env;
// --snip--
use std::error::Error;
use std::fs;
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
pub 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,
})
}
}
pub 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(())
}
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)
);
}
}
Тут ми створюємо нову змінну 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 [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
містить ще багато корисного в роботі зі змінними середовища: перегляньте її документацію, щоб побачити що є можливим.