iT邦幫忙

2025 iThome 鐵人賽

DAY 3
0
Rust

Rust 實戰專案集:30 個漸進式專案從工具到服務系列 第 3

檔案搜尋工具 - 實作類似 grep 的文字搜尋功能

  • 分享至 

  • xImage
  •  

開始

今天我們要實作一個檔案搜尋工具,類似於 Unix/Linux 系統中的 grep 命令。這個專案將幫助我們學習 Rust 中的檔案處理、字串匹配、命令列參數解析,以及錯誤處理等重要概念。

首先我們先建立 cargo 專案,至於前面做過我們就不贅述

[package]
name = "test-grep"
version = "0.1.0"
edition = "2024"

[dependencies]
clap = { version = "4.5.47", features = ["derive"] }
colored = "3.0.0"
regex = "1.11.2"
walkdir = "2.5.0"

我們會用到 clap colored regex walkdir

實作

利用 clap定義命令列參數 :

use clap::Parser;

#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
pub struct Args {
    /// 要搜尋的模式
    #[arg(value_name = "PATTERN")]
    pub pattern: String,

    /// 要搜尋的檔案路徑
    #[arg(value_name = "FILE")]
    pub file: String,

    /// 忽略大小寫
    #[arg(short, long)]
    pub ignore_case: bool,

    /// 顯示行號
    #[arg(short, long)]
    pub line_number: bool,

    /// 使用正則表達式
    #[arg(short, long)]
    pub regex: bool,

    /// 遞迴搜尋目錄
    #[arg(short = 'R', long)]
    pub recursive: bool,

    /// 只顯示檔案名稱
    #[arg(short, long)]
    pub files_with_matches: bool,
}

建立搜尋機制

use regex::Regex;
use std::error::Error;

pub struct SearchEngine {
    pattern: String,
    ignore_case: bool,
    use_regex: bool,
    compiled_regex: Option<Regex>,
}

impl SearchEngine {
    pub fn new(pattern: String, ignore_case: bool, use_regex: bool) -> Result<Self, Box<dyn Error>> {
        let compiled_regex = if use_regex {
            let regex_pattern = if ignore_case {
                format!("(?i){}", pattern)
            } else {
                pattern.clone()
            };
            Some(Regex::new(&regex_pattern)?)
        } else {
            None
        };

        Ok(SearchEngine {
            pattern,
            ignore_case,
            use_regex,
            compiled_regex,
        })
    }

    pub fn matches(&self, line: &str) -> bool {
        if let Some(ref regex) = self.compiled_regex {
            regex.is_match(line)
        } else if self.ignore_case {
            line.to_lowercase().contains(&self.pattern.to_lowercase())
        } else {
            line.contains(&self.pattern)
        }
    }
}

建立檔案搜尋功能

use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::Path;
use colored::*;

pub struct SearchResult {
    pub file_path: String,
    pub line_number: usize,
    pub line_content: String,
}

pub fn search_file(
    file_path: &Path,
    engine: &SearchEngine,
    show_line_numbers: bool,
) -> Result<Vec<SearchResult>, Box<dyn Error>> {
    let file = File::open(file_path)?;
    let reader = BufReader::new(file);
    let mut results = Vec::new();

    for (line_num, line) in reader.lines().enumerate() {
        let line = line?;
        if engine.matches(&line) {
            results.push(SearchResult {
                file_path: file_path.to_string_lossy().to_string(),
                line_number: line_num + 1,
                line_content: line,
            });
        }
    }

    Ok(results)
}

pub fn display_results(
    results: &[SearchResult],
    pattern: &str,
    show_line_numbers: bool,
    files_with_matches: bool,
) {
    if files_with_matches {
        let mut displayed_files = std::collections::HashSet::new();
        for result in results {
            if displayed_files.insert(&result.file_path) {
                println!("{}", result.file_path.green());
            }
        }
        return;
    }

    for result in results {
        let highlighted_line = highlight_matches(&result.line_content, pattern);
        
        if show_line_numbers {
            println!(
                "{}:{}:{}",
                result.file_path.green(),
                result.line_number.to_string().yellow(),
                highlighted_line
            );
        } else {
            println!("{}:{}", result.file_path.green(), highlighted_line);
        }
    }
}

fn highlight_matches(line: &str, pattern: &str) -> String {
    line.replace(pattern, &pattern.red().bold().to_string())
}

這時候考慮 recursive 搜尋

使用 walkdir 實現

use walkdir::WalkDir;

pub fn search_directory(
    dir_path: &Path,
    engine: &SearchEngine,
    show_line_numbers: bool,
) -> Result<Vec<SearchResult>, Box<dyn Error>> {
    let mut all_results = Vec::new();

    for entry in WalkDir::new(dir_path) {
        let entry = entry?;
        let path = entry.path();

        if path.is_file() {
            // 過濾掉二進位檔案(簡單檢查)
            if let Some(extension) = path.extension() {
                let ext_str = extension.to_string_lossy().to_lowercase();
                if matches!(ext_str.as_str(), "exe" | "bin" | "obj" | "dll" | "so") {
                    continue;
                }
            }

            match search_file(path, engine, show_line_numbers) {
                Ok(mut results) => all_results.append(&mut results),
                Err(e) => eprintln!("警告:無法搜尋檔案 {}: {}", path.display(), e),
            }
        }
    }

    Ok(all_results)
}

整合

use clap::Parser;
use std::path::Path;
use std::process;

mod search;
use search::*;

fn main() {
    let args = Args::parse();

    // 建立搜尋引擎
    let engine = match SearchEngine::new(
        args.pattern.clone(),
        args.ignore_case,
        args.regex,
    ) {
        Ok(engine) => engine,
        Err(e) => {
            eprintln!("錯誤:無法建立搜尋引擎: {}", e);
            process::exit(1);
        }
    };

    let path = Path::new(&args.file);
    let results = if args.recursive {
        if !path.is_dir() {
            eprintln!("錯誤:遞迴模式需要提供目錄路徑");
            process::exit(1);
        }
        search_directory(path, &engine, args.line_number)
    } else {
        if !path.is_file() {
            eprintln!("錯誤:指定的路徑不是有效的檔案");
            process::exit(1);
        }
        search_file(path, &engine, args.line_number)
    };

    match results {
        Ok(results) => {
            if results.is_empty() {
                println!("沒有找到匹配的結果");
                process::exit(1);
            } else {
                display_results(&results, &args.pattern, args.line_number, args.files_with_matches);
                println!("\n找到 {} 個匹配結果", results.len());
            }
        }
        Err(e) => {
            eprintln!("搜尋過程中發生錯誤: {}", e);
            process::exit(1);
        }
    }
}

編譯並測試我們的工具:

cargo build --release

# 基本搜尋
./target/release/rusty-grep "function" src/main.rs

# 忽略大小寫搜尋
./target/release/rusty-grep -i "FUNCTION" src/main.rs

# 顯示行號
./target/release/rusty-grep -n "function" src/main.rs

# 使用正則表達式
./target/release/rusty-grep -r "fn \w+\(" src/

# 遞迴搜尋目錄
./target/release/rusty-grep -R "TODO" ./src/

# 只顯示包含匹配的檔案名稱
./target/release/rusty-grep -l "error" ./src/

快速總結

今天我們成功實作了一個功能豐富的檔案搜尋工具,涵蓋了以下 Rust 重要概念:

命令列參數解析:使用 clap crate 建立使用者友善的 CLI
檔案 I/O:安全高效地讀取檔案內容
正則表達式:使用 regex crate 進行模式匹配
錯誤處理:適當的錯誤傳播和處理
模組化設計:將功能分解為可重用的組件


上一篇
文字計數器 - 統計檔案中的字數、行數、字元數
系列文
Rust 實戰專案集:30 個漸進式專案從工具到服務3
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言