Trabalhando com variáveis de ambiente
Vamos melhorar o binário minigrep adicionando um recurso extra: uma opção de
busca sem distinção entre maiúsculas e minúsculas, que o usuário pode ativar
por meio de uma variável de ambiente. Poderíamos transformar isso em uma opção
de linha de comando e exigir que o usuário a informe toda vez que quiser usá-la.
Mas, ao fazê-la ser uma variável de ambiente, permitimos que a pessoa a defina
uma vez e tenha todas as suas buscas sem distinção entre maiúsculas e minúsculas
naquela sessão do terminal.
Escrevendo um teste que falha para a busca sem distinção entre maiúsculas e minúsculas
Primeiro, adicionamos uma nova função search_case_insensitive à biblioteca
minigrep, que será chamada quando a variável de ambiente tiver algum valor.
Vamos continuar seguindo o processo de TDD, então o primeiro passo é, mais uma
vez, escrever um teste que falha. Adicionaremos um novo teste para a função
search_case_insensitive e renomearemos o teste anterior de one_result para
case_sensitive, para deixar mais clara a diferença entre os dois testes, como
mostra a Listagem 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)
);
}
}
Observe que também editamos o contents do teste antigo. Adicionamos uma nova
linha com o texto "Duct tape.", usando D maiúsculo, que não deve
corresponder à consulta "duct" quando estivermos fazendo uma busca sensível a
maiúsculas e minúsculas. Alterar o teste antigo dessa forma ajuda a garantir
que não quebraremos por acidente a funcionalidade de busca sensível a
maiúsculas e minúsculas que já implementamos. Esse teste deve passar agora e
deve continuar passando enquanto trabalhamos na busca insensível a maiúsculas e
minúsculas.
O novo teste para a busca case-insensitive usa "rUsT" como consulta. Na
função search_case_insensitive que estamos prestes a adicionar, a consulta
"rUsT" deve corresponder à linha que contém "Rust:", com R maiúsculo, e
também à linha "Trust me.", embora ambas usem capitalização diferente da
consulta. Esse é o nosso teste com falha, e ele não compilará porque ainda não
definimos a função search_case_insensitive. Se quiser, você pode adicionar
uma implementação esqueleto que sempre retorna um vetor vazio, semelhante ao
que fizemos com a função search na Listagem 12-16, para ver o teste compilar
e falhar.
Implementando a função search_case_insensitive
A função search_case_insensitive, mostrada na Listagem 12-21, será quase
igual à função search. A única diferença é que colocaremos query e cada
line em minúsculas, para que, independentemente da capitalização dos
argumentos de entrada, ambos estejam no mesmo formato quando verificarmos se a
linha contém a consulta.
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)
);
}
}
search_case_insensitive para colocar query e cada linha em minúsculas antes de compará-lasPrimeiro, convertemos a string query para minúsculas e a armazenamos em uma
nova variável com o mesmo nome, sombreando a query original. Chamar
to_lowercase na consulta é necessário para que, não importa se a pessoa
digite "rust", "RUST", "Rust" ou "rUsT", tratemos a consulta como se
fosse "rust" e a busca fique insensível à capitalização. Embora
to_lowercase lide com Unicode básico, ela não será 100% precisa. Se
estivéssemos escrevendo uma aplicação real, provavelmente precisaríamos de
mais trabalho aqui, mas esta seção trata de variáveis de ambiente, não de
Unicode, então vamos deixar assim.
Observe que agora query é uma String, e não mais um string slice, porque
chamar to_lowercase cria novos dados em vez de apenas referenciar os dados
existentes. Suponha, por exemplo, que a consulta seja "rUsT": esse string
slice não contém um u minúsculo nem um t minúsculo que possamos reutilizar,
então precisamos alocar uma nova String contendo "rust". Quando passamos
query como argumento ao método contains, agora precisamos adicionar um
&, porque a assinatura de contains espera um string slice.
Em seguida, adicionamos uma chamada a to_lowercase em cada line para
converter todos os caracteres para minúsculas. Agora que convertemos line e
query, encontraremos correspondências independentemente da capitalização da
consulta.
Vamos ver se essa implementação passa nos testes:
$ 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
Ótimo! Eles passaram. Agora vamos chamar a nova função
search_case_insensitive a partir de run. Primeiro, adicionaremos uma opção
de configuração à struct Config para alternar entre busca sensível e
insensível a maiúsculas e minúsculas. Adicionar esse campo causará erros de
compilação, porque ainda não estamos inicializando esse campo em nenhum lugar:
Nome do arquivo: 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(())
}
Adicionamos o campo ignore_case, que armazena um booleano. Em seguida,
precisamos que a função run verifique o valor desse campo para decidir se
deve chamar search ou search_case_insensitive, como mostra a Listagem
12-22. Isso ainda não compilará.
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(())
}
search ou search_case_insensitive com base no valor de config.ignore_casePor fim, precisamos verificar a variável de ambiente. As funções para trabalhar
com variáveis de ambiente ficam no módulo env da biblioteca padrão, que já
está em escopo no topo de src/main.rs. Usaremos a função var do módulo
env para verificar se algum valor foi definido para uma variável de ambiente
chamada IGNORE_CASE, como mostra a Listagem 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(())
}
IGNORE_CASEAqui criamos uma nova variável, ignore_case. Para definir seu valor, chamamos
env::var e passamos o nome da variável de ambiente IGNORE_CASE. A função
env::var retorna um Result: será a variante Ok, com o valor da variável
de ambiente, se ela estiver definida com qualquer valor; e será a variante
Err se ela não estiver definida.
Estamos usando o método is_ok de Result para verificar se a variável de
ambiente está definida, o que significa que o programa deve fazer uma busca sem
distinção entre maiúsculas e minúsculas. Se a variável IGNORE_CASE não
estiver definida, is_ok retornará false, e o programa fará uma busca
sensível a maiúsculas e minúsculas. Não nos importamos com o valor da
variável de ambiente, apenas se ela está definida ou não; por isso usamos
is_ok em vez de unwrap, expect ou qualquer outro método que já vimos em
Result.
Passamos o valor da variável ignore_case para a instância de Config, para
que a função run possa ler esse valor e decidir se deve chamar
search_case_insensitive ou search, como implementamos na Listagem 12-22.
Vamos testar! Primeiro, executaremos nosso programa sem a variável de ambiente
definida e com a consulta to, que deve corresponder a qualquer linha que
contenha a palavra to em minúsculas:
$ 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!
Parece que continua funcionando! Agora vamos executar o programa com
IGNORE_CASE definido como 1, mas com a mesma consulta to:
$ IGNORE_CASE=1 cargo run -- to poem.txt
Se você estiver usando PowerShell, precisará definir a variável de ambiente e executar o programa como comandos separados:
PS> $Env:IGNORE_CASE=1; cargo run -- to poem.txt
Isso fará com que IGNORE_CASE permaneça definido pelo restante da sua sessão
de shell. Você pode removê-lo com o cmdlet Remove-Item:
PS> Remove-Item Env:IGNORE_CASE
Devemos obter linhas que contenham to, incluindo aquelas em que a palavra aparece com letras maiúsculas:
Are you nobody, too?
How dreary to be somebody!
To tell your name the livelong day
To an admiring bog!
Excelente, também obtivemos linhas contendo To! Nosso programa minigrep
agora consegue fazer busca sem distinção entre maiúsculas e minúsculas,
controlada por uma variável de ambiente. Agora você sabe como gerenciar opções
definidas usando argumentos de linha de comando ou variáveis de ambiente.
Alguns programas aceitam tanto argumentos quanto variáveis de ambiente para a mesma configuração. Nesses casos, eles decidem que um ou outro tem precedência. Como exercício extra, tente controlar a sensibilidade a maiúsculas e minúsculas por meio de um argumento de linha de comando ou de uma variável de ambiente. Decida se o argumento da linha de comando ou a variável de ambiente deve ter precedência se o programa for executado com um configurado para busca sensível e o outro para ignorar a capitalização.
O módulo std::env contém muitos outros recursos úteis para lidar com
variáveis de ambiente. Consulte a documentação para ver o que está disponível.