🔍 Mini Grep Tool
🎯 Mục Tiêu Dự Án
Xây dựng một công cụ tìm kiếm text đơn giản tương tự grep với các tính năng:
- 🔎 Tìm kiếm pattern trong file
- 📁 Hỗ trợ nhiều file cùng lúc
- 🔤 Case-sensitive và case-insensitive
- 📊 Hiển thị số dòng (line numbers)
- 🎨 Output có màu sắc (colored output)
- 📈 Đếm số kết quả
Bạn Sẽ Học Được
- ✅ Parse command-line arguments với
std::env - ✅ File reading và xử lý line-by-line
- ✅ String searching và pattern matching
- ✅ Error handling với Result
- ✅ Multiple file processing
- ✅ ANSI color codes cho terminal
📦 Bước 1: Tạo Project
cargo new mini_grep
cd mini_grep
🎮 Bước 2: Version 1 - Basic Grep
Mở src/main.rs:
use std::env;
use std::fs;
use std::process;
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
}
fn run(query: &str, filename: &str) -> Result<(), String> {
let contents = fs::read_to_string(filename)
.map_err(|e| format!("Không đọc được file '{}': {}", filename, e))?;
let results = search(query, &contents);
if results.is_empty() {
println!("❌ Không tìm thấy '{}' trong {}", query, filename);
} else {
println!("✅ Tìm thấy {} kết quả trong {}:", results.len(), filename);
for line in results {
println!("{}", line);
}
}
Ok(())
}
fn main() {
let args: Vec<String> = env::args().collect();
if args.len() < 3 {
eprintln!("❌ Cách dùng: {} <pattern> <file>", args[0]);
eprintln!("💡 Ví dụ: {} \"hello\" test.txt", args[0]);
process::exit(1);
}
let query = &args[1];
let filename = &args[2];
if let Err(e) = run(query, filename) {
eprintln!("❌ Lỗi: {}", e);
process::exit(1);
}
}
🚀 Chạy Thử
Tạo file test:
echo "Hello World
Rust is awesome
hello rust
HELLO EVERYONE" > test.txt
Chạy:
cargo run hello test.txt
Output:
✅ Tìm thấy 2 kết quả trong test.txt:
Hello World
hello rust
📖 Giải Thích Code
1. Command-line Arguments
let args: Vec<String> = env::args().collect();
env::args(): Iterator qua tất cả argumentsargs[0]: Tên chương trìnhargs[1]: Argument đầu tiên (pattern)args[2]: Argument thứ hai (filename)
2. Search Function
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
}
<'a>: Lifetime annotation - results sống cùng contentscontains(): Kiểm tra substring
3. Error Handling
let contents = fs::read_to_string(filename)
.map_err(|e| format!("Không đọc được: {}", e))?;
map_err(): Chuyển error type?: Propagate error lên caller
🎨 Bước 3: Version 2 - Case-insensitive và Line Numbers
use std::env;
use std::fs;
use std::process;
struct Config {
query: String,
filename: String,
case_sensitive: bool,
show_line_numbers: bool,
}
impl Config {
fn new(args: &[String]) -> Result<Config, String> {
if args.len() < 3 {
return Err("Thiếu arguments!".to_string());
}
let mut case_sensitive = true;
let mut show_line_numbers = false;
let mut query = String::new();
let mut filename = String::new();
let mut i = 1;
while i < args.len() {
match args[i].as_str() {
"-i" | "--ignore-case" => case_sensitive = false,
"-n" | "--line-number" => show_line_numbers = true,
_ => {
if query.is_empty() {
query = args[i].clone();
} else if filename.is_empty() {
filename = args[i].clone();
}
}
}
i += 1;
}
if query.is_empty() || filename.is_empty() {
return Err("Thiếu pattern hoặc filename!".to_string());
}
Ok(Config {
query,
filename,
case_sensitive,
show_line_numbers,
})
}
}
fn search<'a>(query: &str, contents: &'a str, case_sensitive: bool) -> Vec<(usize, &'a str)> {
let mut results = Vec::new();
let query_lower = query.to_lowercase();
for (line_num, line) in contents.lines().enumerate() {
let matches = if case_sensitive {
line.contains(query)
} else {
line.to_lowercase().contains(&query_lower)
};
if matches {
results.push((line_num + 1, line));
}
}
results
}
fn run(config: Config) -> Result<(), String> {
let contents = fs::read_to_string(&config.filename)
.map_err(|e| format!("Không đọc được file '{}': {}", config.filename, e))?;
let results = search(&config.query, &contents, config.case_sensitive);
if results.is_empty() {
println!("❌ Không tìm thấy '{}' trong {}", config.query, config.filename);
} else {
println!("✅ Tìm thấy {} kết quả trong {}:", results.len(), config.filename);
for (line_num, line) in results {
if config.show_line_numbers {
println!("{}: {}", line_num, line);
} else {
println!("{}", line);
}
}
}
Ok(())
}
fn print_help(program: &str) {
println!("🔍 Mini Grep - Tìm kiếm text trong file");
println!("\n📖 Cách dùng:");
println!(" {} [OPTIONS] <pattern> <file>", program);
println!("\n⚙️ Options:");
println!(" -i, --ignore-case Không phân biệt hoa/thường");
println!(" -n, --line-number Hiển thị số dòng");
println!(" -h, --help Hiển thị trợ giúp");
println!("\n💡 Ví dụ:");
println!(" {} hello test.txt", program);
println!(" {} -i HELLO test.txt", program);
println!(" {} -n -i rust test.txt", program);
}
fn main() {
let args: Vec<String> = env::args().collect();
if args.len() < 2 || args.contains(&"--help".to_string()) || args.contains(&"-h".to_string()) {
print_help(&args[0]);
process::exit(0);
}
let config = match Config::new(&args) {
Ok(c) => c,
Err(e) => {
eprintln!("❌ Lỗi: {}", e);
eprintln!("💡 Dùng --help để xem hướng dẫn");
process::exit(1);
}
};
if let Err(e) = run(config) {
eprintln!("❌ Lỗi: {}", e);
process::exit(1);
}
}
Chạy thử:
# Case-sensitive (mặc định)
cargo run hello test.txt
# Case-insensitive
cargo run -i hello test.txt
# Với line numbers
cargo run -n hello test.txt
# Kết hợp
cargo run -i -n hello test.txt
🎨 Bước 4: Version 3 - Multiple Files và Colored Output
Thêm vào Cargo.toml:
[dependencies]
colored = "2.1"
Code:
use colored::*;
use std::env;
use std::fs;
use std::process;
struct Config {
query: String,
filenames: Vec<String>,
case_sensitive: bool,
show_line_numbers: bool,
count_only: bool,
}
impl Config {
fn new(args: &[String]) -> Result<Config, String> {
if args.len() < 3 {
return Err("Thiếu arguments!".to_string());
}
let mut case_sensitive = true;
let mut show_line_numbers = false;
let mut count_only = false;
let mut query = String::new();
let mut filenames = Vec::new();
let mut i = 1;
while i < args.len() {
match args[i].as_str() {
"-i" | "--ignore-case" => case_sensitive = false,
"-n" | "--line-number" => show_line_numbers = true,
"-c" | "--count" => count_only = true,
_ => {
if query.is_empty() {
query = args[i].clone();
} else {
filenames.push(args[i].clone());
}
}
}
i += 1;
}
if query.is_empty() || filenames.is_empty() {
return Err("Thiếu pattern hoặc filename!".to_string());
}
Ok(Config {
query,
filenames,
case_sensitive,
show_line_numbers,
count_only,
})
}
}
fn search<'a>(query: &str, contents: &'a str, case_sensitive: bool) -> Vec<(usize, &'a str)> {
let mut results = Vec::new();
let query_lower = query.to_lowercase();
for (line_num, line) in contents.lines().enumerate() {
let matches = if case_sensitive {
line.contains(query)
} else {
line.to_lowercase().contains(&query_lower)
};
if matches {
results.push((line_num + 1, line));
}
}
results
}
fn highlight_match(line: &str, query: &str, case_sensitive: bool) -> String {
if case_sensitive {
line.replace(query, &query.red().bold().to_string())
} else {
let query_lower = query.to_lowercase();
let line_lower = line.to_lowercase();
let mut result = String::new();
let mut last_end = 0;
for (i, _) in line_lower.match_indices(&query_lower) {
result.push_str(&line[last_end..i]);
result.push_str(&line[i..i + query.len()].red().bold().to_string());
last_end = i + query.len();
}
result.push_str(&line[last_end..]);
result
}
}
fn search_file(filename: &str, config: &Config) -> Result<(), String> {
let contents = fs::read_to_string(filename)
.map_err(|e| format!("Không đọc được file '{}': {}", filename, e))?;
let results = search(&config.query, &contents, config.case_sensitive);
if config.count_only {
if results.is_empty() {
println!("{}: {}", filename, "0".yellow());
} else {
println!("{}: {}", filename, results.len().to_string().green().bold());
}
} else if results.is_empty() {
println!("\n{}", format!("📁 {}: Không tìm thấy", filename).yellow());
} else {
println!("\n{}", format!("📁 {}: {} kết quả", filename, results.len()).green().bold());
for (line_num, line) in results {
let highlighted = highlight_match(line, &config.query, config.case_sensitive);
if config.show_line_numbers {
println!(" {}: {}", line_num.to_string().blue(), highlighted);
} else {
println!(" {}", highlighted);
}
}
}
Ok(())
}
fn run(config: Config) -> Result<(), String> {
let mut total_matches = 0;
let mut errors = Vec::new();
for filename in &config.filenames {
match search_file(filename, &config) {
Ok(_) => {},
Err(e) => errors.push(e),
}
}
if !errors.is_empty() {
println!("\n{}", "⚠️ Lỗi:".yellow());
for error in errors {
println!(" {}", error.red());
}
}
Ok(())
}
fn print_help(program: &str) {
println!("{}", "🔍 Mini Grep - Tìm kiếm text trong file".bold());
println!("\n📖 Cách dùng:");
println!(" {} [OPTIONS] <pattern> <file...>", program);
println!("\n⚙️ Options:");
println!(" -i, --ignore-case Không phân biệt hoa/thường");
println!(" -n, --line-number Hiển thị số dòng");
println!(" -c, --count Chỉ đếm số kết quả");
println!(" -h, --help Hiển thị trợ giúp");
println!("\n💡 Ví dụ:");
println!(" {} hello test.txt", program);
println!(" {} -i HELLO test.txt", program);
println!(" {} -n rust *.rs", program);
println!(" {} -c -i error log1.txt log2.txt", program);
}
fn main() {
let args: Vec<String> = env::args().collect();
if args.len() < 2 || args.contains(&"--help".to_string()) || args.contains(&"-h".to_string()) {
print_help(&args[0]);
process::exit(0);
}
let config = match Config::new(&args) {
Ok(c) => c,
Err(e) => {
eprintln!("{} {}", "❌ Lỗi:".red().bold(), e);
eprintln!("💡 Dùng --help để xem hướng dẫn");
process::exit(1);
}
};
if let Err(e) = run(config) {
eprintln!("{} {}", "❌ Lỗi:".red().bold(), e);
process::exit(1);
}
}
Chạy thử:
# Tìm trong nhiều file
cargo run rust file1.txt file2.txt file3.txt
# Đếm số kết quả
cargo run -c error *.log
# Tất cả options
cargo run -i -n -c hello *.txt
🎨 Bước 5: Version 4 - Regex Support
Thêm vào Cargo.toml:
[dependencies]
colored = "2.1"
regex = "1.10"
Code với regex:
use colored::*;
use regex::Regex;
use std::env;
use std::fs;
use std::process;
struct Config {
pattern: String,
filenames: Vec<String>,
case_sensitive: bool,
show_line_numbers: bool,
count_only: bool,
use_regex: bool,
}
impl Config {
fn new(args: &[String]) -> Result<Config, String> {
if args.len() < 3 {
return Err("Thiếu arguments!".to_string());
}
let mut case_sensitive = true;
let mut show_line_numbers = false;
let mut count_only = false;
let mut use_regex = false;
let mut pattern = String::new();
let mut filenames = Vec::new();
let mut i = 1;
while i < args.len() {
match args[i].as_str() {
"-i" | "--ignore-case" => case_sensitive = false,
"-n" | "--line-number" => show_line_numbers = true,
"-c" | "--count" => count_only = true,
"-e" | "--regex" => use_regex = true,
_ => {
if pattern.is_empty() {
pattern = args[i].clone();
} else {
filenames.push(args[i].clone());
}
}
}
i += 1;
}
if pattern.is_empty() || filenames.is_empty() {
return Err("Thiếu pattern hoặc filename!".to_string());
}
Ok(Config {
pattern,
filenames,
case_sensitive,
show_line_numbers,
count_only,
use_regex,
})
}
}
fn search_regex<'a>(pattern: &str, contents: &'a str, case_sensitive: bool) -> Result<Vec<(usize, &'a str)>, String> {
let regex_pattern = if case_sensitive {
pattern.to_string()
} else {
format!("(?i){}", pattern)
};
let re = Regex::new(®ex_pattern)
.map_err(|e| format!("Regex không hợp lệ: {}", e))?;
let mut results = Vec::new();
for (line_num, line) in contents.lines().enumerate() {
if re.is_match(line) {
results.push((line_num + 1, line));
}
}
Ok(results)
}
fn search<'a>(query: &str, contents: &'a str, case_sensitive: bool) -> Vec<(usize, &'a str)> {
let mut results = Vec::new();
let query_lower = query.to_lowercase();
for (line_num, line) in contents.lines().enumerate() {
let matches = if case_sensitive {
line.contains(query)
} else {
line.to_lowercase().contains(&query_lower)
};
if matches {
results.push((line_num + 1, line));
}
}
results
}
fn highlight_match(line: &str, pattern: &str, case_sensitive: bool, use_regex: bool) -> String {
if use_regex {
let regex_pattern = if case_sensitive {
pattern.to_string()
} else {
format!("(?i){}", pattern)
};
if let Ok(re) = Regex::new(®ex_pattern) {
return re.replace_all(line, |caps: ®ex::Captures| {
caps[0].red().bold().to_string()
}).to_string();
}
}
if case_sensitive {
line.replace(pattern, &pattern.red().bold().to_string())
} else {
let pattern_lower = pattern.to_lowercase();
let line_lower = line.to_lowercase();
let mut result = String::new();
let mut last_end = 0;
for (i, _) in line_lower.match_indices(&pattern_lower) {
result.push_str(&line[last_end..i]);
result.push_str(&line[i..i + pattern.len()].red().bold().to_string());
last_end = i + pattern.len();
}
result.push_str(&line[last_end..]);
result
}
}
fn search_file(filename: &str, config: &Config) -> Result<(), String> {
let contents = fs::read_to_string(filename)
.map_err(|e| format!("Không đọc được file '{}': {}", filename, e))?;
let results = if config.use_regex {
search_regex(&config.pattern, &contents, config.case_sensitive)?
} else {
search(&config.pattern, &contents, config.case_sensitive)
};
if config.count_only {
if results.is_empty() {
println!("{}: {}", filename, "0".yellow());
} else {
println!("{}: {}", filename, results.len().to_string().green().bold());
}
} else if results.is_empty() {
println!("\n{}", format!("📁 {}: Không tìm thấy", filename).yellow());
} else {
println!("\n{}", format!("📁 {}: {} kết quả", filename, results.len()).green().bold());
for (line_num, line) in results {
let highlighted = highlight_match(line, &config.pattern, config.case_sensitive, config.use_regex);
if config.show_line_numbers {
println!(" {}: {}", line_num.to_string().blue(), highlighted);
} else {
println!(" {}", highlighted);
}
}
}
Ok(())
}
fn run(config: Config) -> Result<(), String> {
let mut errors = Vec::new();
for filename in &config.filenames {
match search_file(filename, &config) {
Ok(_) => {},
Err(e) => errors.push(e),
}
}
if !errors.is_empty() {
println!("\n{}", "⚠️ Lỗi:".yellow());
for error in errors {
println!(" {}", error.red());
}
}
Ok(())
}
fn print_help(program: &str) {
println!("{}", "🔍 Mini Grep - Tìm kiếm text trong file".bold());
println!("\n📖 Cách dùng:");
println!(" {} [OPTIONS] <pattern> <file...>", program);
println!("\n⚙️ Options:");
println!(" -i, --ignore-case Không phân biệt hoa/thường");
println!(" -n, --line-number Hiển thị số dòng");
println!(" -c, --count Chỉ đếm số kết quả");
println!(" -e, --regex Dùng regular expression");
println!(" -h, --help Hiển thị trợ giúp");
println!("\n💡 Ví dụ:");
println!(" {} hello test.txt", program);
println!(" {} -i HELLO test.txt", program);
println!(" {} -n rust *.rs", program);
println!(" {} -e \"fn \\w+\" main.rs", program);
println!(" {} -i -e \"error|warning\" *.log", program);
}
fn main() {
let args: Vec<String> = env::args().collect();
if args.len() < 2 || args.contains(&"--help".to_string()) || args.contains(&"-h".to_string()) {
print_help(&args[0]);
process::exit(0);
}
let config = match Config::new(&args) {
Ok(c) => c,
Err(e) => {
eprintln!("{} {}", "❌ Lỗi:".red().bold(), e);
eprintln!("💡 Dùng --help để xem hướng dẫn");
process::exit(1);
}
};
if let Err(e) = run(config) {
eprintln!("{} {}", "❌ Lỗi:".red().bold(), e);
process::exit(1);
}
}
🐛 Lỗi Thường Gặp
Lỗi 1: Không Kiểm Tra Args Length
// ❌ SAI: Panic nếu thiếu args
let query = &args[1]; // Panic nếu args.len() < 2!
// ✅ ĐÚNG: Kiểm tra trước
if args.len() < 3 {
eprintln!("Thiếu arguments!");
process::exit(1);
}
Lỗi 2: Lifetime Issues
// ❌ SAI: Lifetime không khớp
fn search(query: &str, contents: &str) -> Vec<&str> {
// results chứa references đến contents
}
// ✅ ĐÚNG: Explicit lifetime
fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
// OK!
}
Lỗi 3: Regex Escape
// ❌ SAI: Regex characters không escape
let pattern = "test.txt"; // . match bất kỳ char!
// ✅ ĐÚNG: Escape hoặc dùng literal
let pattern = regex::escape("test.txt");
Lỗi 4: Case-insensitive So Sánh
// ❌ SAI: to_lowercase() mỗi lần
for line in lines {
if line.to_lowercase().contains(&query.to_lowercase()) {
// Inefficient!
}
}
// ✅ ĐÚNG: Lowercase query một lần
let query_lower = query.to_lowercase();
for line in lines {
if line.to_lowercase().contains(&query_lower) {
// Tốt hơn!
}
}