Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

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

Написання тесту, що не проходить, для пошуку без урахування регістру

Спочатку ми додаємо нову функцію search_case_insensitive до бібліотеки minigrep, яка буде викликатися, коли змінна середовища має значення. Ми й надалі будемо дотримуватися процесу TDD, тож перший крок знову — написати тест, що не проходить (failing test). Ми додамо новий тест для нової функції search_case_insensitive і перейменуємо наш старий тест з one_result на case_sensitive, щоб прояснити відмінності між двома тестами, як показано в Listing 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.", хоча в обох випадках регістр відрізняється від запиту. Це наш тест, що не проходить (failing test), і він не зможе скомпілюватися, тому що ми ще не визначили функцію search_case_insensitive. За бажанням можете додати заглушку реалізації, яка завжди повертає порожній вектор, подібно до того, як ми зробили для функції search у Listing 12-16, щоб побачити, як тест скомпілюється і провалиться.

Реалізація функції search_case_insensitive

Функція search_case_insensitive, показана в Listing 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)
        );
    }
}

Спочатку ми переводимо рядок 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, щоб перемикатися між пошуком із урахуванням регістру та без урахування регістру. Додавання цього поля призведе до помилок компілятора, тому що ми ще ніде не ініціалізуємо це поле:

Filename: 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, яке містить Boolean. Далі нам потрібно, щоб функція run перевірила значення поля ignore_case і використала його, щоб вирішити, чи викликати функцію search, чи функцію search_case_insensitive, як показано в Listing 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. Ми використаємо функцію var із модуля env, щоб перевірити, чи було задано будь-яке значення для змінної середовища з назвою IGNORE_CASE, як показано в Listing 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, якщо змінну середовища не встановлено.

Ми використовуємо метод is_ok на Result, щоб перевірити, чи встановлено змінну середовища, що означає, що програма має виконати пошук без урахування регістру. Якщо змінну середовища IGNORE_CASE не встановлено ні в яке значення, is_ok поверне false, і програма виконає пошук з урахуванням регістру. Нас не цікавить значення змінної середовища, лише те, встановлена вона чи ні, тож ми перевіряємо is_ok замість використання unwrap, expect чи будь-якого з інших методів, які ми бачили на Result.

Ми передаємо значення у змінній ignore_case екземпляру Config, щоб функція run могла прочитати це значення і вирішити, чи викликати search_case_insensitive, чи search, як ми реалізували в Listing 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 залишатиметься встановленою протягом решти вашої сесії оболонки. Її можна зняти за допомогою командлета (cmdlet) 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 містить ще багато корисних можливостей для роботи зі змінними середовища: перегляньте його документацію, щоб побачити, що доступно.