---
title: Rust 入门（四）：实战项目 - 构建命令行工具
slug: rust-intro-cli-project
date: 2026-01-10
author: Frankie 徐
category: fullstack
tags: ['rust', 'cli', 'clap', 'project', 'tutorial']
description: Rust 入门系列第四篇：从零构建一个实用的命令行工具，学习 Clap 参数解析、文件 I/O、错误处理等实战技能
permalink: https://www.210k.cc/rust-intro-cli-project
---

> **Rust 入门系列**（第 4 篇，共 5 篇）
> **前置阅读**：[第 3 篇 - 所有权、借用与生命周期](/rust-intro-ownership)
> **阅读时间**：20 分钟

---

## 引言

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

这个项目会用到：
- ✅ Clap：解析命令行参数
- ✅ 文件 I/O：读取文件内容
- ✅ 错误处理：Result 和 ? 操作符
- ✅ 迭代器和闭包

---

## 一、项目介绍

### 1.1 功能需求

构建一个类似 `grep` 的工具 `minigrep`：

```bash
cargo run -- query filename.txt
```

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

### 1.2 创建项目

```bash
cargo new minigrep
cd minigrep
```

---

## 二、基础版本

### 2.1 读取命令行参数

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

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

**运行**：
```bash
$ cargo run -- query filename.txt
["target/debug/minigrep", "query", "filename.txt"]
```

### 2.2 提取参数

```rust
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
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`：
```
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
$ cargo run -- nobody poem.txt
```

---

## 三、重构：提高可维护性

### 3.1 提取配置

```rust
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
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
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
$ cargo run -- nobody poem.txt
I'm nobody! Who are you?
Are you nobody, too?
```

---

## 四、使用 Clap 改进

### 4.1 添加依赖

[Clap 官方教程](https://docs.rs/clap/latest/clap/_tutorial/index.html)：

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

### 4.2 使用 Clap

```rust
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
# 普通搜索
$ cargo run -- nobody poem.txt

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

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

---

## 五、测试

### 5.1 添加测试

```rust
#[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
cargo test
```

---

## 六、完整代码

### 6.1 main.rs

```rust
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
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
[dependencies]
clap = { version = "4.4", features = ["derive"] }
regex = "1.10"
```

```rust
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
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/O**：`fs::read_to_string`
3. **错误处理**：`Result` 和 `?` 操作符
4. **迭代器**：`lines()`, `filter()`, `collect()`
5. **生命周期**：函数返回引用
6. **测试**：单元测试

### 8.2 Rust 最佳实践

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

---

## 九、下一步

### 9.1 练习建议

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

### 9.2 推荐资源

- [Clap Documentation](https://docs.rs/clap/latest/clap/_tutorial/index.html)
- [Command Line Book](https://rust-cli.github.io/book/)
- [Building a CLI with Clap - Shuttle](https://www.shuttle.dev/blog/2023/12/08/clap-rust)

---

## 系列总结

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

**下一篇（最后一篇）**：《Rust 入门（五）：Rust + WebAssembly》

---

## 参考资料

- [Getting Started with Clap - DEV Community](https://dev.to/moseeh_52/getting-started-with-clap-a-beginners-guide-to-rust-cli-apps-1n3f)
- [Writing a CLI Tool with Clap - Shuttle](https://www.shuttle.dev/blog/2023/12/08/clap-rust)
- [Build a Rust CLI Tutorial - Codez Up](https://codezup.com/rust-clap-cli-tutorial/)
- [The Rust Programming Language Book - Chapter 12](https://doc.rust-lang.org/book/ch12-00-an-io-project.html)

---

## 系列导航

- **上一篇**：[Rust 入门（三）：所有权、借用与生命周期](/rust-intro-ownership)
- **下一篇**：[Rust 入门（五）：Rust + WebAssembly 实战](/rust-intro-wasm) 🦀