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 без учёта регистра

Сначала мы добавляем новую функцию search_case_insensitive, которая будет вызываться, когда переменная окружения имеет значение. Мы продолжим следовать процессу TDD, поэтому первый шаг снова — написать тест, который не проходит. Мы добавим новый тест для новой функции search_case_insensitive и переименуем наш старый тест с one_result на case_sensitive, чтобы прояснить разницу между двумя тестами, как показано в Листинге 12-20.

Filename: 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: Добавление нового теста, который не проходит, для функции без учёта регистра, которую мы собираемся добавить

Обратите внимание, что мы также отредактировали поле 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 к нижнему регистру, чтобы независимо от регистра входных аргументов они были в одном регистре при проверке, содержит ли строка запрос.

Filename: 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: Определение функции 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/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, которое содержит логическое значение. Далее нам нужно, чтобы функция run проверяла значение поля ignore_case и использовала его для принятия решения о вызове функции search или функции search_case_insensitive, как показано в Листинге 12-22. Это всё ещё не скомпилируется.

Filename: 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)
        );
    }
}
Listing 12-22: Вызов либо search, либо search_case_insensitive в зависимости от значения в config.ignore_case

Наконец, нам нужно проверить переменную окружения. Функции для работы с переменными окружения находятся в модуле env в стандартной библиотеке, поэтому мы подключаем этот модуль в область видимости в начале src/lib.rs. Затем мы будем использовать функцию var из модуля env, чтобы проверить, установлено ли какое-либо значение для переменной окружения с именем IGNORE_CASE, как показано в Листинге 12-23.

Filename: 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)
        );
    }
}
Listing 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` 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 содержит много других полезных возможностей для работы с переменными окружения: ознакомьтесь с его документацией, чтобы увидеть, что доступно.