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

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.

Filename: src/lib.rs
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)
        );
    }
}
Listing 12-20: Adicionando um novo teste com falha para a função de busca sem distinção entre maiúsculas e minúsculas que estamos prestes a criar

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.

Filename: src/lib.rs
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)
        );
    }
}
Listing 12-21: Definindo a função search_case_insensitive para colocar query e cada linha em minúsculas antes de compará-las

Primeiro, 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á.

Filename: 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(())
}
Listing 12-22: Chamando search ou search_case_insensitive com base no valor de config.ignore_case

Por 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.

Filename: src/main.rs
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(())
}
Listing 12-23: Verificando se existe algum valor definido na variável de ambiente IGNORE_CASE

Aqui 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.