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.
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)
);
}
}
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.
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)
);
}
}
cerca_case_insensitive
per rendere minuscole la query e la riga prima di confrontarlePer 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.
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(())
}
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.
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(())
}
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.