Rust 入门(四):实战项目 - 构建命令行工具

January, 10th 2026 8 min read Markdown
Rust 入门(四):实战项目 - 构建命令行工具

Rust 入门系列(第 4 篇,共 5 篇) 前置阅读第 3 篇 - 所有权、借用与生命周期 阅读时间:20 分钟


引言

前三篇学习了 Rust 的基础知识,这一篇我们来做一个实战项目:构建一个命令行文件搜索工具。

这个项目会用到:

  • ✅ Clap:解析命令行参数
  • ✅ 文件 I/O:读取文件内容
  • ✅ 错误处理:Result 和 ? 操作符
  • ✅ 迭代器和闭包

一、项目介绍

1.1 功能需求

构建一个类似 grep 的工具 minigrep

bash
1
cargo run -- query filename.txt

功能

  1. 在文件中搜索指定字符串
  2. 显示包含该字符串的所有行
  3. 支持大小写不敏感搜索

1.2 创建项目

bash
12
cargo new minigrep
cd minigrep

二、基础版本

2.1 读取命令行参数

rust
1234567
// src/main.rs
use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();
    println!("{:?}", args);
}

运行

bash
12
$ cargo run -- query filename.txt
["target/debug/minigrep", "query", "filename.txt"]

2.2 提取参数

rust
123456789
fn main() {
    let args: Vec<String> = env::args().collect();

    let query = &args[1];
    let file_path = &args[2];

    println!("搜索: {}", query);
    println!("文件: {}", file_path);
}

2.3 读取文件

rust
12345678910111213141516
use std::env;
use std::fs;

fn main() {
    let args: Vec<String> = env::args().collect();

    let query = &args[1];
    let file_path = &args[2];

    println!("搜索: {}", query);

    let contents = fs::read_to_string(file_path)
        .expect("无法读取文件");

    println!("文件内容:\n{}", contents);
}

测试: 创建 poem.txt

plaintext
1234
I'm nobody! Who are you?
Are you nobody, too?
Then there's a pair of us - don't tell!
They'd banish us, you know.

运行:

bash
1
$ cargo run -- nobody poem.txt

三、重构:提高可维护性

3.1 提取配置

rust
12345678910111213141516171819202122232425
struct Config {
    query: String,
    file_path: String,
}

impl Config {
    fn new(args: &[String]) -> Config {
        let query = args[1].clone();
        let file_path = args[2].clone();

        Config { query, file_path }
    }
}

fn main() {
    let args: Vec<String> = env::args().collect();
    let config = Config::new(&args);

    println!("搜索: {}", config.query);

    let contents = fs::read_to_string(config.file_path)
        .expect("无法读取文件");

    println!("文件内容:\n{}", contents);
}

3.2 错误处理

rust
1234567891011121314151617181920212223
impl Config {
    fn new(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("参数不足");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        Ok(Config { query, file_path })
    }
}

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::new(&args).unwrap_or_else(|err| {
        println!("参数解析错误: {}", err);
        std::process::exit(1);
    });

    // ...
}

3.3 提取核心逻辑

rust
1234567891011121314151617181920212223242526272829303132333435
fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::new(&args).unwrap_or_else(|err| {
        println!("参数解析错误: {}", err);
        std::process::exit(1);
    });

    if let Err(e) = run(config) {
        println!("应用错误: {}", e);
        std::process::exit(1);
    }
}

fn run(config: Config) -> Result<(), Box<dyn std::error::Error>> {
    let contents = fs::read_to_string(config.file_path)?;

    for line in search(&config.query, &contents) {
        println!("{}", line);
    }

    Ok(())
}

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
}

运行

bash
123
$ cargo run -- nobody poem.txt
I'm nobody! Who are you?
Are you nobody, too?

四、使用 Clap 改进

4.1 添加依赖

Clap 官方教程

toml
123
# Cargo.toml
[dependencies]
clap = { version = "4.4", features = ["derive"] }

4.2 使用 Clap

rust
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657
use clap::Parser;
use std::fs;

#[derive(Parser, Debug)]
#[command(name = "minigrep")]
#[command(about = "A simple grep clone in Rust", long_about = None)]
struct Args {
    /// 搜索的字符串
    query: String,

    /// 文件路径
    file_path: String,

    /// 大小写不敏感搜索
    #[arg(short, long)]
    ignore_case: bool,
}

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

    if let Err(e) = run(args) {
        eprintln!("错误: {}", e);
        std::process::exit(1);
    }
}

fn run(args: Args) -> Result<(), Box<dyn std::error::Error>> {
    let contents = fs::read_to_string(&args.file_path)?;

    let results = if args.ignore_case {
        search_case_insensitive(&args.query, &contents)
    } else {
        search(&args.query, &contents)
    };

    for line in results {
        println!("{}", line);
    }

    Ok(())
}

fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    contents
        .lines()
        .filter(|line| line.contains(query))
        .collect()
}

fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let query = query.to_lowercase();
    contents
        .lines()
        .filter(|line| line.to_lowercase().contains(&query))
        .collect()
}

使用示例

bash
12345678
# 普通搜索
$ cargo run -- nobody poem.txt

# 大小写不敏感
$ cargo run -- NOBODY poem.txt -i

# 查看帮助
$ cargo run -- --help

五、测试

5.1 添加测试

rust
12345678910111213141516171819202122232425262728293031
#[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)
        );
    }
}

运行测试

bash
1
cargo test

六、完整代码

6.1 main.rs

rust
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980
use clap::Parser;
use std::fs;
use std::process;

#[derive(Parser, Debug)]
#[command(name = "minigrep")]
#[command(about = "A simple grep clone in Rust")]
struct Args {
    /// 搜索的字符串
    query: String,

    /// 文件路径
    file_path: String,

    /// 大小写不敏感搜索
    #[arg(short, long)]
    ignore_case: bool,
}

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

    if let Err(e) = run(args) {
        eprintln!("应用错误: {}", e);
        process::exit(1);
    }
}

fn run(args: Args) -> Result<(), Box<dyn std::error::Error>> {
    let contents = fs::read_to_string(&args.file_path)?;

    let results = if args.ignore_case {
        search_case_insensitive(&args.query, &contents)
    } else {
        search(&args.query, &contents)
    };

    for line in results {
        println!("{}", line);
    }

    Ok(())
}

fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    contents
        .lines()
        .filter(|line| line.contains(query))
        .collect()
}

fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let query = query.to_lowercase();
    contents
        .lines()
        .filter(|line| line.to_lowercase().contains(&query))
        .collect()
}

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

    #[test]
    fn case_sensitive() {
        let query = "duct";
        let contents = "Rust:\nsafe, fast, productive.\nPick three.\nDuct tape.";
        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }

    #[test]
    fn case_insensitive() {
        let query = "rUsT";
        let contents = "Rust:\nsafe, fast, productive.\nPick three.\nTrust me.";
        assert_eq!(
            vec!["Rust:", "Trust me."],
            search_case_insensitive(query, contents)
        );
    }
}

七、扩展功能

7.1 添加行号

rust
12345678
fn search_with_line_numbers<'a>(query: &str, contents: &'a str) -> Vec<(usize, &'a str)> {
    contents
        .lines()
        .enumerate()
        .filter(|(_, line)| line.contains(query))
        .map(|(i, line)| (i + 1, line))
        .collect()
}

7.2 正则表达式支持

toml
123
[dependencies]
clap = { version = "4.4", features = ["derive"] }
regex = "1.10"
rust
123456789
use regex::Regex;

fn search_regex<'a>(pattern: &str, contents: &'a str) -> Vec<&'a str> {
    let re = Regex::new(pattern).expect("无效的正则表达式");
    contents
        .lines()
        .filter(|line| re.is_match(line))
        .collect()
}

7.3 多文件搜索

rust
123456789101112131415161718192021222324
use std::fs;
use std::path::PathBuf;

#[derive(Parser)]
struct Args {
    query: String,

    /// 文件路径(可以多个)
    #[arg(required = true)]
    files: Vec<PathBuf>,
}

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

    for file_path in &args.files {
        println!("\n文件: {:?}", file_path);
        let contents = fs::read_to_string(file_path).expect("读取失败");
        let results = search(&args.query, &contents);
        for line in results {
            println!("{}", line);
        }
    }
}

八、学到了什么?

8.1 核心技能

  1. 命令行参数解析:使用 Clap
  2. 文件 I/Ofs::read_to_string
  3. 错误处理Result? 操作符
  4. 迭代器lines(), filter(), collect()
  5. 生命周期:函数返回引用
  6. 测试:单元测试

8.2 Rust 最佳实践

  • ✅ 早期失败:参数错误立即退出
  • ✅ 错误传播:使用 ? 简化代码
  • ✅ 迭代器优先:避免手动循环
  • ✅ 测试覆盖:核心逻辑都有测试

九、下一步

9.1 练习建议

  1. 添加更多选项(上下文行数、高亮显示等)
  2. 支持递归目录搜索
  3. 添加性能基准测试

9.2 推荐资源


系列总结

已完成

  1. Rust 是什么?能做什么?
  2. 基础语法与核心概念
  3. 所有权、借用与生命周期
  4. 实战项目 - CLI 工具

下一篇(最后一篇):《Rust 入门(五):Rust + WebAssembly》


参考资料


系列导航