本文发表于入职啦(公众号: ruzhila) 大家可以访问入职啦学习更多的编程实战。
部分内容可能无法直接访问,如果有需要可以加我们的群交流 我们会定期在群里分享最新的项目实战代码,包括不同语言的实现
老师还会详细讲解代码优化的思路,扫码加入实战群:

项目地址
代码已经开源, devstrip 👏 Star
DevStrip 入门指南
DevStrip 是一个面向 macOS 的 Rust 命令行工具,帮助开发者清理久未使用的构建产物与缓存目录,从而快速回收磁盘空间。本文面向 Rust 初学者,通过 DevStrip 项目具体讲解如何构建一个实用的 CLI 应用,并给出后续扩展方向。
快速开始
- 获取源码或安装
cargo install devstrip # 从 crates.io 安装 cargo install --path . # 在仓库根目录执行,安装本地版本
- 查看帮助与运行
devstrip --help # 查看全部命令行参数 devstrip --dry-run # 先进行预演,确认删除内容 devstrip --yes # 跳过确认并直接清理
- 典型用法示例
这条命令会扫描两个额外目录、排除指定仓库、保留最近的 DerivedData 与缓存,并删除 5 天以上未使用的候选目录devstrip \ --roots ~/Projects/demo ~/Work/company \ --exclude ~/Projects/demo/ios-app \ --keep-latest-derived 2 \ --keep-latest-cache 3 \ --min-age-days 5 --dry-run
(如果去掉--dry-run 换成-y 才会真的清理)
下面执行效果:
核心功能拆解
1. 命令行参数解析
DevStrip 使用 clap 的派生宏快速构建 CLI 接口:
#[derive(Parser, Debug)]
struct Args {
#[arg(long = "roots", value_name = "PATH", num_args = 1..)]
roots: Vec<PathBuf>,
#[arg(short = 'x', long = "exclude", value_name = "PATH")]
excludes: Vec<PathBuf>,
#[arg(long = "min-age-days", default_value_t = 2)]
min_age_days: u64,
#[arg(long = "dry-run")]
dry_run: bool,
#[arg(short = 'y', long = "yes")]
yes: bool,
}
- Vec<PathBuf> 支持重复传入路径参数,方便一次指定多个根目录或排除目录。
- 默认值与别名通过属性一次定义,减少手写解析代码的负担。
2. 扫描配置构建
build_scan_config 负责展开 ~、合并默认目录、过滤排除项,并生成最终的扫描列表:
fn build_scan_config(args: &Args) -> Result<ScanConfig> {
let mut roots = expand_paths(&args.roots);
roots.extend(expand_paths(&args.positional_roots));
let exclude_inputs = expand_paths(&args.excludes);
let exclude_paths = normalize_paths(&exclude_inputs);
let resolved_roots = default_roots(&roots, &exclude_paths)?;
Ok(ScanConfig { roots: resolved_roots, exclude_paths, .. })
}
- expand_paths 将 ~/Projects 这类路径转换成绝对路径。
- default_roots 会自动加入当前目录以及 ~/Projects、~/workspace 等常见开发文件夹,降低用户操作成本。
3. 目录遍历与候选收集
collect_matching_dirs 使用广度优先遍历(BFS)扫描项目目录,匹配常见的构建与缓存文件夹:
let mut queue: VecDeque<(PathBuf, u32)> = VecDeque::new();
queue.push_back((root.clone(), 0));
while let Some((current, depth)) = queue.pop_front() {
if depth > max_depth || is_excluded(¤t, excludes) {
continue;
}
let entries = match fs::read_dir(¤t) {
Ok(iter) => iter,
Err(_) => continue,
};
for entry in entries.flatten() {
let path = entry.path();
if file_type.is_dir() && should_collect(&path, cutoff, &pattern_set) {
let size = calculate_size(&path);
candidates.push(Candidate { path, size_bytes: size, .. });
} else if depth < max_depth {
queue.push_back((path, depth + 1));
}
}
}
- 通过深度限制避免深入无关目录。
- should_collect 根据目录名与最后修改时间判断是否为目标缓存。
- calculate_size 会跳过符号链接、防止递归引用导致的死循环。
4. 扫描进度与交互体验
run_with_spinner 将耗时扫描放入后台线程,并通过 mpsc::channel 实时更新主线程的状态指示:
let (status_tx, status_rx) = mpsc::channel::<String>();
let (result_tx, result_rx) = mpsc::channel::<Result<T>>();
thread::spawn(move || {
let reporter = StatusReporter::channel(status_tx);
let outcome = func(reporter);
let _ = result_tx.send(outcome);
});
// 主线程轮询 status_rx 更新动画帧
- 终端动画仅在支持时启用,避免在非交互环境(如日志或 CI)中产生杂乱输出。
- StatusReporter 为扫描函数提供 update 接口,用于上报当前处理的目录。
5. 清理执行与结果汇总
cleanup_with_progress 遍历候选项、执行删除,并输出进度条与失败原因:
for (index, candidate) in candidates.iter().enumerate() {
if styler.supports_animation {
let bar = render_progress_bar(index + 1, total, 28);
print!("\rCleaning [{}] {}/{} {}", bar, index + 1, total, candidate.display_name());
}
let (success, error) = match delete_path(&candidate.path) {
Ok(_) => (true, None),
Err(err) => (false, Some(err.to_string())),
};
results.push(CleanupResult { candidate: candidate.clone(), success, error });
}
- 成功删除后统计总回收空间,失败项将逐一列出,便于后续人工介入。
- 在 --dry-run 模式下跳过删除动作,只展示潜在收益。
通过项目学习 Rust
- 掌握 clap、chrono、std::fs、std::thread 等常用 crate 与标准库组件的结合方式。
- 体验 Result<T, E> 与 match 模式匹配在错误处理中的实际应用。
- 学习如何封装终端输出(颜色、进度条)与跨线程通信(通道、共享状态)。
- 尝试在 --dry-run 模式下运行并阅读源码中的 println! 调试输出来理解流程。
后续扩展建议
- Windows 支持:使用 dirs crate 获取平台相关缓存目录,针对 %LOCALAPPDATA%、AppData\Local\Temp 等路径新增默认规则;处理 Windows 上只读文件的删除权限问题。
- 持久化排除列表:新增配置文件或环境变量,在 gather_candidates 中合并默认排除项,方便团队共享保护目录。
- 按类型清理:开放 --only <CATEGORY> 与 --skip <CATEGORY> 选项,或支持读取 JSON/YAML 规则,让用户精确控制清理范围。
- 结果报告增强:将删除结果输出为 JSON/CSV,或集成到日志系统,便于在 CI/CD 或企业环境中审计。
通过研读 DevStrip,Rust 初学者不仅能理解一个完整 CLI 工具的结构,也能快速积累文件系统操作、并发编程与用户交互方面的实战经验。动手尝试实现上述扩展,将进一步巩固你对 Rust 生态的理解。
我们会定期在群里分享最新的项目实战代码,包括不同语言的实现
老师还会详细讲解代码优化的思路,扫码加入实战群:
