Interacționând cu variabilele de mediu

Vom îmbunătăți minigrep prin adăugarea unei opțiuni noi: căutarea care nu face distincție între majuscule și minuscule, ce poate fi activată prin intermediul unei variabile de mediu. Deși această caracteristică ar putea fi oferită ca o opțiune de linie de comandă, ceea ce ar impune utilizatorilor să o introducă de fiecare dată, alegerea de a o configura ca variabilă de mediu le permite acestora să o seteze o singură dată, astfel toate căutările lor în cadrul sesiunii curente de terminal să fie insesibilă la majuscule.

Scrierea unui test nereușit pentru funcția search_case_insensitive

Adăugăm mai întâi o nouă funcție search_case_insensitive care va fi apelată atunci când variabila de mediu deține o valoare. Continuăm să urmăm procesul TDD, așadar primul pas este, din nou, să scriem un test nereușit. Vom adăuga un nou test pentru funcția search_case_insensitive și vom redenumi testul vechi din one_result în case_sensitive pentru a face distincția clară între cele două teste, după cum se arată în Listarea 12-20.

Numele fișierului: 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)
        );
    }
}

Listarea 12-20: Adăugarea unui test nou nereușit pentru funcția de căutare insensibilă la majuscule pe care urmează să o implementăm

Observați că am editat și conținutul testului anterior. Am introdus o linie nouă cu textul "Duct tape." care utilizează un D majuscul și care nu ar trebui să corespundă cu interogarea "duct" când efectuăm căutarea în mod sensibil la majuscule. Această modificare a testului anterior ajută la asigurarea faptului că nu stricăm din greșeală funcționalitatea de căutare sensibilă la majuscule pe care am realizat-o deja. Testul actual ar trebui să fie valid acum și ar trebui să rămână așa pe măsură ce dezvoltăm căutarea insensibilă la majuscule.

Testul nou pentru căutarea insensibilă la majuscule folosește interogarea "rUsT". În funcția search_case_insensitive pe care o vom adăuga în curând, interogarea "rUsT" ar trebui să găsească linia ce conține "Rust:" cu un R majuscul și să identifice și linia "Trust me.", deși ambele au diferite utilizări de majuscule față de interogare. Aceasta este definiția testului nereușit, care va eșua în compilare deoarece nu am definit încă funcția search_case_insensitive. Nu vă abțineți de la adăugarea unei implementări schelet care să returneze mereu un vector gol, asemănător cu modul în care am procedat pentru funcția search în Listarea 12-16, pentru a observa compilarea nereușită a testului.

Implementarea funcției search_case_insensitive

Funcția search_case_insensitive, ilustrată în Listarea 12-21, va fi foarte similară cu funcția search. Unica diferență constă în faptul că vom converti atât interogarea cât și fiecare linie la litere mici, astfel încât, indiferent de cazul literelor argumentelor de intrare, ele vor fi în același format când verificăm dacă linia conține interogarea.

Numele fișierului: 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)
        );
    }
}

Listarea 12-21: Definirea funcției search_case_insensitive pentru a converti interogarea și linia la litere mici înainte de a efectua comparația

În primul rând, convertim string-ul query la litere mici și îl salvăm într-o variabilă nouă, umbrită, păstrând același nume. Apelarea funcției to_lowercase asupra interogării este esențială astfel încât, indiferent de forma interogării utilizatorului – fie "rust", fie "RUST", fie "Rust", fie "rUsT" – noi vom considera interogarea ca fiind "rust", tratând-o fără să ținem cont de cazul literelor. Deși to_lowercase poate procesa caractere Unicode de bază, acuratețea nu va fi de 100%. Dacă am dezvolta o aplicație reală, am dori să fim mai meticuloși în această privință, însă aici ne concentrăm pe variabilele de mediu și nu pe Unicode, așa că o lăsăm așa cum este.

Să reținem că acum query devine un String și nu o secțiune de string, deoarece aplicarea funcției to_lowercase generează date noi în loc să referințieze datele existente. Să luăm de exemplu interogarea "rUsT": secțiunea de string originală nu include un u sau un t în litere mici la care noi să avem acces, așadar este necesar să creăm un nou String care să conțină "rust". Când transmitem interogarea ca argument funcției contains, trebuie să adăugăm semnul ampersand (&) deoarece semnătura funcției contains necesită o secțiune de string.

Acum adăugăm un apel la to_lowercase pentru fiecare line pentru a transforma toate caracterele în litere mici. După ce am convertit line și query la litere mici, vom identifica potriviri indiferent de majusculele sau minusculele interogării.

Să verificăm dacă această implementare trece testele:

$ cargo test
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished test [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

Excelent! Au trecut. Acum, să invocăm funcția nouă search_case_insensitive din cadrul funcției run. Pentru început, vom adăuga o opțiune de configurare în structura Config pentru a alege între căutarea sensibilă și căutarea insensibilă la majuscule. Adăugarea acestui câmp va cauza erori de compilare deoarece nu inițializăm acest câmp în niciun loc momentan:

Numele fișierului: 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)
        );
    }
}

Am adăugat câmpul ignore_case care conține un Boolean. Acum trebuie ca funcția run să verifice valoarea câmpului ignore_case și să folosească această informație pentru a decide dacă va apela funcția search sau search_case_insensitive, după cum este indicat în Listarea 12-22. Această parte încă nu va compila.

Numele fișierului: 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)
        );
    }
}

Listarea 12-22: Apelarea fie search, fie search_case_insensitive în funcție de valoarea din config.ignore_case

În final, avem nevoie să verificăm variabila de mediu. Funcțiile pentru interacțiunea cu variabilele de mediu se află în modulul env din biblioteca standard, așadar îl importăm în domeniu de vizibilitate la începutul src/lib.rs. Vom folosi apoi funcția var din modulul env pentru a determina dacă o valoare a fost stabilită pentru o variabilă de mediu numită IGNORE_CASE, așa cum se arată in Listarea 12-23.

Numele fișierului: 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)
        );
    }
}

Listarea 12-23: Căutarea pentru orice valoare într-o variabilă de mediu numită IGNORE_CASE

Aici, definim o nouă variabilă denumită ignore_case. Pentru a-i aloca o valoare, apelăm funcția env::var pe care o furnizează modulul env, pasându-i ca argument numele variabilei de mediu IGNORE_CASE. Funcția env::var returnează un Result care în caz de succes devine varianta Ok și conține valoarea variabilei de mediu, dacă aceasta a fost setată. Dacă variabila de mediu nu este prezentă, funcția va returna varianta Err.

Folosim metoda is_ok de pe Result pentru a verifica existența variabilei de mediu, semn că programul ar trebui să efectueze o căutare insensibilă la majuscule. Dacă variabila IGNORE_CASE nu este specificată, is_ok va da ca rezultat fals și, în consecință, programul va realiza o căutare sensibilă la majuscule (case-sensitive). Nu ne concentram asupra valorii variabilei de mediu, ci doar asupra faptului dacă aceasta este definită sau nu, prin urmare ne bazăm pe metoda is_ok în loc să optăm pentru unwrap, expect sau alte metode asociate cu Result.

Valoarea din ignore_case este transmisă instanței Config, permițând astfel funcției run să acceseze această informație și să decidă dacă să apeleze search_case_insensitive sau search, conform implementării din Listarea 12-22.

Să încercăm! În primul rând, să rulăm programul fără să setăm variabila de mediu și cu interogarea to, care ar trebui să potrivească orice linie ce conține cuvântul „to” în litere mici:

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

Iată că funcționează și așa! Acum, să executăm programul cu variabila IGNORE_CASE setată la 1, dar păstrând aceeași interogare to.

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

Dacă utilizezi PowerShell, va trebui să setezi variabila de mediu și să execuți programul cu două comenzi separate:

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

Aceasta va face ca IGNORE_CASE să rămână activă pentru restul sesiunii de shell. Poate fi dezactivată folosind cmdlet-ul Remove-Item:

PS> Remove-Item Env:IGNORE_CASE

Ar trebui să obținem linii ce conțin „to” și care ar putea avea litere mari:

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

Excelent, am primit și linii care conțin „To”! Programul nostru minigrep poate acum efectua căutări care nu țin cont de caz, controlate prin intermediul unei variabile de mediu. Acum cunoști modul în care se pot gestiona opțiunile setate fie prin argumente de linie de comandă, fie prin variabile de mediu.

Unele programe permit folosirea argumentelor și variabilelor de mediu pentru aceeași configurare. În asemenea situații, programele stabilesc care din cele două opțiuni are prioritate. Ca exercițiu suplimentar individual, încearcă să controlezi sensibilitatea la majuscule fie printr-un argument de linie de comandă, fie printr-o variabilă de mediu. Decide care dintre cele două - argumentul de linie de comandă sau variabila de mediu - ar trebui să prevaleze în cazul în care programul este executat cu unul setat pe sensibilitate la majuscule și celălalt pe ignorarea diferențelor.

Modulul std::env include multe alte funcționalități utile legate de variabilele de mediu: verifică documentația pentru a vedea ce alte posibilități îți oferă.