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

Lavorare con le Variabili d’Ambiente

Miglioreremo il programma minigrep implementando una funzionalità aggiuntiva: un’opzione per la ricerca senza distinzione tra maiuscole e minuscole, che l’utente può attivare tramite una variabile d’ambiente. Potremmo rendere questa funzionalità un’opzione della riga di comando e richiedere che gli utenti la inseriscano ogni volta che desiderano applicarla, ma rendendola invece una variabile d’ambiente, consentiamo ai nostri utenti di impostare la variabile d’ambiente una sola volta e di fare in modo che tutte le loro ricerche in quella sessione di terminale siano senza distinzione (case-insensitive).

Scrivere un Test che Fallisce per la Ricerca Case-Insensitive

Aggiungiamo innanzitutto una nuova funzione cerca_case_insensitive alla libreria minigrep che verrà chiamata quando la variabile d’ambiente ha un valore. Continueremo a seguire il processo TDD, quindi il primo passo sarà di scrivere un nuovo test che fallisce. Aggiungeremo un nuovo test per la nuova funzione cerca_case_insensitive e rinomineremo il nostro vecchio test da un_risultato a case_sensitive per chiarire le differenze tra i due test, come mostrato nel Listato 12-20.

File: src/lib.rs
pub fn cerca<'a>(query: &str, contenuto: &'a str) -> Vec<&'a str> {
    let mut risultato = Vec::new();

    for line in contenuto.lines() {
        if line.contains(query) {
            risultato.push(line);
        }
    }

    risultato
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn case_sensitive() {
        let query = "dut";
        let contenuto = "\
Rust:
sicuro, veloce, produttivo.
Scegline tre.
Duttilità.";

        assert_eq!(
            vec!["sicuro, veloce, produttivo."],
            cerca(query, contenuto)
        );
    }

    #[test]
    fn case_insensitive() {
        let query = "rUsT";
        let contenuto = "\
Rust:
sicuro, veloce, produttivo.
Scegline tre.
Una frusta.";

        assert_eq!(
            vec!["Rust:", "Una frusta."],
            cerca_case_insensitive(query, contenuto)
        );
    }
}
Listato 12-20: Aggiunta di un nuovo test che fallisce per la funzione case-insensitive che stiamo per aggiungere

Nota che abbiamo modificato anche il contenuto del vecchio test. Abbiamo aggiunto una nuova riga con il testo "Duttilità." usando una D maiuscola che non dovrebbe corrispondere alla query "dut" quando effettuiamo una ricerca con distinzione tra maiuscole e minuscole. Modificare il vecchio test in questo modo ci aiuta a garantire di non interrompere accidentalmente la funzionalità di ricerca con distinzione tra maiuscole e minuscole che abbiamo già implementato. Questo test dovrebbe ora essere superato e dovrebbe continuare a essere superato mentre lavoriamo sulla ricerca senza distinzione tra maiuscole e minuscole.

Il nuovo test per la ricerca case-insensitive utilizza "rUsT" come query. Nella funzione cerca_case_insensitive che stiamo per aggiungere, la query "rUsT" dovrebbe corrispondere alla riga contenente "Rust:" con una R maiuscola e corrispondere alla riga "Una frusta." anche se entrambe differiscono dalla query. Questo è il nostro test che fallisce e non verrà compilato perché non abbiamo ancora definito la funzione cerca_case_insensitive. Sentiti libero di aggiungere un’implementazione scheletro che restituisca sempre un vettore vuoto, simile a quella che abbiamo fatto per la funzione cerca nel Listato 12-16 per verificare che il test si compili correttamente e fallisca.

Implementare la Funzione cerca_case_insensitive

La funzione cerca_case_insensitive, mostrata nel Listato 12-21, sarà quasi la stessa della funzione cerca. L’unica differenza è che metteremo in minuscolo la query e ogni line in modo che, qualunque sia il carattere maiuscolo/minuscolo degli argomenti di input, saranno gli stessi quando controlleremo se la riga contiene la query.

File: src/lib.rs
pub fn cerca<'a>(query: &str, contenuto: &'a str) -> Vec<&'a str> {
    let mut risultato = Vec::new();

    for line in contenuto.lines() {
        if line.contains(query) {
            risultato.push(line);
        }
    }

    risultato
}

pub fn cerca_case_insensitive<'a>(
    query: &str,
    contenuto: &'a str,
) -> Vec<&'a str> {
    let query = query.to_lowercase();
    let mut risultato = Vec::new();

    for line in contenuto.lines() {
        if line.to_lowercase().contains(&query) {
            risultato.push(line);
        }
    }

    risultato
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn case_sensitive() {
        let query = "dut";
        let contenuto = "\
Rust:
sicuro, veloce, produttivo.
Scegline tre.
Duttilità.";

        assert_eq!(vec!["sicuro, veloce, produttivo."], cerca(query, contenuto));
    }

    #[test]
    fn case_insensitive() {
        let query = "rUsT";
        let contenuto = "\
Rust:
sicuro, veloce, produttivo.
Scegline tre.
Una frusta.";

        assert_eq!(
            vec!["Rust:", "Una frusta."],
            cerca_case_insensitive(query, contenuto)
        );
    }
}
Listato 12-21: Definizione della funzione cerca_case_insensitive per rendere minuscole la query e la riga prima di confrontarle

Per prima cosa, rendiamo minuscola la stringa query e la memorizziamo in una nuova variabile con lo stesso nome, adombrando la query originale. La chiamata a to_lowercase sulla query è necessaria affinché, indipendentemente dal fatto che la query dell’utente sia "rust", "RUST", "Rust" o "rUsT", la query verrà trattata come se fosse "rust" e non sarà case-sensitive. Sebbene to_lowercase gestisca Unicode di base, non sarà accurato al 100%. Se stessimo scrivendo un’applicazione reale, dovremmo lavorare un po’ di più qui, ma questa sezione riguarda le variabili d’ambiente, non Unicode, quindi ci fermeremo qui.

Nota che query ora è una String anziché una slice di stringa, perché la chiamata a to_lowercase crea nuovi dati anziché fare reference a dati esistenti. Supponiamo che la query sia "rUsT", ad esempio: quella slice non contiene una u o una t minuscola da utilizzare, quindi dobbiamo allocare una nuova String contenente "rust". Quando passiamo query come argomento al metodo contains ora, dobbiamo aggiungere una & (e commerciale) perché la firma di contains è definita per accettare una slice di stringa.

Successivamente, aggiungiamo una chiamata a to_lowercase su ogni line per convertire in minuscolo tutti i caratteri della riga su cui stiamo facendo la ricerca. Ora che abbiamo convertito line e query in minuscolo, troveremo corrispondenze indipendentemente dalle maiuscole e dalle minuscole della query.

Vediamo se questa implementazione supera i test:

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

Ottimo! Hanno superato i test. Ora, chiamiamo la nuova funzione cerca_case_insensitive dalla funzione esegui. Per prima cosa, aggiungeremo un’opzione di configurazione alla struttura Config per passare dalla ricerca case-sensitive a quella case-insensitive. L’aggiunta di questo campo causerà errori di compilazione perché non lo stiamo ancora inizializzando da nessuna parte:

File: src/main.rs

use std::env;
use std::error::Error;
use std::fs;
use std::process;

use minigrep::{cerca, cerca_case_insensitive};

// --taglio--


fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::build(&args).unwrap_or_else(|err| {
        println!("Problema nella lettura degli argomenti: {err}");
        process::exit(1);
    });

    if let Err(e) = esegui(config) {
        println!("Errore dell'applicazione: {e}");
        process::exit(1);
    }
}

pub struct Config {
    pub query: String,
    pub percorso_file: String,
    pub ignora_maiuscole: bool,
}

impl Config {
    fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("non ci sono abbastanza argomenti");
        }

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

        Ok(Config { query, percorso_file })
    }
}

fn esegui(config: Config) -> Result<(), Box<dyn Error>> {
    let contenuto = fs::read_to_string(config.percorso_file)?;

    let risultato = if config.ignora_maiuscole {
        cerca_case_insensitive(&config.query, &contenuto)
    } else {
        cerca(&config.query, &contenuto)
    };

    for line in risultato {
        println!("{line}");
    }

    Ok(())
}

Abbiamo aggiunto il campo ignora_maiuscole che contiene un valore booleano. Successivamente, abbiamo bisogno della funzione esegui per controllare il valore del campo ignora_maiuscole e utilizzarlo per decidere se chiamare la funzione cerca o la funzione cerca_case_insensitive come mostrato nel Listato 12-22. Questo non verrà ancora compilato.

File: src/main.rs
use std::env;
use std::error::Error;
use std::fs;
use std::process;

use minigrep::{cerca, cerca_case_insensitive};

// --taglio--


fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::build(&args).unwrap_or_else(|err| {
        println!("Problema nella lettura degli argomenti: {err}");
        process::exit(1);
    });

    if let Err(e) = esegui(config) {
        println!("Errore dell'applicazione: {e}");
        process::exit(1);
    }
}

pub struct Config {
    pub query: String,
    pub percorso_file: String,
    pub ignora_maiuscole: bool,
}

impl Config {
    fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("non ci sono abbastanza argomenti");
        }

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

        Ok(Config { query, percorso_file })
    }
}

fn esegui(config: Config) -> Result<(), Box<dyn Error>> {
    let contenuto = fs::read_to_string(config.percorso_file)?;

    let risultato = if config.ignora_maiuscole {
        cerca_case_insensitive(&config.query, &contenuto)
    } else {
        cerca(&config.query, &contenuto)
    };

    for line in risultato {
        println!("{line}");
    }

    Ok(())
}
Listato 12-22: Chiamata cerca o cerca_case_insensitive in base al valore in config.ignora_maiuscole

Infine, dobbiamo verificare la variabile d’ambiente. Le funzioni per lavorare con le variabili d’ambiente si trovano nel modulo env della libreria standard, che è già nello scope all’inizio di src/main.rs. Useremo la funzione var del modulo env per verificare se è stato impostato un valore per una variabile d’ambiente denominata IGNORA_MAIUSCOLE, come mostrato nel Listato 12-23.

File: src/main.rs
use std::env;
use std::error::Error;
use std::fs;
use std::process;

use minigrep::{cerca, cerca_case_insensitive};

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::build(&args).unwrap_or_else(|err| {
        println!("Problema nella lettura degli argomenti: {err}");
        process::exit(1);
    });

    if let Err(e) = esegui(config) {
        println!("Errore dell'applicazione: {e}");
        process::exit(1);
    }
}

pub struct Config {
    pub query: String,
    pub percorso_file: String,
    pub ignora_maiuscole: bool,
}

impl Config {
    fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("non ci sono abbastanza argomenti");
        }

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

        let ignora_maiuscole = env::var("IGNORA_MAIUSCOLE").is_ok();

        Ok(Config {
            query,
            percorso_file,
            ignora_maiuscole,
        })
    }
}

fn esegui(config: Config) -> Result<(), Box<dyn Error>> {
    let contenuto = fs::read_to_string(config.percorso_file)?;

    let risultato = if config.ignora_maiuscole {
        cerca_case_insensitive(&config.query, &contenuto)
    } else {
        cerca(&config.query, &contenuto)
    };

    for line in risultato {
        println!("{line}");
    }

    Ok(())
}
Listato 12-23: Controllo di qualsiasi valore in una variabile d’ambiente denominata IGNORA_MAIUSCOLE

Qui creiamo una nuova variabile, ignora_maiuscole. Per impostarne il valore, chiamiamo la funzione env::var e le passiamo il nome della variabile d’ambiente IGNORA_MAIUSCOLE. La funzione env::var restituisce un Result che sarà la variante Ok corretta che contiene il valore della variabile d’ambiente se la variabile d’ambiente è impostata su un valore qualsiasi. Restituirà la variante Err se la variabile d’ambiente non è impostata.

Stiamo utilizzando il metodo is_ok su Result per verificare se la variabile d’ambiente è impostata, il che significa che il programma dovrebbe eseguire una ricerca senza distinzione tra maiuscole e minuscole. Se la variabile d’ambiente IGNORA_MAIUSCOLE non è impostata, is_ok restituirà false e il programma eseguirà una ricerca facendo distinzione tra maiuscole e minuscole. Non ci interessa il valore della variabile d’ambiente, ma solo se è impostata o meno, quindi usare is_ok è sufficiente in questo caso anziché utilizzare unwrap, expect o uno qualsiasi degli altri metodi che abbiamo visto su Result.

Passiamo il valore nella variabile ignora_maiuscole all’istanza Config in modo che la funzione esegui possa leggere quel valore e decidere se chiamare cerca_case_insensitive o cerca, come abbiamo implementato nel Listato 12-22.

Proviamo! Per prima cosa eseguiamo il nostro programma senza la variabile d’ambiente impostata e con la query che, che dovrebbe corrispondere a qualsiasi riga che contenga la parola che in minuscolo:

$ cargo run -- che poesia.txt
   Compiling minigrep v0.1.0 (file:///progetti/minigrep)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.45s
     Running `target/debug/minigrep che poesia.txt`
Sei Nessuno anche tu?
che gracida il tuo nome — tutto giugno —

Sembra che funzioni ancora! Ora eseguiamo il programma con IGNORA_MAIUSCOLE impostato a 1 ma con la stessa query che:

$ IGNORA_MAIUSCOLE=1 cargo run -- che poesia.txt

Se si utilizza PowerShell, sarà necessario impostare la variabile d’ambiente ed eseguire il programma con comandi separati:

PS> $Env:IGNORA_MAIUSCOLE=1; cargo run -- che poesia.txt

Questo farà sì che IGNORA_MAIUSCOLE persista per il resto della sessione shell. Può essere annullato con il cmdlet Remove-Item:

PS> Remove-Item Env:IGNORA_MAIUSCOLE

Dovremmo ottenere righe che contengono che e che potrebbero contenere lettere maiuscole:

Sei Nessuno anche tu?
Che grande peso essere Qualcuno!
che gracida il tuo nome — tutto giugno —

Eccellente, abbiamo trovato anche le righe contenenti C maiuscolo! Il nostro programma minigrep ora può effettuare ricerche case-insensitive, controllate da una variabile d’ambiente. Ora sai come gestire le opzioni impostate utilizzando argomenti della riga di comando o variabili d’ambiente.

Alcuni programmi consentono argomenti e variabili d’ambiente per la stessa configurazione. In questi casi, i programmi decidono che l’uno o l’altro abbia la precedenza. Come esercizio e per sperimentare un po’, prova a controllare la distinzione tra maiuscole e minuscole tramite un argomento della riga di comando o una variabile d’ambiente. Decidi se l’argomento della riga di comando o la variabile d’ambiente debbano avere la precedenza se il programma viene eseguito con uno impostato case-sensitive e l’altro impostato come case-insensitive.

Il modulo std::env contiene molte altre utili funzionalità per gestire le variabili d’ambiente: consulta la sua documentazione per scoprire quali sono disponibili.