Работа с переменными окружения

Мы улучшим minigrep, добавив дополнительную функцию: опцию для поиск без учёта регистра, которую пользователь может включить с помощью переменной среды окружения. Мы могли бы сделать эту функцию параметром командной строки и потребовать, чтобы пользователи вводили бы её каждый раз при её применении, но вместо этого мы будем использовать переменную среды окружения. Это позволит нашим пользователям устанавливать переменную среды один раз и все поиски будут не чувствительны к регистру в этом терминальном сеансе.

Написание ошибочного теста для функции search с учётом регистра

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

Файл: src/lib.rs


#![allow(unused)]
fn main() {
use std::error::Error;
use std::fs;

pub struct Config {
    pub query: String,
    pub filename: String,
}

impl Config {
    pub fn new(args: &[String]) -> Result<Config, &str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let filename = args[2].clone();

        Ok(Config { query, filename })
    }
}

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.filename)?;

    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)
        );
    }
}
}

Листинг 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


#![allow(unused)]
fn main() {
use std::error::Error;
use std::fs;

pub struct Config {
    pub query: String,
    pub filename: String,
}

impl Config {
    pub fn new(args: &[String]) -> Result<Config, &str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let filename = args[2].clone();

        Ok(Config { query, filename })
    }
}

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.filename)?;

    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)
        );
    }
}
}

Листинг 12-21. Определение функции search_case_insensitive с уменьшением регистра строки запроса и строки содержимого перед их сравнением

Сначала преобразуем в нижний регистр строку 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, прежде чем проверять, содержит ли она query из всех строчных символов. Теперь, когда мы преобразовали line и query в нижний регистр, мы найдём совпадения независимо от того, в каком регистре находится переменная с запросом.

Давайте посмотрим, проходит ли эта реализация тесты:

$ cargo test
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished test [unoptimized + debuginfo] target(s) in 1.33s
     Running target/debug/deps/minigrep-4672b652f7794785

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 target/debug/deps/minigrep-4672b652f7794785

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 filename: String,
    pub case_sensitive: bool,
}

impl Config {
    pub fn new(args: &[String]) -> Result<Config, &str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let filename = args[2].clone();

        Ok(Config { query, filename })
    }
}

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.filename)?;

    let results = if config.case_sensitive {
        search(&config.query, &contents)
    } else {
        search_case_insensitive(&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)
        );
    }
}

Обратите внимание, что мы добавили поле case_sensitive, которое содержит логическое значение. Далее нам нужна функция run, чтобы проверить значение поля case_sensitive и использовать его, чтобы решить, вызывать ли функцию search или функцию search_case_insensitive, как показано в листинге 12-22. Обратите внимание, что код все ещё не компилируется.

Файл: src/lib.rs

use std::error::Error;
use std::fs;

pub struct Config {
    pub query: String,
    pub filename: String,
    pub case_sensitive: bool,
}

impl Config {
    pub fn new(args: &[String]) -> Result<Config, &str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let filename = args[2].clone();

        Ok(Config { query, filename })
    }
}

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.filename)?;

    let results = if config.case_sensitive {
        search(&config.query, &contents)
    } else {
        search_case_insensitive(&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.case_sensitive

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

Файл: src/lib.rs


#![allow(unused)]
fn main() {
use std::env;
// --snip--

use std::error::Error;
use std::fs;

pub struct Config {
    pub query: String,
    pub filename: String,
    pub case_sensitive: bool,
}

impl Config {
    pub fn new(args: &[String]) -> Result<Config, &str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let filename = args[2].clone();

        let case_sensitive = env::var("CASE_INSENSITIVE").is_err();

        Ok(Config {
            query,
            filename,
            case_sensitive,
        })
    }
}

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.filename)?;

    let results = if config.case_sensitive {
        search(&config.query, &contents)
    } else {
        search_case_insensitive(&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. Проверка переменной среды с именем CASE_INSENSITIVE

Здесь мы создаём новую переменную case_sensitive. Чтобы установить её значение, мы вызываем функцию env::var и передаём ей имя переменной окружения CASE_INSENSITIVE. Функция env::var возвращает Result, который будет успешным вариантом Ok содержащий значение переменной среды, если переменная среды установлена. Он вернёт вариант Err, если переменная окружения не установлена.

Мы используем метод is_err у Result, чтобы проверить возвращается ли ошибка и следовательно, переменная среды не установлена, что означает, что должен выполняться чувствительный к регистру поиск. Если для переменной среды CASE_INSENSITIVE что-либо задано, то is_err вернёт значение false и программа выполнит поиск без учёта регистра. Мы не заботимся о значении переменной среды, нас интересует только установлена она или нет, поэтому мы проверяем is_err, а не используем unwrap, expect или любой другой метод, который мы видели у Result.

Мы передаём значение переменной case_sensitive экземпляру Config, чтобы функция run могла прочитать это значение и решить, следует ли вызывать search или search_case_insensitive, как мы реализовали в листинге 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!

Похоже, все ещё работает! Теперь давайте запустим программу с CASE_INSENSITIVE, установленным в 1, но с тем же значением запроса to.

Если вы используете PowerShell, вам нужно установить переменную среды и запустить программу двумя командами, а не одной:

PS> $Env:CASE_INSENSITIVE=1; cargo run to poem.txt

Это заставит переменную окружения CASE_INSENSITIVE сохраниться до конца сеанса работы консоли. Переменную можно отключить с помощью команды Remove-Item:

PS> Remove-Item Env:CASE_INSENSITIVE

Мы должны получить строки, содержащие «to», которые могут иметь заглавные буквы:

$ CASE_INSENSITIVE=1 cargo run to poem.txt
    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!
To tell your name the livelong day
To an admiring bog!

Отлично, мы также получили строки, содержащие «To»! Наша программа minigrep теперь может выполнять поиск без учёта регистра, управляемая переменной среды. Теперь вы знаете, как управлять параметрами, заданными с помощью аргументов командной строки или переменных среды.

Некоторые программы допускают использование аргументов и переменных среды для одной и той же конфигурации. В таких случаях программы решают, что из них имеет больший приоритет. Для другого самостоятельного упражнения попробуйте управлять нечувствительностью к регистру с помощью аргумента командной строки или переменной окружения. Решите, должен ли приоритет иметь аргумент командной строки или переменная среды, если программа запускается с установленным параметром для нечувствительного к регистру поиска и установленного для поиска с учётом регистра.

Модуль std::env содержит много других полезных функций для работы с переменными среды: ознакомьтесь с его документацией, чтобы узнать доступные.