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

Praca ze zmiennymi środowiskowymi

Udoskonalimy plik binarny minigrep, dodając dodatkową funkcję: opcję wyszukiwania bez uwzględniania wielkości liter, którą użytkownik może włączyć za pomocą zmiennej środowiskowej. Moglibyśmy uczynić tę funkcję opcją wiersza poleceń i wymagać od użytkowników wprowadzania jej za każdym razem, gdy chcą ją zastosować, ale zamiast tego, tworząc zmienną środowiskową, pozwalamy naszym użytkownikom ustawić zmienną środowiskową raz i mieć wszystkie ich wyszukiwania bez uwzględniania wielkości liter w tej sesji terminala.

Pisanie nieudanego testu dla wyszukiwania bez uwzględniania wielkości liter

Najpierw dodajemy nową funkcję search_case_insensitive do biblioteki minigrep, która zostanie wywołana, gdy zmienna środowiskowa będzie miała wartość. Będziemy kontynuować proces TDD, więc pierwszym krokiem ponownie jest napisanie nieudanego testu. Dodamy nowy test dla nowej funkcji search_case_insensitive i zmienimy nazwę naszego starego testu z one_result na case_sensitive, aby wyjaśnić różnice między dwoma testami, jak pokazano w 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)
        );
    }
}

Zauważ, że zmodyfikowaliśmy również contents starego testu. Dodaliśmy nowy wiersz z tekstem "Duct tape." z wielką literą D, który nie powinien pasować do zapytania "duct" podczas wyszukiwania z uwzględnieniem wielkości liter. Zmiana starego testu w ten sposób pomaga upewnić się, że nie uszkodzimy przypadkowo funkcjonalności wyszukiwania z uwzględnieniem wielkości liter, którą już zaimplementowaliśmy. Ten test powinien teraz przejść i powinien nadal przechodzić podczas pracy nad wyszukiwaniem bez uwzględniania wielkości liter.

Nowy test dla wyszukiwania bez uwzględniania wielkości liter (case-insensitive) używa "rUsT" jako zapytania. W funkcji search_case_insensitive, którą zaraz dodamy, zapytanie "rUsT" powinno pasować do wiersza zawierającego "Rust:" z wielką literą R oraz do wiersza "Trust me." mimo że oba mają inną wielkość liter niż zapytanie. To jest nasz test, który zakończy się niepowodzeniem, ponieważ nie zdefiniowaliśmy jeszcze funkcji search_case_insensitive. Możesz dodać szkieletową implementację, która zawsze zwraca pusty wektor, podobnie jak zrobiliśmy to dla funkcji search w Listing 12-16, aby zobaczyć, jak test się kompiluje i kończy niepowodzeniem.

Implementowanie funkcji search_case_insensitive

Funkcja search_case_insensitive, pokazana w Listing 12-21, będzie prawie taka sama jak funkcja search. Jedyną różnicą jest to, że zmienimy query i każdy line na małe litery, tak aby niezależnie od wielkości liter w argumentach wejściowych, miały one tę samą wielkość liter, gdy sprawdzamy, czy wiersz zawiera zapytanie.

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

Najpierw zmieniamy ciąg znaków query na małe litery i przechowujemy go w nowej zmiennej o tej samej nazwie, zasłaniając oryginalny query. Wywołanie to_lowercase na zapytaniu jest konieczne, aby niezależnie od tego, czy zapytanie użytkownika to "rust", "RUST", "Rust", czy "rUsT", traktować zapytanie tak, jakby było "rust" i ignorować wielkość liter. Chociaż to_lowercase poradzi sobie z podstawowym Unicode, nie będzie w 100 procentach dokładne. Gdybyśmy pisali prawdziwą aplikację, chcielibyśmy włożyć w to więcej pracy, ale ta sekcja dotyczy zmiennych środowiskowych, a nie Unicode, więc na tym poprzestaniemy.

Zauważ, że query jest teraz String, a nie wycinkiem ciągu znaków, ponieważ wywołanie to_lowercase tworzy nowe dane, zamiast odwoływać się do istniejących. Na przykład, jeśli zapytanie to "rUsT": ten wycinek ciągu znaków nie zawiera małych liter u ani t, których moglibyśmy użyć, więc musimy zaalokować nowy String zawierający "rust". Kiedy teraz przekazujemy query jako argument do metody contains, musimy dodać ampersand, ponieważ sygnatura contains jest zdefiniowana tak, aby przyjmowała wycinek ciągu znaków.

Następnie dodajemy wywołanie to_lowercase na każdym line, aby zmienić wszystkie znaki na małe litery. Teraz, gdy przekonwertowaliśmy line i query na małe litery, znajdziemy dopasowania niezależnie od wielkości liter w zapytaniu.

Sprawdźmy, czy ta implementacja przechodzi testy:

$ 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

Świetnie! Testy przeszły. Teraz wywołajmy nową funkcję search_case_insensitive z funkcji run. Najpierw dodamy opcję konfiguracji do struktury Config, aby przełączać się między wyszukiwaniem z uwzględnianiem wielkości liter a wyszukiwaniem bez uwzględniania wielkości liter. Dodanie tego pola spowoduje błędy kompilacji, ponieważ nigdzie jeszcze nie inicjalizujemy tego pola:

Nazwa pliku: 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(())
}

Dodaliśmy pole ignore_case, które przechowuje wartość typu boolean. Następnie funkcja run musi sprawdzić wartość pola ignore_case i na tej podstawie zdecydować, czy wywołać funkcję search, czy funkcję search_case_insensitive, jak pokazano w Listing 12-22. To jeszcze się nie skompiluje.

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

Na koniec musimy sprawdzić zmienną środowiskową. Funkcje do pracy ze zmiennymi środowiskowymi znajdują się w module env w standardowej bibliotece, który jest już w zakresie na początku pliku src/main.rs. Użyjemy funkcji var z modułu env, aby sprawdzić, czy jakakolwiek wartość została ustawiona dla zmiennej środowiskowej o nazwie IGNORE_CASE, jak pokazano w 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(())
}

Tutaj tworzymy nową zmienną ignore_case. Aby ustawić jej wartość, wywołujemy funkcję env::var i przekazujemy jej nazwę zmiennej środowiskowej IGNORE_CASE. Funkcja env::var zwraca Result, który będzie wariantem Ok zawierającym wartość zmiennej środowiskowej, jeśli zmienna środowiskowa jest ustawiona na jakąkolwiek wartość. Zwróci wariant Err, jeśli zmienna środowiskowa nie jest ustawiona.

Używamy metody is_ok na Result, aby sprawdzić, czy zmienna środowiskowa jest ustawiona, co oznacza, że program powinien przeprowadzić wyszukiwanie bez uwzględniania wielkości liter. Jeśli zmienna środowiskowa IGNORE_CASE nie jest ustawiona na nic, is_ok zwróci false, a program przeprowadzi wyszukiwanie z uwzględnieniem wielkości liter. Nie obchodzi nas wartość zmiennej środowiskowej, tylko to, czy jest ustawiona, czy nie, więc sprawdzamy is_ok zamiast używać unwrap, expect lub innych metod, które widzieliśmy na Result.

Przekazujemy wartość zmiennej ignore_case do instancji Config, aby funkcja run mogła odczytać tę wartość i zdecydować, czy wywołać search_case_insensitive, czy search, jak zaimplementowaliśmy w Listing 12-22.

Wypróbujmy to! Najpierw uruchomimy nasz program bez ustawionej zmiennej środowiskowej i z zapytaniem to, które powinno pasować do każdego wiersza zawierającego słowo to pisanego małymi literami:

$ 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!

Wygląda na to, że to nadal działa! Teraz uruchommy program z IGNORE_CASE ustawionym na 1, ale z tym samym zapytaniem to:

$ IGNORE_CASE=1 cargo run -- to poem.txt

Jeśli używasz PowerShell, musisz ustawić zmienną środowiskową i uruchomić program jako osobne polecenia:

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

Spowoduje to, że IGNORE_CASE będzie obowiązywać przez resztę sesji powłoki. Można go usunąć za pomocą polecenia Remove-Item:

PS> Remove-Item Env:IGNORE_CASE

Ppowinniśmy otrzymać wiersze, które zawierają to z ewentualnymi dużymi literami:

Are you nobody, too?
How dreary to be somebody!
To tell your name the livelong day
To an admiring bog!

Doskonale, dostaliśmy też linie zawierające To! Nasz program minigrep może teraz przeprowadzać wyszukiwanie bez uwzględniania wielkości liter, sterowane zmienną środowiskową. Teraz wiesz, jak zarządzać opcjami ustawionymi za pomocą argumentów wiersza poleceń lub zmiennych środowiskowych.

Niektóre programy pozwalają na użycie argumentów i zmiennych środowiskowych dla tej samej konfiguracji. W takich przypadkach programy decydują, które z nich mają pierwszeństwo. W ramach samodzielnego ćwiczenia spróbuj kontrolować czułość na wielkość liter za pomocą argumentu wiersza poleceń lub zmiennej środowiskowej. Zdecyduj, czy argument wiersza poleceń, czy zmienna środowiskowa powinna mieć pierwszeństwo, jeśli program jest uruchamiany z jednym ustawionym na uwzględnianie wielkości liter, a drugim na ignorowanie wielkości liter.

Moduł std::env zawiera wiele innych przydatnych funkcji do obsługi zmiennych środowiskowych: zajrzyj do jego dokumentacji, aby zobaczyć, co jest dostępne.