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, добавив ещё одну возможность: поиск без учёта регистра, который пользователь может включить через переменную окружения. Можно было бы сделать это параметром командной строки, который пользователю приходилось бы указывать каждый раз. Но если сделать это переменной окружения, пользователь сможет задать её один раз — и в этом сеансе терминала все поиски будут без учёта регистра.

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

Файл: src/lib.rs
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)
        );
    }
}
Листинг 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 к нижнему регистру. Тогда, каким бы ни был регистр входных аргументов, при проверке, есть ли в строке запрос, регистр у них будет одинаковым.

Файл: src/lib.rs
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-21: Функция search_case_insensitive приводит запрос и строку к нижнему регистру, прежде чем сравнивать их

Сначала приводим строку query к нижнему регистру и сохраняем в новую переменную с тем же именем, затеняя исходную query. to_lowercase нужен затем, чтобы любой вариант запроса — "rust", "RUST", "Rust" или "rUsT" — обрабатывался одинаково, как "rust", то есть без учёта регистра. to_lowercase справляется с базовым Юникодом, но не идеально. В настоящем приложении здесь пришлось бы поработать ещё, но этот раздел о переменных окружения, а не о Юникоде, так что на этом остановимся.

Обратите внимание: 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};

// --пропуск--


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. Код всё ещё не компилируется.

Файл: src/main.rs
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();

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

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

Файл: src/main.rs
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(())
}
Листинг 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 есть ещё много полезного для работы с переменными окружения — загляните в его документацию и посмотрите, что там доступно.