Робота зі Змінними Середовища

Ми покращимо minigrep, додавши екстра функціонал: можливість нечутливого до регістру пошуку, який користувач може увімкнути використавши змінну середовища. Ми могли б зробити цю особливість опцією командного рядку та вимагати, щоб користувачі вводили її кожного разу, коли вони хотіли б її застосувати, але, замість цього, зробивши її змінною середовища, ми дозволяємо нашим користувачам встановлювати змінну середовища одноразово і мати будь-який пошук в цій сесії терміналу нечутливим до регістру.

Спочатку ми додаємо нову функцію 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)
        );
    }
}

Listing 12-20: Adding a new failing test for the case-insensitive function we’re about to add

Зверніть увагу, що ми також відредагували зміст старого тесту. Ми додали новий рядок із текстом "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)
        );
    }
}

Listing 12-21: Defining the search_case_insensitive function to lowercase the query and the line before comparing them

Спочатку ми зменшуємо регістр стрічки 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)
        );
    }
}

Блок коду 12-22: Виклик search або search_case_insensitive на основі значення в config.ignore_case

Наостанок, нам потрібно перевірити змінну середовища. Функції для роботи зі змінними середовища є в модулі 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)
        );
    }
}

Блок коду 12-23: Перевірка, чи є якесь значення в змінній середовища з назвою 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 [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 містить ще багато корисного в роботі зі змінними середовища: перегляньте її документацію, щоб побачити що є можливим.