Rust: Rust em ação

Última atualização: 2026-08-26

Esta é a última parte do tutorial sobre Rust — vamos usar tudo o que você aprendeu nas primeiras 30 lições para criar uma ferramenta funcional de busca de arquivos na linha de comando (uma versão simplificada do grep). Abordaremos tudo, desde os requisitos até o código, dos testes até a execução, sem pular nenhuma etapa.

Se as primeiras 30 aulas foram sobre “aprender os movimentos”, então esta aula é sobre “colocá-los em prática”. É como ter dominado todas as técnicas culinárias e, então, preparar de fato uma refeição completa para os convidados. Você pode perceber que seu tempo ainda não está perfeito ou que suas habilidades com a faca ainda não estão totalmente aprimoradas — mas, assim que terminar este prato, você terá realmente passado de “ter aprendido Rust” para “ser capaz de escrever código em Rust”.


1. O que você vai aprender


2. História: Procurando uma agulha em um mar de toras

(1) O trabalho de analisar manualmente os registros

Tom é responsável pela manutenção do serviço de pedidos de uma plataforma de comércio eletrônico. Tarde da noite de uma quarta-feira, o sistema de alertas disparou — um grande número de pedidos havia falhado, e a taxa de erros disparou para 30%.

Trinta minutos depois, Tom finalmente descobriu a causa do erro: o pool de conexões do banco de dados havia se esgotado. Mas aqueles preciosos 30 minutos já haviam sido desperdiçados “procurando a ferramenta certa”.

“Se eu tivesse uma ferramenta de busca que eu mesmo tivesse criado, conseguiria fazer isso em questão de segundos...”

(2) Nossa proposta

Hoje, vou escrever uma ferramenta de busca de arquivos na linha de comando em Rust (grep-lite), que:

BASH
# Basic Usage
cargo run -- "ERROR" order-service.log

# Case-insensitive mode
IGNORE_CASE=1 cargo run -- "error" order-service.log

# Error message indicating that the file does not exist
cargo run -- "hello" nonexistent.txt
# Output: minigrep: error reading file: The system cannot find the file specified. (os error 2)

3. Requisitos do projeto

(1) Lista de recursos

# Recurso Descrição
1 Argumentos da linha de comando Aceita dois argumentos: termo de pesquisa (query) e caminho do arquivo (file_path)
2 Leitura de arquivo Ler todo o conteúdo do arquivo especificado
3 Pesquisar linha por linha Verifica cada linha em busca do termo de pesquisa e exibe as linhas correspondentes (com os números das linhas)
4 Sensibilidade a maiúsculas e minúsculas Use a variável de ambiente IGNORE_CASE para controlar se as maiúsculas e minúsculas devem ser ignoradas
5 Tratamento de erros Tratar adequadamente erros como arquivos não encontrados, permissões insuficientes e parâmetros ausentes

(2) Comparação entre os módulos do projeto e os conceitos-chave

Módulo/Função Conceito correspondente em Rust Curso de origem
parse_args() Argumentos da linha de comando env::args, Result 29 — Biblioteca padrão
search() Pesquisa em cadeias de caracteres contains, Iteradores enumerate 03-Cadeias de caracteres, 22-Iteradores
run() Operadores de E/S de arquivo fs::read_to_string, ? 27-E/S de arquivo, 09-Tratamento de erros
main() Controle de Processos match, process::exit 05-Controle de Processos
IGNORE_CASE Variáveis de ambiente env::var 29-Biblioteca padrão
#[cfg(test)] Módulo de Testes Unitários 12-Testes
&str / String Propriedade e Empréstimo 02-Propriedade, 03-Cordas
Vec<(usize, &str)> Tuplas de vetores, genéricos 14-Vetores, 19-Genéricos

(3) Projeto de argumentos de linha de comando

Posição do parâmetro Nome da variável Tipo Obrigatório Descrição
args[0] Nome do programa String Automático minigrep
args[1] query &str Sim Palavras-chave para pesquisa
args[2] file_path &str Sim Caminho do arquivo de destino
Variável de ambiente IGNORE_CASE String Não Ignorar maiúsculas e minúsculas quando definida como 1

(4) Arquitetura do projeto

100%
graph TB
    A[main] --> B[parse_args]
    A --> C[run]
    C --> D[read file via fs::read_to_string]
    C --> E[read env var IGNORE_CASE]
    C --> F[search lines]
    F --> G[print matching lines]
    C --> H[return Result for error handling]

    B --> I{args.len() < 3?}
    I -->|Yes| J[return Err]
    I -->|No| K[return (query, file_path)]

    subgraph search
        F1[query + content + case_sensitive] --> F2[iterate lines with enumerate]
        F2 --> F3{line contains query?}
        F3 -->|Yes| F4[push (line_no, line)]
        F3 -->|No| F5[skip]
    end

    style A fill:#4a90d9,color:#fff
    style C fill:#67c23a,color:#fff
    style F fill:#e6a23c,color:#fff
    style H fill:#f56c6c,color:#fff

4. Código completo do projeto

▶ Exemplo: Implementação completa do código do grep-lite

(1) Estrutura do projeto

TEXT 📖 Somente leitura
minigrep/
├── Cargo.toml
└── src/
    └── main.rs      # All the code is in main.rs (Standard Library Version)

(2) Cargo.toml

TOML
[package]
name = "minigrep"
version = "0.1.0"
edition = "2021"

# This course uses only the standard library.,No external dependencies required
[dependencies]

(3) src/main.rs (Código completo)

RUST
// ============================================
// minigrep - Command-Line File Search Tool (grep Simplified Version)
// Features:
//   1. Accept two command-line arguments: Search Terms + File Path
//   2. Read the contents of a file,Search by Line
//   3. Print matching lines (With line numbers)
//   4. Via environment variables IGNORE_CASE Case Sensitivity
//   5. Elegant Error Handling
// ============================================

use std::env;
use std::fs;
use std::process;

/// Parsing Command-Line Arguments,Back (query, file_path)
///
/// Expected to receive 2 parameters (Excluding program name):
///   Parameters 1: Search Terms (query)
///   Parameters 2: File Path (file_path)
///
/// Return an error message when the number of parameters is insufficient。
fn parse_args(args: &[String]) -> Result<(&str, &str), &'static str> {
    if args.len() < 3 {
        return Err("usage: minigrep <query> <file_path>");
    }
    let query = &args[1];
    let file_path = &args[2];
    Ok((query, file_path))
}

/// Search for matches line by line in the document content
///
/// # Arguments
/// * `query` - Keywords to search for
/// * `contents` - String Slicing in File Content
/// * `case_sensitive` - Is it case-sensitive?
///
/// # Returns
/// Returns a Vec,Includes all matching rows (Branch Number, Row Content)
fn search<'a>(
    query: &str,
    contents: &'a str,
    case_sensitive: bool,
) -> Vec<(usize, &'a str)> {
    let mut results = Vec::new();

    for (line_no, line) in contents.lines().enumerate() {
        let matched = if case_sensitive {
            line.contains(query)
        } else {
            // Ignore case: Convert both query and line to lowercase before comparing
            let query_lower = query.to_lowercase();
            let line_lower = line.to_lowercase();
            line_lower.contains(&query_lower)
        };

        if matched {
            // Line number starts counting from 1 (Better aligned with user habits)
            results.push((line_no + 1, line));
        }
    }

    results
}

/// Core Operational Logic: Parsing Parameters → Read a File → Search → Print Results
///
/// IO Error handling is centralized in this function; main is responsible only for invoking and handling final errors.
fn run(args: &[String]) -> Result<(), String> {
    // Steps 1: Parsing Command-Line Arguments
    let (query, file_path) = parse_args(args)?;

    // Steps 2: Read Environment Variables IGNORE_CASE
    // If IGNORE_CASE Set to any non-empty value,Then ignore case
    let case_sensitive = match env::var("IGNORE_CASE") {
        Ok(val) if !val.is_empty() => false, // Ignore case
        _ => true,                           // Case-sensitive by default
    };

    // Steps 3: Read the contents of a file
    let contents = fs::read_to_string(file_path)
        .map_err(|e| format!("minigrep: error reading file: {}", e))?;

    // Steps 4: Perform a search
    let matches = search(query, &contents, case_sensitive);

    // Steps 5: Print Results
    if matches.is_empty() {
        println!("minigrep: no matches found for '{}'", query);
    } else {
        println!(
            "minigrep: found {} match(es) for '{}'{}",
            matches.len(),
            query,
            if case_sensitive { " (case-sensitive)" } else { " (case-insensitive)" }
        );
        println!("---");
        for (line_no, line) in &matches {
            println!("{:>4}: {}", line_no, line);
        }
    }

    Ok(())
}

/// Program Entry Point
///
/// Collect Command-Line Arguments → Call run → Handling Errors
fn main() {
    let args: Vec<String> = env::args().collect();

    // Call run,If an error occurs, print a message and exit.
    if let Err(err_msg) = run(&args) {
        eprintln!("{}", err_msg);
        process::exit(1);
    }
}

// ============================================
// Unit Testing
// ============================================

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

    /// Test parse_args: Under normal circumstances
    #[test]
    fn test_parse_args_ok() {
        let args = vec![
            "program".to_string(),
            "hello".to_string(),
            "test.txt".to_string(),
        ];
        let result = parse_args(&args);
        assert!(result.is_ok());
        let (query, path) = result.unwrap();
        assert_eq!(query, "hello");
        assert_eq!(path, "test.txt");
    }

    /// Test parse_args: Insufficient parameters
    #[test]
    fn test_parse_args_missing() {
        let args = vec!["program".to_string()];
        let result = parse_args(&args);
        assert!(result.is_err());
    }

    /// Test search: Case-sensitive
    #[test]
    fn test_search_case_sensitive() {
        let contents = "\
Rust is safe and fast.
rust is a systems language.
I love Rust programming.
RUST is awesome!";

        let query = "Rust";
        let results = search(query, contents, true);

        assert_eq!(results.len(), 2);
        assert_eq!(results[0], (1, "Rust is safe and fast."));
        assert_eq!(results[1], (3, "I love Rust programming."));
    }

    /// Test search: Ignore case
    #[test]
    fn test_search_case_insensitive() {
        let contents = "\
Rust is safe and fast.
rust is a systems language.
I love Rust programming.
RUST is awesome!";

        let query = "rust";
        let results = search(query, contents, false);

        // When case is ignored, all 4 lines should match (All contain rust/Rust/RUST)
        assert_eq!(results.len(), 4);
        assert_eq!(results[0], (1, "Rust is safe and fast."));
        assert_eq!(results[1], (2, "rust is a systems language."));
        assert_eq!(results[2], (3, "I love Rust programming."));
        assert_eq!(results[3], (4, "RUST is awesome!"));
    }

    /// Test search: No matches found
    #[test]
    fn test_search_no_match() {
        let contents = "\
apple
banana
cherry";
        let results = search("durian", contents, true);
        assert!(results.is_empty());
    }

    /// Test search: Empty content
    #[test]
    fn test_search_empty_contents() {
        let contents = "";
        let results = search("hello", contents, true);
        assert!(results.is_empty());
    }

    /// Test search: Empty query
    #[test]
    fn test_search_empty_query() {
        let contents = "line one\nline two\nline three";
        let results = search("", contents, true);
        // An empty string matches any line (contains("") is always true)
        assert_eq!(results.len(), 3);
    }

    /// Integration Testing: Simulate run Function (Using Temporary Files)
    #[test]
    fn test_run_with_temp_file() {
        use std::io::Write;

        // Create a temporary file
        let mut temp_file = tempfile::NamedTempFile::new().unwrap();
        write!(temp_file, "hello world\nrust is great\nHELLO everyone\n").unwrap();
        let temp_path = temp_file.path().to_str().unwrap().to_string();

        // Construction Parameters
        let args = vec![
            "minigrep".to_string(),
            "hello".to_string(),
            temp_path.clone(),
        ];

        // Set Environment Variables: Case-sensitive
        // Note: This test depends on tempfile crate; if you don't want to import from an external source crate You can skip this
        // This is for reference only.,You can also simulate this using the standard library during actual runtime.
        let result = run(&args);
        assert!(result.is_ok());
    }
}

// ============================================
// Note:
// 1. You can copy the code above directly into src/main.rs Compile and Run
// 2. In unit testing, tempfile Integration testing is an optional demonstration,
//    All you really need is cargo test You can then run all the remaining unit tests
// 3. Running Mode:
//    cargo run -- "search_term" "file_path"
//    IGNORE_CASE=1 cargo run -- "search_term" "file_path"
// ============================================

▶ Exemplo: Compilando e executando

BASH
# 1. Create a Project
cargo new minigrep
cd minigrep

# 2. Copy the code above to src/main.rs

# 3. Compilation
cargo build

# 4. Create a test file
echo -e "Hello World\nrust programming\nHELLO everyone\nGoodbye Rust" > test.txt

# 5. Run (Case-sensitive)
cargo run -- "rust" test.txt

# 6. Run (Ignore case)
IGNORE_CASE=1 cargo run -- "rust" test.txt

# 7. Run Test
cargo test

▶ Exemplo: Executando o exemplo

Exemplo 1: Pesquisa básica (diferencia maiúsculas de minúsculas)

Criar arquivo de teste test.txt:

TEXT 📖 Somente leitura
Hello World
Rust is awesome
rust is fast
I love Rust
RUST is powerful
Goodbye

Comando:

BASH
cargo run -- "Rust" test.txt

Resultado:

TEXT 📖 Somente leitura
minigrep: found 2 match(es) for 'Rust' (case-sensitive)
---
   1: Hello World
   3: I love Rust

Exemplo 2: Ignorar maiúsculas e minúsculas

Comando:

BASH
IGNORE_CASE=1 cargo run -- "rust" test.txt

Resultado:

TEXT 📖 Somente leitura
minigrep: found 4 match(es) for 'rust' (case-insensitive)
---
   1: Hello World
   2: Rust is awesome
   3: rust is fast
   4: I love Rust

Exemplo 3: O arquivo não existe

Comando:

BASH
cargo run -- "hello" nonexistent.txt

Saída:

TEXT 📖 Somente leitura
minigrep: error reading file: The system cannot find the file specified. (os error 2)

Exemplo 4: Sem correspondência

Comando:

BASH
cargo run -- "python" test.txt

Resultado:

TEXT 📖 Somente leitura
minigrep: no matches found for 'python'

Exemplo 5: Argumentos insuficientes

Comando:

BASH
cargo run -- "hello"

Resultado:

TEXT 📖 Somente leitura
usage: minigrep <query> <file_path>

(4) Resultado do teste

BASH
$ cargo test
   Compiling minigrep v0.1.0
    Finished `test` profile [unoptimized + debuginfo] target(s) in 1.23s
     Running unittests src/main.rs

running 7 tests
test tests::test_parse_args_ok ... ok
test tests::test_parse_args_missing ... ok
test tests::test_search_case_sensitive ... ok
test tests::test_search_case_insensitive ... ok
test tests::test_search_no_match ... ok
test tests::test_search_empty_contents ... ok
test tests::test_search_empty_query ... ok

test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

Todos os 7 testes unitários foram aprovados! Eles abrangem casos extremos, como buscas normais (com e sem distinção de maiúsculas e minúsculas), ausência de resultados, conteúdo vazio, consultas vazias e análise de parâmetros. Esse é o poder do sistema de testes do Rust #[cfg(test)] e #[test] — o código de teste é colocado no mesmo arquivo que o código de produção, e a compilação condicional garante que o código de teste não seja incluído nas compilações de produção.


5. Explicação do código

(1) parse_args Função

RUST
fn parse_args(args: &[String]) -> Result<(&str, &str), &'static str> {
    if args.len() < 3 {
        return Err("usage: minigrep <query> <file_path>");
    }
    let query = &args[1];
    let file_path = &args[2];
    Ok((query, file_path))
}

(2) Função search

RUST
fn search<'a>(
    query: &str,
    contents: &'a str,
    case_sensitive: bool,
) -> Vec<(usize, &'a str)> {
    let mut results = Vec::new();
    for (line_no, line) in contents.lines().enumerate() {
        let matched = if case_sensitive {
            line.contains(query)
        } else {
            let query_lower = query.to_lowercase();
            let line_lower = line.to_lowercase();
            line_lower.contains(&query_lower)
        };
        if matched {
            results.push((line_no + 1, line));
        }
    }
    results
}

(3) Função run

RUST
fn run(args: &[String]) -> Result<(), String> {
    let (query, file_path) = parse_args(args)?;
    // ...
    let case_sensitive = match env::var("IGNORE_CASE") {
        Ok(val) if !val.is_empty() => false,
        _ => true,
    };
    let contents = fs::read_to_string(file_path)
        .map_err(|e| format!("minigrep: error reading file: {}", e))?;
    // ...
}

(4) Função main

RUST
fn main() {
    let args: Vec<String> = env::args().collect();
    if let Err(err_msg) = run(&args) {
        eprintln!("{}", err_msg);
        process::exit(1);
    }
}

▶ Exemplo: Recursos avançados — Contagem de números de linha e contexto nos resultados da pesquisa

RUST
// ============================================
// Expand grep-lite:Show line numbers and surrounding lines
// ============================================

use std::env;
use std::fs;
use std::process;

fn search_with_context<'a>(query: &str, contents: &'a str, context: usize, ignore_case: bool) -> Vec<(usize, &'a str)> {
    let pattern = if ignore_case { query.to_lowercase() } else { query.to_string() };
    contents.lines()
        .enumerate()
        .filter(|(_, line)| {
            let haystack = if ignore_case { line.to_lowercase() } else { line.to_string() };
            haystack.contains(&pattern)
        })
        .map(|(i, line)| (i + 1, line))
        .collect()
}

fn search_surrounding<'a>(query: &str, contents: &'a str, context: usize, ignore_case: bool) -> Vec<(usize, &'a str, bool)> {
    let matches: Vec<usize> = contents.lines().enumerate()
        .filter(|(_, line)| {
            let haystack = if ignore_case { line.to_lowercase() } else { line.to_string() };
            haystack.contains(&if ignore_case { query.to_lowercase() } else { query.to_string() })
        })
        .map(|(i, _)| i)
        .collect();

    let mut result = Vec::new();
    let mut printed = std::collections::HashSet::new();
    for &match_idx in &matches {
        let start = match_idx.saturating_sub(context);
        let end = (match_idx + context + 1).min(contents.lines().count());
        for i in start..end {
            if printed.insert(i) {
                let is_match = i == match_idx;
                result.push((i + 1, contents.lines().nth(i).unwrap_or(""), is_match));
            }
        }
    }
    result
}

fn main() {
    let args: Vec<String> = env::args().collect();
    if args.len() < 3 {
        eprintln!("Usage: {} <Search> <Documents> [--context N] [--ignore-case]", args[0]);
        process::exit(1);
    }

    let query = &args[1];
    let filename = &args[2];
    let context = args.iter().position(|a| a == "--context")
        .and_then(|i| args.get(i + 1))
        .and_then(|s| s.parse::<usize>().ok())
        .unwrap_or(0);
    let ignore_case = args.iter().any(|a| a == "--ignore-case") ||
        env::var("IGNORE_CASE").is_ok();

    let contents = fs::read_to_string(filename).unwrap_or_else(|err| {
        eprintln!("Read a File '{}' Failure: {}", filename, err);
        process::exit(1);
    });

    println!("=== Search '{}' (Ignore case: {}) ===\n", query, ignore_case);

    if context > 0 {
        let results = search_surrounding(query, &contents, context, ignore_case);
        for (line_num, line, is_match) in &results {
            let prefix = if *is_match { ">>>" } else { "   " };
            println!("{} {}: {}", prefix, line_num, line);
        }
    } else {
        let results = search_with_context(query, &contents, context, ignore_case);
        for (line_num, line) in &results {
            println!("{}: {}", line_num, line);
        }
        println!("\nTotal {} matching lines", results.len());
    }
}

A versão estendida adiciona a exibição do número da linha (enumerate) e linhas de contexto (o parâmetro --context N). search_surrounding utiliza HashSet para remover duplicatas e evitar a exibição repetida quando as linhas correspondentes são adjacentes. >>> marca as linhas correspondentes, enquanto as linhas normais são exibidas com recuo.


▶ Exemplo: Recursos avançados — Pesquisa em vários arquivos e pesquisa recursiva em diretórios

RUST
// ============================================
// Expand grep-lite:Supports multiple files and recursive directories
// ============================================

use std::env;
use std::fs;
use std::path::Path;
use std::process;

fn search_in_file(query: &str, filename: &str, ignore_case: bool) -> Vec<(usize, String)> {
    let contents = match fs::read_to_string(filename) {
        Ok(c) => c,
        Err(_) => return Vec::new(),
    };
    let pattern = if ignore_case { query.to_lowercase() } else { query.to_string() };
    contents.lines()
        .enumerate()
        .filter(|(_, line)| {
            let haystack = if ignore_case { line.to_lowercase() } else { line.to_string() };
            haystack.contains(&pattern)
        })
        .map(|(i, line)| (i + 1, line.to_string()))
        .collect()
}

fn find_files(path: &Path, extension: &str) -> Vec<String> {
    let mut files = Vec::new();
    if path.is_file() {
        files.push(path.to_string_lossy().to_string());
    } else if path.is_dir() {
        if let Ok(entries) = fs::read_dir(path) {
            for entry in entries.flatten() {
                let sub_path = entry.path();
                if sub_path.is_dir() {
                    files.extend(find_files(&sub_path, extension));
                } else if sub_path.extension().map(|e| e == extension).unwrap_or(false) {
                    files.push(sub_path.to_string_lossy().to_string());
                }
            }
        }
    }
    files
}

fn main() {
    let args: Vec<String> = env::args().collect();
    if args.len() < 3 {
        eprintln!("Usage: {} <Search> <Path...> [--ignore-case] [--ext rs]", args[0]);
        process::exit(1);
    }

    let query = &args[1];
    let ignore_case = args.iter().any(|a| a == "--ignore-case");
    let extension = args.iter().position(|a| a == "--ext")
        .and_then(|i| args.get(i + 1))
        .map(|s| s.as_str())
        .unwrap_or("rs");

    let mut all_files = Vec::new();
    for path_str in args[2..].iter().filter(|a| !a.starts_with('-')) {
        let path = Path::new(path_str);
        all_files.extend(find_files(path, extension));
    }

    println!("Search '{}' in {} file(s) (Extension: .{})\n", query, all_files.len(), extension);

    let mut total_matches = 0;
    let mut files_with_matches = 0;

    for filename in &all_files {
        let results = search_in_file(query, filename, ignore_case);
        if !results.is_empty() {
            println!("--- {} ---", filename);
            for (line_num, line) in &results {
                println!("  {}: {}", line_num, line);
            }
            total_matches += results.len();
            files_with_matches += 1;
        }
    }

    println!("\n=== Statistics ===");
    println!("Scanned Documents: {}, Matches found: {}, Total Matching Rows: {}", all_files.len(), files_with_matches, total_matches);
}

Suporte à pesquisa em vários arquivos: find_files percorre recursivamente os diretórios e filtra por extensão de arquivo; search_in_file pesquisa dentro de um único arquivo; o formato de saída utiliza --- filename --- para separar os resultados de cada arquivo; e, por fim, fornece um resumo dos resultados da varredura e das correspondências.


❓ Perguntas Frequentes

P: Por que usar env::args() em vez de std::env::args_os()? R: args() retorna um iterador Args, produzindo o tipo String, que é adequado para a maioria dos cenários. args_os() retorna OsString, que pode lidar com parâmetros que não sejam UTF-8, mas é mais complexo de usar. Para os requisitos deste projeto, args() é suficiente.

P: Qual é a diferença entre a leitura linha por linha com fs::read_to_string() e BufReader? R: fs::read_to_string() carrega o arquivo inteiro na memória de uma só vez, o que é adequado para arquivos de pequeno e médio porte. BufReader lê linha por linha e é adequado para arquivos grandes (como arquivos de log com várias centenas de MB), com menor uso de memória. Esta lição usa read_to_string para manter o código conciso; se você estiver processando arquivos extremamente grandes, pode usar os iteradores BufReader + lines() para processá-los linha por linha.

P: Por que a função search requer um parâmetro de tempo de vida 'a? R: Porque o &str na Vec<(usize, &str)> retornada se refere aos dados do parâmetro contents. A anotação de tempo de vida 'a informa ao compilador que a referência retornada tem o mesmo tempo de vida que o parâmetro contents. Isso permite que o compilador garanta que os dados contents ainda estejam válidos quando os resultados da busca forem utilizados, evitando referências pendentes.

P: Quais são as regras para o valor da variável de ambiente IGNORE_CASE? R: Desde que IGNORE_CASE esteja definida com qualquer valor não vazio, não se leva em conta a distinção entre maiúsculas e minúsculas. Definir IGNORE_CASE=1, IGNORE_CASE=true ou IGNORE_CASE=yes tem o mesmo efeito. Se a variável de ambiente não estiver definida (ou seja, env::var retorna Err) ou estiver definida como uma string vazia, a distinção entre maiúsculas e minúsculas será aplicada.

P: Por que usar process::exit(1) em vez de simplesmente chamar o panic? R: process::exit(1) encerra o programa com um código de saída especificado, o que é adequado para ferramentas de CLI — o processo pai (como um script de shell) pode determinar se a execução foi bem-sucedida com base no código de saída. panic exibe um rastreamento de pilha, o que não é intuitivo para os usuários finais. A prática padrão para ferramentas de CLI é retornar o código de saída 0 em caso de sucesso e um código de saída diferente de zero em caso de erro.


📖 Resumo


📝 Exercícios

  1. Dificuldade ⭐: Adicionar um recurso de “contagem de correspondências” à função de pesquisa: Com base na função search, adicione uma nova função count_matches que retorne apenas o número de linhas correspondentes, sem retornar o conteúdo específico das linhas. Por exemplo, count_matches("Rust", contents, true) retorna 3. Adicione um sinalizador --count à função main (determinado pelo terceiro parâmetro); se definido como --count, imprima apenas o número de linhas correspondentes.

  2. Dificuldade ⭐⭐: Adicione um recurso de “linhas de contexto” à ferramenta: modifique a função search para que ela retorne um determinado número de linhas antes e depois da linha correspondente (semelhante à grep -C). Adicione um novo parâmetro, context_lines: usize, para especificar quantas linhas devem ser exibidas antes e depois da linha correspondente. Por exemplo, com context_lines=1, a linha correspondente será acompanhada pela linha imediatamente anterior e pela linha imediatamente posterior a ela. Certifique-se de lidar com casos extremos (linhas no início ou no final do arquivo não têm contexto).

  3. Dificuldade ⭐⭐⭐: Expanda o projeto para uma “ferramenta de busca em vários arquivos”: modifique o programa para permitir a busca em vários arquivos. O novo formato da linha de comando é minigrep <query> <file1> <file2> ... (onde o número de nomes de arquivos é variável). Altere o formato de saída para que cada linha correspondente seja precedida pelo nome do arquivo, como file1.txt:5: Hello World. Dica: use um loop para processar vários arquivos. Embora o modo $()* não seja uma macro, o conceito é semelhante — “processar a lógica uma vez e aplicá-la a várias entradas”.

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%