🦀 "如果你能用 Rust 写 CLI,你就能用 Rust 做任何事。"
------某位在 segfault 里挣扎过太久的 C++ 程序员
目录
- [为什么选 Rust 写 CLI?](#为什么选 Rust 写 CLI? "#why")
- 我们要做什么?
- 环境搭建
- 项目初始化与结构设计
- 第一步:解析命令行参数(clap)
- 第二步:文件遍历与内容搜索
- 第三步:彩色输出与用户体验
- 第四步:优雅的错误处理
- 第五步:性能优化小技巧
- 测试:不测试的代码叫"薛定谔的正确"
- 打包与发布
- 常见误区与避坑指南
- 总结
1. 为什么选 Rust 写 CLI?
在回答这个问题之前,先问你一个灵魂拷问:你有没有用过一个 CLI 工具,它慢得像一只在 64GB 内存的服务器上用 Python 写的爬虫?
Rust 的三大核心优势
| 特性 | 说人话 |
|---|---|
| 零成本抽象 | 写起来像高级语言,跑起来像 C |
| 内存安全 | 不需要 GC,也不会 segfault(编译器会盯着你) |
| 并发安全 | 数据竞争?Rust 编译器:我不允许 |
为什么 CLI 是最好的入门场景?
- 输入输出简单:stdin/stdout,不需要 GUI 框架
- 快速看到成果:写完就能在终端跑
- 生态成熟 :
clap、colored、anyhow......开箱即用 - 真实世界应用 :
ripgrep、fd、bat、exa都是 Rust 写的
📌 小知识 :
ripgrep(rg)比 GNU grep 快 2-10 倍,作者 Andrew Gallant 就是用 Rust 写的。所以你以后可以骄傲地说:"我在学和 rg 同一门语言。"
2. 我们要做什么?
我们将从零开始构建一个叫 crabgrep 的文件搜索工具(致敬🦀),它支持:
- ✅ 在文件或目录中搜索关键词
- ✅ 支持递归搜索子目录
- ✅ 彩色高亮匹配内容
- ✅ 显示匹配行号
- ✅ 大小写不敏感选项
- ✅ 优雅的错误提示
- ✅ 跨平台(Linux / macOS / Windows)
最终效果长这样:
css
$ crabgrep "hello" ./src --ignore-case
./src/main.rs:3: fn say_hello() {
./src/main.rs:4: println!("Hello, world!"); ← 这里高亮显示
./src/lib.rs:12: // hello from lib
3. 环境搭建
安装 Rust
bash
# 官方推荐方式(一行搞定)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# 验证安装
rustc --version # rustc 1.78.0 (...)
cargo --version # cargo 1.78.0 (...)
💡 Tip:如果你在国内,可以配置镜像加速:
bash# 在 ~/.cargo/config.toml 中添加 [source.crates-io] replace-with = 'ustc' [source.ustc] registry = "sparse+https://mirrors.ustc.edu.cn/crates.io-index/"
推荐的 IDE 配置
- VS Code +
rust-analyzer插件(强烈推荐) - RustRover(JetBrains 出品,开箱即用)
rust-analyzer 会帮你:自动补全、内联类型提示、实时报错------基本上就是一个随时在旁边的 Rust 老师,只是不会嘲笑你。
4. 项目初始化与结构设计
bash
cargo new crabgrep
cd crabgrep
Cargo 自动生成的结构:
bash
crabgrep/
├── Cargo.toml # 项目配置 & 依赖声明
└── src/
└── main.rs # 程序入口
我们最终的项目结构会是这样:
csharp
crabgrep/
├── Cargo.toml
├── Cargo.lock # 锁定依赖版本,要提交到 git!
└── src/
├── main.rs # 入口:解析参数,调度逻辑
├── lib.rs # 核心逻辑(便于测试)
├── search.rs # 搜索逻辑
└── output.rs # 输出格式化
🏗️ 最佳实践 :把业务逻辑放在
lib.rs里,main.rs只负责"胶水代码"(解析参数、调用函数、处理顶层错误)。这样测试方便,结构清晰,你三个月后回来看还能认出来这是自己写的。
5. 第一步:解析命令行参数(clap)
为什么用 clap?
手动解析 std::env::args() 是可以的,但就像用石头磨面粉------能做到,但没必要。
clap 是 Rust 最流行的 CLI 参数解析库,支持:
- 自动生成
--help和--version - 子命令(subcommands)
- 类型安全的参数绑定
- Shell 自动补全生成
添加依赖
编辑 Cargo.toml:
toml
[package]
name = "crabgrep"
version = "0.1.0"
edition = "2021"
[dependencies]
clap = { version = "4", features = ["derive"] }
colored = "2"
anyhow = "1"
walkdir = "2"
依赖说明:
| 库 | 用途 |
|---|---|
clap |
命令行参数解析 |
colored |
终端彩色输出 |
anyhow |
简化错误处理 |
walkdir |
递归遍历目录 |
定义 CLI 结构
src/main.rs:
rust
use clap::Parser;
/// 🦀 crabgrep - 一个用 Rust 写的文件搜索工具
#[derive(Parser, Debug)]
#[command(
name = "crabgrep",
version = "0.1.0",
author = "你的名字 <you@example.com>",
about = "在文件中搜索关键词,比你手快,比你准"
)]
struct Cli {
/// 要搜索的关键词
pattern: String,
/// 搜索的目标路径(文件或目录)
#[arg(default_value = ".")]
path: std::path::PathBuf,
/// 忽略大小写
#[arg(short = 'i', long = "ignore-case")]
ignore_case: bool,
/// 递归搜索子目录
#[arg(short = 'r', long = "recursive", default_value_t = true)]
recursive: bool,
/// 显示匹配行的上下文行数
#[arg(short = 'C', long = "context", default_value_t = 0)]
context: usize,
}
fn main() {
let cli = Cli::parse();
println!("{:?}", cli); // 先打印出来看看
}
运行一下:
bash
cargo run -- "hello" ./src --ignore-case
# 输出: Cli { pattern: "hello", path: "./src", ignore_case: true, recursive: true, context: 0 }
cargo run -- --help
# 自动生成完整的帮助文档!
🎉 神奇的地方 :
#[derive(Parser)]宏帮你把结构体变成了完整的 CLI 接口。这就是 Rust "零成本抽象"的魅力------你写声明,编译器写实现。
6. 第二步:文件遍历与内容搜索
创建搜索模块
src/search.rs:
rust
use std::path::Path;
use anyhow::{Context, Result};
use walkdir::WalkDir;
/// 单条搜索结果
#[derive(Debug)]
pub struct SearchResult {
pub file_path: String,
pub line_number: usize,
pub line_content: String,
pub match_start: usize, // 匹配开始位置(用于高亮)
pub match_end: usize, // 匹配结束位置
}
/// 搜索配置
pub struct SearchConfig {
pub pattern: String,
pub ignore_case: bool,
pub recursive: bool,
}
/// 在单个文件中搜索
pub fn search_in_file(
path: &Path,
config: &SearchConfig,
) -> Result<Vec<SearchResult>> {
// 读取文件内容,遇到错误时附带上下文信息
let content = std::fs::read_to_string(path)
.with_context(|| format!("无法读取文件: {}", path.display()))?;
let pattern = if config.ignore_case {
config.pattern.to_lowercase()
} else {
config.pattern.clone()
};
let mut results = Vec::new();
for (line_idx, line) in content.lines().enumerate() {
let search_line = if config.ignore_case {
line.to_lowercase()
} else {
line.to_string()
};
// 找到所有匹配位置
if let Some(match_start) = search_line.find(&pattern) {
results.push(SearchResult {
file_path: path.display().to_string(),
line_number: line_idx + 1, // 行号从 1 开始
line_content: line.to_string(),
match_start,
match_end: match_start + pattern.len(),
});
}
}
Ok(results)
}
/// 在路径(文件或目录)中搜索
pub fn search(
path: &Path,
config: &SearchConfig,
) -> Result<Vec<SearchResult>> {
let mut all_results = Vec::new();
if path.is_file() {
// 直接搜索单个文件
let results = search_in_file(path, config)?;
all_results.extend(results);
} else if path.is_dir() {
// 遍历目录
let walker = if config.recursive {
WalkDir::new(path)
} else {
WalkDir::new(path).max_depth(1)
};
for entry in walker.into_iter().filter_map(|e| e.ok()) {
let entry_path = entry.path();
// 只处理文件,跳过目录和隐藏文件
if entry_path.is_file() && !is_hidden(entry_path) {
// 跳过二进制文件
if is_likely_binary(entry_path) {
continue;
}
match search_in_file(entry_path, config) {
Ok(results) => all_results.extend(results),
Err(e) => {
// 单个文件失败不影响整体,打印警告继续
eprintln!("警告: {}", e);
}
}
}
}
}
Ok(all_results)
}
/// 判断是否是隐藏文件(以 . 开头)
fn is_hidden(path: &Path) -> bool {
path.file_name()
.and_then(|name| name.to_str())
.map(|name| name.starts_with('.'))
.unwrap_or(false)
}
/// 简单判断是否可能是二进制文件
fn is_likely_binary(path: &Path) -> bool {
let binary_extensions = [
"png", "jpg", "jpeg", "gif", "bmp", "ico", "svg",
"pdf", "zip", "tar", "gz", "rar", "7z",
"exe", "dll", "so", "dylib", "bin",
"mp3", "mp4", "avi", "mov", "flv",
"woff", "woff2", "ttf", "eot",
];
path.extension()
.and_then(|ext| ext.to_str())
.map(|ext| binary_extensions.contains(&ext.to_lowercase().as_str()))
.unwrap_or(false)
}
⚠️ 避坑! 注意这里对文件读取失败的处理:
- 遍历目录时 :单个文件失败只打印警告,不中断整个搜索(用
match+eprintln!)- 直接搜索文件时 :用
?传播错误给调用者两种策略都是对的,取决于你的业务语义。很多初学者在这里一刀切,要么全报错,要么全吞掉。
7. 第三步:彩色输出与用户体验
没有颜色的 CLI 就像黑白电视------能用,但是没灵魂。
创建输出模块
src/output.rs:
rust
use colored::*;
use crate::search::SearchResult;
/// 打印搜索结果
pub fn print_results(results: &[SearchResult], pattern: &str, ignore_case: bool) {
if results.is_empty() {
println!("{}", "没有找到匹配内容。".yellow());
return;
}
let mut current_file = String::new();
for result in results {
// 文件名发生变化时,打印新的文件标题
if result.file_path != current_file {
current_file = result.file_path.clone();
println!("\n{}", current_file.cyan().bold().underline());
}
// 打印行号
print!(" {}{} ", result.line_number.to_string().green(), ":".green());
// 打印行内容,高亮匹配部分
print_highlighted_line(
&result.line_content,
result.match_start,
result.match_end,
);
}
// 打印统计摘要
println!("\n{}", "─".repeat(50).dimmed());
println!(
"共找到 {} 处匹配,涉及 {} 个文件",
results.len().to_string().yellow().bold(),
count_unique_files(results).to_string().yellow().bold()
);
}
/// 在一行中高亮显示匹配的部分
fn print_highlighted_line(line: &str, match_start: usize, match_end: usize) {
// 注意:这里的 start/end 是字节索引,处理 Unicode 要小心
let before = &line[..match_start];
let matched = &line[match_start..match_end];
let after = &line[match_end..];
print!("{}", before);
print!("{}", matched.red().bold()); // 匹配部分红色加粗
println!("{}", after);
}
/// 统计涉及的文件数量
fn count_unique_files(results: &[SearchResult]) -> usize {
let mut files = std::collections::HashSet::new();
for r in results {
files.insert(&r.file_path);
}
files.len()
}
⚠️ Unicode 陷阱 !上面的
&line[..match_start]使用的是字节索引,如果字符串中有中文等多字节字符,直接切片可能 panic!安全的做法是用字符索引:
rust// 危险:直接字节切片 let before = &line[..match_start]; // 可能 panic! // 安全:先找字符边界 // 或者使用 str::is_char_boundary() 检查 assert!(line.is_char_boundary(match_start));在生产代码中,推荐用
regex库处理搜索,它原生支持 Unicode。
8. 第四步:优雅的错误处理
错误处理的三个层次
rust
Panic(不该发生的 bug)
└── Result(可恢复的错误)
└── Option(值可能不存在)
用 anyhow 简化错误传播
rust
// ❌ 初学者常见写法:.unwrap() 满天飞
let content = std::fs::read_to_string(path).unwrap(); // 文件不存在 = 程序崩溃
// ❌ 略好一点:.expect() 至少有提示
let content = std::fs::read_to_string(path).expect("读取文件失败");
// ✅ 正确做法:用 ? 传播错误
fn read_content(path: &Path) -> Result<String> {
let content = std::fs::read_to_string(path)
.with_context(|| format!("无法读取文件: {}", path.display()))?;
Ok(content)
}
完整的 main.rs
现在把所有模块组合起来:
rust
mod search;
mod output;
use clap::Parser;
use anyhow::Result;
use std::process;
#[derive(Parser, Debug)]
#[command(
name = "crabgrep",
version,
about = "🦀 在文件中搜索关键词,比你手快,比你准"
)]
struct Cli {
/// 要搜索的关键词
pattern: String,
/// 搜索的目标路径(文件或目录)
#[arg(default_value = ".")]
path: std::path::PathBuf,
/// 忽略大小写
#[arg(short = 'i', long = "ignore-case")]
ignore_case: bool,
/// 递归搜索子目录
#[arg(short = 'r', long = "recursive", default_value_t = true)]
recursive: bool,
}
fn main() {
// 把真正的逻辑放到 run() 里,main 只负责处理顶层错误
if let Err(e) = run() {
eprintln!("{} {}", "错误:".red().bold(), e);
// 打印错误链(anyhow 的 context 会形成错误链)
for cause in e.chain().skip(1) {
eprintln!(" 原因: {}", cause);
}
process::exit(1);
}
}
fn run() -> Result<()> {
let cli = Cli::parse();
// 验证路径存在
if !cli.path.exists() {
anyhow::bail!("路径不存在: {}", cli.path.display());
}
let config = search::SearchConfig {
pattern: cli.pattern.clone(),
ignore_case: cli.ignore_case,
recursive: cli.recursive,
};
println!(
"🔍 在 {} 中搜索 \"{}\"{}...",
cli.path.display(),
cli.pattern,
if cli.ignore_case { "(忽略大小写)" } else { "" }
);
let results = search::search(&cli.path, &config)?;
output::print_results(&results, &cli.pattern, cli.ignore_case);
Ok(())
}
💡 最佳实践 :
main()函数应该尽量简短。把业务逻辑抽到run() -> Result<()>里,让main()只处理错误展示和退出码。这个模式在 Rust CLI 项目中极为常见。
错误处理速查表
| 场景 | 推荐做法 |
|-------------|---------------------------|---|-----------------|
| 快速原型 / 测试代码 | .unwrap() 可以接受 |
| 生产代码,期望不会失败 | .expect("这里不该失败,因为...") |
| 向上传播错误 | ? 运算符 |
| 需要附加上下文 | `.with_context( | | format!(...))` |
| 直接创建错误 | anyhow::bail!("错误信息") |
| 自定义错误类型 | 使用 thiserror 库 |
9. 第五步:性能优化小技巧
Rust 默认就很快,但有些地方还是值得注意。
字符串:避免不必要的 clone
rust
// ❌ 每次循环都分配新字符串
for line in content.lines() {
let lower = line.to_lowercase(); // 不得不 clone
if lower.contains(&pattern) { ... }
}
// ✅ 只在需要时才转换
let lower_pattern = if ignore_case {
pattern.to_lowercase()
} else {
pattern.to_string()
};
for line in content.lines() {
let search_target = if ignore_case {
std::borrow::Cow::Owned(line.to_lowercase())
} else {
std::borrow::Cow::Borrowed(line) // 不分配!
};
if search_target.contains(&lower_pattern) { ... }
}
Cow<'_, str>(Clone-on-Write)是个好东西:需要时才克隆,不需要时借用。
大文件:逐行读取而非一次性读入
rust
// ❌ 小文件没问题,几 GB 的文件直接爆内存
let content = fs::read_to_string(path)?;
// ✅ 逐行读取
use std::io::{BufRead, BufReader};
use std::fs::File;
let file = File::open(path)?;
let reader = BufReader::new(file);
for (idx, line) in reader.lines().enumerate() {
let line = line?;
// 处理每一行...
}
并行搜索:用 rayon
当需要搜索大量文件时,可以利用多核:
rust
// 添加依赖: rayon = "1"
use rayon::prelude::*;
// 把 .iter() 换成 .par_iter(),魔法就发生了
let results: Vec<_> = files
.par_iter()
.flat_map(|file| search_in_file(file, &config).unwrap_or_default())
.collect();
🚀
rayon的设计哲学:把串行迭代器变成并行迭代器,只需要把.iter()改成.par_iter()。这就是零成本抽象的极致体现。
10. 测试:不测试的代码叫"薛定谔的正确"
单元测试
在 src/search.rs 底部添加:
rust
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile; // 添加依赖: tempfile = "3"
fn create_temp_file(content: &str) -> NamedTempFile {
let mut file = NamedTempFile::new().expect("创建临时文件失败");
file.write_all(content.as_bytes()).expect("写入失败");
file
}
#[test]
fn test_search_basic() {
let file = create_temp_file("hello world\nrust is awesome\nhello again");
let config = SearchConfig {
pattern: "hello".to_string(),
ignore_case: false,
recursive: false,
};
let results = search_in_file(file.path(), &config).unwrap();
assert_eq!(results.len(), 2);
assert_eq!(results[0].line_number, 1);
assert_eq!(results[1].line_number, 3);
}
#[test]
fn test_search_ignore_case() {
let file = create_temp_file("Hello World\nhello world\nHELLO WORLD");
let config = SearchConfig {
pattern: "hello".to_string(),
ignore_case: true,
recursive: false,
};
let results = search_in_file(file.path(), &config).unwrap();
assert_eq!(results.len(), 3); // 三行都匹配
}
#[test]
fn test_search_no_match() {
let file = create_temp_file("rust is great\nno match here");
let config = SearchConfig {
pattern: "python".to_string(),
ignore_case: false,
recursive: false,
};
let results = search_in_file(file.path(), &config).unwrap();
assert!(results.is_empty());
}
#[test]
fn test_file_not_found() {
let config = SearchConfig {
pattern: "test".to_string(),
ignore_case: false,
recursive: false,
};
let result = search_in_file(std::path::Path::new("/不存在的路径/file.txt"), &config);
assert!(result.is_err());
}
}
运行测试:
bash
cargo test
# 或者运行特定测试
cargo test test_search_ignore_case
# 看测试输出
cargo test -- --nocapture
集成测试
在项目根目录创建 tests/ 目录:
rust
// tests/integration_test.rs
use std::process::Command;
#[test]
fn test_cli_basic_usage() {
let output = Command::new(env!("CARGO_BIN_EXE_crabgrep"))
.args(["hello", "tests/fixtures/sample.txt"])
.output()
.expect("执行失败");
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains("hello"));
}
#[test]
fn test_cli_no_match_exit_code() {
let output = Command::new(env!("CARGO_BIN_EXE_crabgrep"))
.args(["不存在的内容xyz123", "tests/fixtures/sample.txt"])
.output()
.expect("执行失败");
// 没有找到匹配时,grep 惯例返回退出码 1
// 你可以根据自己的设计决定
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains("没有找到"));
}
🧪 测试金字塔原则:
- 单元测试:测试每个函数的逻辑,快速、隔离
- 集成测试:测试模块间的协作
- 端对端测试:测试实际的 CLI 行为
三者都要有,但单元测试数量最多。
11. 打包与发布
优化构建产物
默认的 cargo build --release 已经很好了,但可以进一步优化:
toml
# Cargo.toml
[profile.release]
opt-level = 3 # 最高优化级别
lto = true # 链接时优化(减小体积)
codegen-units = 1 # 更好的优化(慢一点)
strip = true # 去除调试符号(更小的二进制)
panic = "abort" # panic 时直接退出(更小的二进制)
bash
cargo build --release
# 产物在 target/release/crabgrep
ls -lh target/release/crabgrep
交叉编译(在 Mac 上编译 Linux 版本)
bash
# 安装目标平台
rustup target add x86_64-unknown-linux-musl
# 构建
cargo build --release --target x86_64-unknown-linux-musl
发布到 crates.io
bash
# 首先注册 crates.io 账号,然后:
cargo login
cargo publish
发布前记得在 Cargo.toml 填写完整信息:
toml
[package]
name = "crabgrep"
version = "0.1.0"
edition = "2021"
description = "一个用 Rust 写的文件搜索工具"
license = "MIT"
repository = "https://github.com/你的名字/crabgrep"
keywords = ["cli", "grep", "search"]
categories = ["command-line-utilities"]
用 GitHub Actions 自动发布
yaml
# .github/workflows/release.yml
name: Release
on:
push:
tags:
- 'v*'
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- run: cargo build --release
- uses: actions/upload-artifact@v4
with:
name: crabgrep-${{ matrix.os }}
path: target/release/crabgrep*
12. 常见误区与避坑指南
误区一:.clone() 是万能胶
rust
// ❌ 看到编译错误就 clone,结果到处都是 clone
fn process(data: String) -> String {
let copy1 = data.clone();
let copy2 = data.clone();
// ...
}
// ✅ 先思考:我真的需要所有权吗?还是借用就够了?
fn process(data: &str) -> String {
// 只有在真正需要新字符串时才 clone
format!("processed: {}", data)
}
经验法则 :看到 .clone(),先问自己:为什么要 clone?能用引用吗?
误区二:滥用 .unwrap()
rust
// ❌ 这是在赌 CPU 不会让你的文件突然消失
let content = fs::read_to_string("config.toml").unwrap();
// ✅ 给用户一个体面的错误提示
let content = fs::read_to_string("config.toml")
.context("读取配置文件失败,请确认 config.toml 存在")?;
误区三:忽略 Clippy 的建议
bash
# Clippy 是 Rust 的 lint 工具,比你更懂 Rust
cargo clippy
# 甚至可以设置为有 warning 就失败(CI 推荐)
cargo clippy -- -D warnings
Clippy 会帮你发现:不必要的 clone、可以简化的 match 表达式、性能问题等。把它当成免费的代码审查员。
误区四:不知道什么时候用 String vs &str
rust
&str = 字符串的借用视图(像是指向内存的指针+长度)
String = 拥有所有权的字符串(堆分配)
函数参数接收字符串?用 &str(更通用)
函数需要返回创建的字符串?用 String
结构体字段存储字符串?通常用 String(除非有明确的生命周期)
误区五:让 main 函数承担太多责任
rust
// ❌ main 里写 200 行代码
fn main() {
// 解析参数
// 读取文件
// 处理逻辑
// 输出结果
// 错误处理
// ...200行...
}
// ✅ main 只负责调度和错误处理
fn main() {
if let Err(e) = run() {
eprintln!("错误: {e:#}");
std::process::exit(1);
}
}
fn run() -> anyhow::Result<()> {
// 清晰的主流程...
Ok(())
}
误区六:把 Rust 当 Python/Java 写
rust
// ❌ Java 思维:先建对象再设置属性
let mut config = Config::new();
config.set_pattern(pattern);
config.set_ignore_case(true);
// ✅ Rust 风格:构建器模式 或 直接结构体字面量
let config = Config {
pattern: pattern.to_string(),
ignore_case: true,
recursive: false,
};
13. 总结
恭喜你看到这里!🎉 让我们回顾一下这一路我们学到了什么:
知识地图
rust
CLI 开发
├── 参数解析
│ └── clap(derive 宏,声明式定义)
├── 核心逻辑
│ ├── 文件 I/O(std::fs, BufReader)
│ ├── 目录遍历(walkdir)
│ └── 字符串处理(str 方法,注意 Unicode)
├── 用户体验
│ └── 彩色输出(colored)
├── 错误处理
│ ├── anyhow(快速原型/应用)
│ ├── thiserror(库/可复用错误类型)
│ └── ? 运算符(错误传播)
├── 性能
│ ├── Cow<str>(避免不必要 clone)
│ ├── BufReader(大文件友好)
│ └── rayon(并行)
└── 工程实践
├── 模块化(lib.rs + mod)
├── 测试(单元 + 集成)
└── 发布(cargo publish + CI)
推荐的下一步
- 为 crabgrep 添加正则表达式支持 :引入
regex库替换str::find - 学习
thiserror:当你需要定义自己的错误类型时(写库必备) - 阅读
ripgrep源码:世界级 Rust CLI 项目,大量最佳实践 - 探索 TUI :用
ratatui库给你的工具加个交互界面 - 性能测试 :用
criterion库做基准测试,让优化有数据支撑
资源推荐
| 资源 | 链接 | 适合 |
|---|---|---|
| The Rust Book | doc.rust-lang.org/book/ | 系统学习 |
| Rust by Example | doc.rust-lang.org/rust-by-exa... | 示例学习 |
| Command Line Apps in Rust | rust-cli.github.io/book/ | CLI 专项 |
| Rustlings | github.com/rust-lang/r... | 练手题目 |
| clap 文档 | docs.rs/clap | 参数解析 |
| anyhow 文档 | docs.rs/anyhow | 错误处理 |
🦀 最后,记住 Rust 学习曲线的三个阶段:
- 困惑期:"为什么编译器不让我这样做?!"
- 顿悟期:"哦!原来借用检查器是在保护我!"
- 熟练期:"其他语言怎么没有这个?!"
大多数人卡在第一阶段就放弃了。坚持过去,你会发现 Rust 的编译器不是你的敌人------它是世界上最严格、最有耐心、最不会让你在生产环境背锅的队友。
Happy Hacking! 🚀
本文示例代码已在 Rust 1.78.0 下测试通过。如有任何问题,欢迎提 Issue。