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
- Aplicação abrangente dos fundamentos do Rust: propriedade, manipulação de strings, Vec e tratamento de erros com Result
- Projetar uma arquitetura funcional para uma ferramenta de linha de comando (CLI): análise de parâmetros → leitura de arquivos → processamento de dados → saída de resultados
- Use
std::env::args()para analisar os argumentos da linha de comando - Use
std::fs::read_to_string()eBufReadpara ler o arquivo linha por linha - Escrever testes de unidade para verificar se a lógica de pesquisa está correta
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%.
- A equipe de operações enviou um arquivo de log de 200 MB
order-service.log - Encontre todas as linhas que contenham a palavra-chave
ERROR - Tom abriu o editor para fazer uma busca — e o editor travou (o arquivo era muito grande).
- Use o
grepintegrado ao sistema — mas o Tom não o tem em seu computador com Windows - Usando o
Select-Stringdo PowerShell — não conseguia me lembrar da sintaxe, então passei um bom tempo procurando na documentação
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:
- Aceita termos de pesquisa e caminhos de arquivos a partir da linha de comando
- Pesquise linha por linha e imprima as linhas correspondentes e seus números de linha
- Suporta os modos com e sem distinção entre maiúsculas e minúsculas
- Possui um tratamento robusto de erros
# 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
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
minigrep/
├── Cargo.toml
└── src/
└── main.rs # All the code is in main.rs (Standard Library Version)
(2) Cargo.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)
// ============================================
// 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
# 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:
Hello World
Rust is awesome
rust is fast
I love Rust
RUST is powerful
Goodbye
Comando:
cargo run -- "Rust" test.txt
Resultado:
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:
IGNORE_CASE=1 cargo run -- "rust" test.txt
Resultado:
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:
cargo run -- "hello" nonexistent.txt
Saída:
minigrep: error reading file: The system cannot find the file specified. (os error 2)
Exemplo 4: Sem correspondência
Comando:
cargo run -- "python" test.txt
Resultado:
minigrep: no matches found for 'python'
Exemplo 5: Argumentos insuficientes
Comando:
cargo run -- "hello"
Resultado:
usage: minigrep <query> <file_path>
(4) Resultado do teste
$ 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
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))
}
- Recebi a fatia
&[String](não sou o dono; estou apenas pegando emprestada) - Verifique o número de parâmetros; se não houver um número suficiente, retorne
Err(&'static str) - Retorna
Ok((&str, &str))em caso de sucesso — dois trechos de string - Use o tipo
Resultpara o tratamento unificado de erros
(2) Função search
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
}
- O uso da anotação de ciclo de vida
'aindica que o&strretornado tem o mesmo ciclo de vida que o parâmetrocontents. contents.lines()Retorna um iterador para cada linha (excluindo os caracteres de nova linha).enumerate()Adicione um índice a cada linha (começando por 0; ao exibir, some 1 para começar por 1)- Ao ignorar maiúsculas e minúsculas, converta tanto
queryquantolinepara minúsculas antes de compará-los
(3) Função run
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))?;
// ...
}
?Operador: Separse_argsretornarErr, o erro é propagado automaticamenteenv::var("IGNORE_CASE"): Ler variáveis de ambientefs::read_to_string: Ler o arquivo e gravá-lo emStringem uma única operação.map_err(): Converter os tipos de erro da biblioteca padrão em strings mais fáceis de entender
(4) Função main
fn main() {
let args: Vec<String> = env::args().collect();
if let Err(err_msg) = run(&args) {
eprintln!("{}", err_msg);
process::exit(1);
}
}
env::args().collect(): Coletar argumentos da linha de comando paraVec<String>if let Err(...): Processa o resultado retornado porrunpor meio da correspondência de padrõeseprintln!: As mensagens de erro são enviadas para o stderr (erro padrão) em vez de para o stdoutprocess::exit(1): Encerrar o programa com um código de saída diferente de zero (indicando um encerramento anormal)
▶ Exemplo: Recursos avançados — Contagem de números de linha e contexto nos resultados da pesquisa
// ============================================
// 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_surroundingutilizaHashSetpara 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
// ============================================
// 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_filespercorre recursivamente os diretórios e filtra por extensão de arquivo;search_in_filepesquisa 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 destd::env::args_os()? R:args()retorna um iteradorArgs, produzindo o tipoString, que é adequado para a maioria dos cenários.args_os()retornaOsString, 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()eBufReader? 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.BufReaderlê 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 usaread_to_stringpara manter o código conciso; se você estiver processando arquivos extremamente grandes, pode usar os iteradoresBufReader+lines()para processá-los linha por linha.
P: Por que a função
searchrequer um parâmetro de tempo de vida'a? R: Porque o&strnaVec<(usize, &str)>retornada se refere aos dados do parâmetrocontents. A anotação de tempo de vida'ainforma ao compilador que a referência retornada tem o mesmo tempo de vida que o parâmetrocontents. Isso permite que o compilador garanta que os dadoscontentsainda 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 queIGNORE_CASEesteja definida com qualquer valor não vazio, não se leva em conta a distinção entre maiúsculas e minúsculas. DefinirIGNORE_CASE=1,IGNORE_CASE=trueouIGNORE_CASE=yestem o mesmo efeito. Se a variável de ambiente não estiver definida (ou seja,env::varretornaErr) 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 opanic? 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.panicexibe 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
- Arquitetura do projeto: A ferramenta CLI adota uma
main → run → searcharquitetura de três camadas, na qualmainatua apenas como ponto de entrada e lida com o tratamento de erros,runé responsável pela lógica de orquestração esearchconsiste em funções puras. - Argumentos da linha de comando: Use
std::env::args()para coletar argumentos, personalize a funçãoparse_argspara validar o número de argumentos e useResultpara o tratamento unificado de erros - Leitura de arquivos:
std::fs::read_to_string()lê arquivos pequenos em uma única passagem;BufReader+lines()é adequado para processar arquivos grandes linha por linha - Controle de variáveis de ambiente:
std::env::var("IGNORE_CASE")lê as variáveis de ambiente e utiliza a correspondência de padrõesmatchpara determinar se elas estão definidas - Tratamento de erros: Use
Result<(), String>como tipo de retorno;?propaga erros;eprintln!envia a saída para o stderr;process::exit(1)define o código de saída - Testes unitários: Escreva testes usando
#[cfg(test)]e#[test]para cobrir três cenários — normal, de limite e de erro — e execute-os com um único clique usandocargo test
📝 Exercícios
-
Dificuldade ⭐: Adicionar um recurso de “contagem de correspondências” à função de pesquisa: Com base na função
search, adicione uma nova funçãocount_matchesque retorne apenas o número de linhas correspondentes, sem retornar o conteúdo específico das linhas. Por exemplo,count_matches("Rust", contents, true)retorna3. Adicione um sinalizador--countà funçãomain(determinado pelo terceiro parâmetro); se definido como--count, imprima apenas o número de linhas correspondentes. -
Dificuldade ⭐⭐: Adicione um recurso de “linhas de contexto” à ferramenta: modifique a função
searchpara 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, comcontext_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). -
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, comofile1.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”.