关于本Rust实战教程系列:
- 定位:一份从Rust基础迈向项目实战的练习记录。
- 内容:聚焦实战中遇到的具体问题、解决思路与心得总结。
- 读者 :适合有Rust基础,但急需项目经验巩固知识的开发者。资深玩家可忽略。
- 计划:本期为系列首篇,将根据我的学习进度持续更新。
简要说明
Rust命令行工具(CLI)用于查询当前文件夹下文件的功能,支持一些常用功能:查询、排序、过滤、统计分析等
需求拆解
- 列出当前文件夹下的所有文件和文件夹(不递归/递归可选)。
- 支持显示文件的详细信息(如大小、修改时间、类型等)。
- 支持通过参数过滤(如只显示某种类型的文件,或按名称过滤)。
- 支持排序(如按名称、大小、时间排序)。
- 支持输出格式(如表格、JSON、纯文本)。
- 支持显示隐藏文件(可选)。
- 支持统计信息(如文件总数、总大小等,可选)。
实现思路
- 使用clap库来解析命令行参数,支持递归、过滤、排序等选项;
- 使用std::fs::read_dir遍历当前目录;
- 获取文件元数据:std:fs::metadata,提取大小、修改时间、类型等;
- 根据参数对文件列表进行过滤和排序等;
- 格式化输出,可以用tabwriter库美化表格输出,或者serde_json输出JSON;
- 统计信息汇总展示,包含递归结果等;
温馨提示
本文的章节结构遵循我的实际开发流程。如果您在跟随实践时遇到报错或卡点,建议直接跳转至文末的「过程问题&&解决方案」章节。这里汇总了开发中可能遇到的各类问题,希望能助您快速排查,高效解决,继续迎接下一个挑战。
实现步骤
1.命令行如何使用?
之所以将此作为第一个模块,是因为需求拆解仅能得到孤立的功能点,而功能点如何转化为代码结构,中间还缺失了关键一环:用户使用场景。明确用户如何与软件交互,才能将抽象的功能转化为具体的操作流程。这一理解是后续开发工作的基石,它直接决定了我们如何设计代码架构与模块
js
// 默认用法
fm
// 查询当前文件夹下文件信息 支持正则表达式进行匹配
fm main.rs
// 正则表达式
fm -r ".rs"
fm -r "^main.*\\.rs$"
// 是否递归查询目录下的文件 支持层级配置
fm main.rs -R
// 三层递归查询
fm main.rs -d 3
// 是否排序 支持配置 不同排序字段:name 、size 、time 、type 等
fm -s
fm -s name
fm -s name size type
// 是否展示统计信息 name size time type等
fm -t
fm -t name size time
fm -t type
// 是否展示隐藏文件
fm -a
// 支持输出格式配置 csv json 默认为csv格式
fm -o
fm -o json
// 支持配置查询目录 默认是当前目录
fm -r ".rs" -w /home/user/work
2.代码实现逻辑
基于我们上面的使用场景以及实现思路,我们现在第一步的话 就来实现clap库需要的命令行的结构体Args:
结构体Args
rust
// src/args.rs
//
use std::path::PathBuf;
use clap::{Parser,ArgAction, ValueEnum, ValueHint};
/// A simple file manager written in Rust
#[derive(Parser, Debug)]
#[command(name="fm", author, version, about, long_about = None)]
pub struct Args {
/// 位置参数:按名字包含匹配(可选)
/// 示例: fm main.rs
#[arg(value_name = "NAME")]
pub name: Option<String>,
/// 正则匹配 (可选)
/// 示例: fm -r ".rs" 或者 fm -r "^main.*\\.rs$"
#[arg(short='r', long="regex", value_name = "REGEX")]
pub regex: Option<String>,
/// 支持展示隐藏的文件 (可选)
/// 示例: fm -a
#[arg(short='a', long="all", action=ArgAction::SetTrue)]
pub all: bool,
/// 是否递归查询(可选)
// ArgAction::SetTrue 表示:当该选项出现在命令行里时,把对应的布尔字段设置为 true(不出现则保持默认,通常为 false)。适用于"开关型"无值参数。
/// 示例: fm -r ".rs"
#[arg(short='R', long="recursive", action=ArgAction::SetTrue)]
pub recursive: bool,
/// 递归层级 (可选)
/// 示例: fm -r ".rs" -d 2
#[arg(short='d', long="depth", num_args= 0.. , default_missing_value = "10", value_name = "DEPTH")]
pub depth: Option<u8>,
/// 排序字段 (可选, 支持多值: name,size,time,type)
/// 示例:
/// fm -r ".rs" -s ==> 默认值为 name 等同于 fm -r ".rs" -s name
/// fm -r ".rs" -s ===> 按 size 进行排序
/// fm -r ".rs" -s time size name ==> 依次按 time,size,name 进行排序
#[arg(short='s', long="sort", value_enum, num_args = 0.. , default_missing_value = "name", value_name = "SORT_FIELD")]
pub sort: Vec<SortField>,
/// 显示统计信息(可选,支持多值,仅出现 -t 不写值时,默认为all)
/// 示例:
/// fm -r ".rs" -t ==> 等同于 fm -r ".rs" -t all
/// fm -r ".rs" -t all ==> 显示所有统计
/// fm -r ".rs" -t size time type ==> 显示大小,时间,类型统计
/// fm -r ".rs" -t size ==> 仅显示总大小统计
/// fm -r ".rs" -t type ==> 仅显示类型统计
#[arg(short='t', long="stats", value_enum, num_args = 0.. , default_missing_value = "all", value_name = "STATS_FIELD")]
pub stats: Vec<StatsField>,
/// 输出格式配置 (可选): csv, json 默认为 csv
/// 示例: fm -r ".rs" -o json
/// fm -r ".rs" -o csv
#[arg(short='o', long="output", value_enum, num_args = 0.., default_missing_value = "csv", value_name = "FORMAT")]
pub output: Option<OutputFormat>,
/// 工作目录 (可选)
// 示例: fm -r ".rs" -w /home/user/work
#[arg(short='w', long="workdir", value_hint=ValueHint::DirPath, value_name = "WORKDIR")]
pub workdir: Option<PathBuf>,
}
#[derive(Clone, Debug, Copy, ValueEnum)]
pub enum SortField {
Name,
Size,
Time,
Type,
}
#[derive(Clone, Debug, Copy, ValueEnum)]
pub enum OutputFormat {
Csv,
Json,
}
#[derive(Clone, Debug, Copy, ValueEnum)]
pub enum StatsField {
All,
Name,
Time,
Size,
Type,
}
有几个点来做些简单的解释:
- 参数根据使用场景返回不同的数据类型,如Option、String、Enum、Vec等,在实际使用中需要注意区分不同类型的处理方式和判空逻辑;
- short:声明命令行中对应参数的缩写 比如:
fm -stats
缩写就是fm -t
- value_enum:声明该参数为一个枚举类型,可以通过default_missing_value中的值应该是字符串,而不是枚举值;
- num_args:声明每个出现该参数时接收多少个值 ,常见取值:
0..
:表示可以接受0个或更多值(出现但可不跟值)0..=1
:表示可接受0或者1个值1..
:表示至少1个值(1个或多个)3
:表示必须正好3个值1..=3
:表示1到3个值
在main.rs文件中引入结构体模块,然后执行parse命令,然后执行cargo run -- name
就可以得到一个默认的命令参数解析后的结构体实例了
rust
// src/main.rs
// fm - 文件管理器
// A simple file manager written in Rust
mod args;
use args::Args;
// 这一行必须加 否则无法直接调用parse方法
use clap::Parser;
fn main() {
let args_opts = Args::parse();
/*
打印:Args { name: Some("name"), regex: None, all: false, recursive: false, depth: None, sort: [], stats: [], output: None, workdir: None }
*/
println!("{:?}", args_opts);
}
上面我们已经定义好了用户输入的命令行结构体,并且在main函数中也已经拿到了相关的结构体参数,下面的话我们来跟据参数一步步实现不同的逻辑:
文件辅助函数
其实我们细想一下这个功能最核心的一个点:拿到对应目录下的文件列表,这是我们的核心能力,然后我们在根据上面不同的参数逻辑来进行过滤,排序等操作,最后在进行不同格式的输出;所以我们可以首先定义一个查询文件列表的函数,这里的话我们单独定义一个文件处理的模块,用来处理所有跟文件相关的逻辑:
rust
// src/utils/file_help.rs
use std::path::{Path,PathBuf};
use crate::args::{Args}
// 定义文件的元数据 struct
#[derive(Debug, Clone)]
pub struct FileMetaData {
name: String,
path: PathBuf,
size: u64,
time: String,
is_dir: bool,
file_type: String,
}
// 获取当前的工作目录
pub fn get_current_dir() -> Result<PathBuf,std::io::Error>{
std::env::current_dir()
}
// 查询当前文件夹下面的文件列表函数,入参 是命令行参数,出参是文件原始数据列表
pub fn get_file_list(args: &Args)-> Result<Vec<FileMetaData>, std::io::Error> {
let mut file_list = Vec::new();
// 首先要判断是当前的目录 还是说命令行中传入了对应的工作目录
let cwd = get_current_dir()?;
// as_ref() 是因为 workdir 是 Option<PathBuf>,需要转换为 Option<&PathBuf> 以便后续使用
let workdir = args.workdir.as_ref().unwrap_or(&cwd);
// 遍历workdir目录下的文件列表
for entry in std::fs::read_dir(workdir)? {
/**
这个函数里面其实就是我们的核心逻辑
*/
}
ok(file_list)
}
上面其实已经把我们的大体的框架搭好了,下面就是一点点的来实现我们的逻辑
是否隐藏文件 -a
这里我们统一把开头为
.
的文件定义为隐藏文件
rust
// 其实我们只需要拿到文件名 判断文件名开头是不是`.`即可
...
for entry in std::fs::read_dir(workdir)? {
let entry = entry?;// entry 类型是`io::Result<DirEntry>` 使用`?`解包成DirEntry类型
// entry.file_name() 返回OsString(跨平台的文件名类型,不一定是UTF_8)
// to_string_lossy() 把OsString 尝试按照UTF-8对待,返回Cow<str>
// to_string() 把Cow<str>拿成拥有所有权额String,
let name = entry.file_name().to_string_lossy().to_string();
// 判断是否隐藏.开头文件
if name.starts_with(".") && !args.all{
continue;
}
}
...
是否递归查询 -R
只需要判断当前entry是否为目录,如果是目录 并且命令行有-R的话 那就进行递归查询(递归调用get_file_list方法)
rust
//
...
for entry in std::fs::read_dir(workdir)? {
let entry = entry?;
// 获取当前文件原始信息
let metadata = enrty.metadata()?;
let name = entry.file_name().to_string_lossy().to_string();
// 判断是否为目录
let is_dir = metadata.is_dir();
let args.recursive && is_dir {
// 判断递归的层级,如果有的话 但是0 就不进行递归查询了 做一个异常处理
if args.depth.is_some() && args.depth.unwrap() == 0 {
continue;
}
let sub_args = Args {
depth: args.depth.map(|d| if d>0 {d-1} else {0}, // 递归层级要逐渐减少 避免死循环
workdir: Some(entry.path()), // 查询工作目录要调整成当前需要查询的目录路径
...args.clone()
}
// 同时为了区分name 递归时增加父级路径
let p_name = if let Some(ref p) = p_name{
Some(p.clone() + "/" + &name)
}else {
Some(name.clone())
}
// 这里给 get_file_list一个额外的参数 用于拼接父级的路径名称
let sub_file_list = get_file_list(&sub_args,p_name)?;
file_list.extend(sub_file_list);
}
}
...
名称匹配 name && 正则匹配 -r ".rs"
对于名称匹配的话,就是看下文件名称中是否包含name
正则匹配的话,直接安装regex
包来进行正则匹配
rust
for entry in std::fs::read_dir(workdir)? {
let entry = entry?;// entry 类型是`io::Result<DirEntry>` 使用`?`解包成DirEntry类型
// entry.file_name() 返回OsString(跨平台的文件名类型,不一定是UTF_8)
// to_string_lossy() 把OsString 尝试按照UTF-8对待,返回Cow<str>
// to_string() 把Cow<str>拿成拥有所有权额String,
let name = entry.file_name().to_string_lossy().to_string();
// 是否包含name
if let Some(ref needle) = args.name {
if !name.contains(needle) { continue; }
}
// 正则表达式匹配
if let Some(ref regex_str) = args.regex {
let re = Regex::new(regex_str).unwrap();
if !re.is_match(&name){
continue;
}
}
}
上面这几个不同的参数的逻辑我们已经实现了,此时我们也已经拿到了最终的file_list,剩下的几个参数其实都是我们实际输出的参数配置了,首先我们来看下排序字段:
排序字段 -s
默认按照name
来进行排序,如果传参就按照参数来进行依次排序:
rust
...
for entry in std::fs::read_dir(workdir)?{
// 上面所有逻辑
...
}
...
// 对遍历得到的file_list进行排序
if !args.sort.is_empty() {
// 会从最后一个排序字段开始遍历,为了实现`多关键字排序` 应先按低优先级排序,再按高优先级排序,比如: -s time size name 此时用.rev() 先按name,再按size,最后按time,得到期望结果。
for sort_field in args.sort.iter().rev(){
match sort_field {
crate::args::SortField::Name => {
file_list.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
}
crate::args::SortField::Size => {
file_list.sort_by(|a, b| a.size.cmp(&b.size));
}
crate::args::SortField::Time => {
file_list.sort_by(|a, b| a.time.cmp(&b.time));
}
crate::args::SortField::Type => {
file_list.sort_by(|a, b| a.file_type.to_lowercase().cmp(&b.file_type.to_lowercase()));
}
}
}
}
Ok(file_list)
输出格式 -o && 输出统计信息 -t
目前我们定义了两种输出格式:CSV
和 LINE
,CSV
就是按照表格形式来输出,这里我们也是直接引入prettytable
包即可LINE
就是直接按行输出,直接使用println!
即可;
对于输出统计信息,就可以输出时遍历统计信息的值,分别进行拼接就可以了;
rust
// 依然是在 file_help.rs文件中定义两种不同输出函数,入参都是file_list,
pub fn print_file_list_table(file_list: &[FileMetaData], stats_fields: &[crate::args::StatsField]){
// 引入相对应的包内容
use prettytable::{Table, Row, Cell};
// 默认输出 Name 和 Is Directory
let mut columns = vec![Cell::new("Name"), Cell::new("Is Directory")];
// 设置表头
stats_fields.iter().for_each(|field| {
match field {
crate::args::StatsField::Size => columns.push(Cell::new("Size")),
crate::args::StatsField::Time => columns.push(Cell::new("Modified Time")),
crate::args::StatsField::Type => columns.push(Cell::new("Type")),
crate::args::StatsField::All => {
columns.push(Cell::new("Path"));
columns.push(Cell::new("Size"));
columns.push(Cell::new("Modified Time"));
columns.push(Cell::new("Type"));
}
_ => {}
}
});
let mut table = Table::new();
table.add_row(Row::new(columns));
// 设置表格的内容
for file in file_list {
let mut columns = vec![Cell::new(&file.name), Cell::new(&file.is_dir.to_string())];
stats_fields.iter().for_each(|field| {
match field {
crate::args::StatsField::Size => columns.push(Cell::new(&file.size.to_string())),
crate::args::StatsField::Time => columns.push(Cell::new(&file.time)),
crate::args::StatsField::Type => columns.push(Cell::new(&file.file_type)),
crate::args::StatsField::All => {
columns.push(Cell::new(&file.path.to_string_lossy()));
columns.push(Cell::new(&file.size.to_string()));
columns.push(Cell::new(&file.time));
columns.push(Cell::new(&file.file_type));
}
_ => {}
}
});
table.add_row(Row::new(columns));
}
table.printstd();
}
// 使用行形式 打印信息
pub fn print_file_list_row(file_list: &[FileMetaData], stats_fields: &[crate::args::StatsField]) {
stats_fields.iter().for_each(|field| {
match field {
crate::args::StatsField::Name => print!("{}\n", "name"),
crate::args::StatsField::Size => println!("{}\\{}\n ", "Name", "Size"),
crate::args::StatsField::Time => println!("{}\\{}\n ", "Name", "Time"),
crate::args::StatsField::Type => println!("{}\\{}\n ", "Name", "Type"),
crate::args::StatsField::All => {
// 使用` \` 跟多层级路径的`/`区分
println!("{}\\{}\\{}\\{}\n ", "Name", "Size", "Modified Time", "Type");
}
_ => {}
}
});
for file in file_list {
stats_fields.iter().for_each(|field| {
match field {
crate::args::StatsField::Name => print!("{}\n", file.name),
crate::args::StatsField::Size => println!("{}\\{}\n ", file.name, file.size),
crate::args::StatsField::Time => println!("{}\\{}\n ", file.name, file.time),
crate::args::StatsField::Type => println!("{}\\{}\n ", file.name, file.file_type),
crate::args::StatsField::All => {
println!("{}\\{}\\{}\\{}\n ", file.name, file.size, file.time, file.file_type);
}
_ => {println!("\n "); }
}
});
}
}
main.rs
中调用逻辑
上面我们已经把整体的核心功能实现了,现在我们要在工程入口进行实际的调用,把我们的功能跑通,其实也是比较简单的,因为我们已经定义好了方法,在main.rs中直接引用,然后调用对应的方法就可以了:
rust
// fm - 文件管理器
// A simple file manager written in Rust
mod args;
use args::Args;
// 这一行必须加 否则无法直接调用parse方法
use clap::Parser;
mod utils;
// 直接引入函数(因为在 mod.rs 里 pub use 了)
use utils::{get_file_list, print_file_list_table, print_file_list_row};
fn main() {
let args_opts = Args::parse();
let file_list = get_file_list(&args_opts, None).unwrap();
// 打印参数 和格式化输出结果
let stats_fields = &args_opts.stats;
// 设置输出的格式
let output_format = args_opts.output.unwrap();
if let args::OutputFormat::Csv = output_format {
print_file_list_table(&file_list, stats_fields);
} else {
// 拼接字符串 按行输出
print_file_list_row(&file_list, stats_fields);
}
// println!("{:?}\n当前目录为{:#?}", args_opts, file_list);
}
完整代码
目录结构
rust
├── fm
├── src
| ├── utils
| | |------ file_help.rs // 核心处理逻辑
| | └── mod.rs // 模块导出文件
| └── args.rs // 命令行结构体
| └── main.rs // 入口文件
├── Cargo.toml // 项目配置文件
file_help.rs
rust
use std::path::{Path,PathBuf};
use crate::args::{Args};
use chrono::{DateTime, Local};
use std::time::SystemTime;
use regex::Regex;
/// 获取当前的工作目录
pub fn get_current_dir() -> Result<PathBuf, std::io::Error> {
std::env::current_dir()
}
fn fmt_system_time(system_time: SystemTime) -> String {
let datetime: DateTime<Local> = system_time.into();
datetime.format("%Y-%m-%d %H:%M:%S").to_string()
}
// 定义文件的元数据 struct
#[derive(Debug, Clone)]
pub struct FileMetaData {
name: String,
path: PathBuf,
size: u64,
time: String,
is_dir: bool,
file_type: String,
}
// 查询当前目录下的文件和文件夹列表
pub fn get_file_list(args: &Args, p_name: Option<String>) -> Result<Vec<FileMetaData>, std::io::Error> {
let mut file_list = Vec::new();
// 判断当前的args中 是否有工作目录,如果没有则使用当前工作目录
// as_ref() 是因为 workdir 是 Option<PathBuf>,需要转换为 Option<&PathBuf> 以便后续使用
let cwd = get_current_dir()?;
let workdir = args.workdir.as_ref().unwrap_or(&cwd);
for entry in std::fs::read_dir(workdir)? {
let entry = entry?;
let metadata = entry.metadata()?;
let name = entry.file_name().to_string_lossy().to_string();
let is_dir = metadata.is_dir();
// 判断是否隐藏.开头文件
if name.starts_with(".") && !args.all {
continue;
}
// 递归查询x层级
if args.recursive && is_dir {
if args.depth.is_some() && args.depth.unwrap() == 0 {
continue;
}
// 这里可以添加递归查询的逻辑,目前仅支持一层
let sub_args = Args {
depth: args.depth.map(|d| if d > 0 { d - 1 } else { 0 }), // 递减层级
workdir: Some(entry.path()),
..args.clone()
};
let p_name = if let Some(ref p) = p_name {
Some(p.clone() + "/" + &name)
} else {
Some(name.clone())
};
let sub_file_list = get_file_list(&sub_args, p_name)?;
file_list.extend(sub_file_list);
}
// 判断一下 如果不包含 name 则跳过
if let Some(ref needle) = args.name {
if !name.contains(needle) {
continue;
}
}
// 正则表达式匹配
if let Some(ref regex_str) = args.regex {
let re = Regex::new(regex_str).unwrap();
if !re.is_match(&name) {
continue;
}
}
let show_name = if let Some(ref p_name) = p_name {
p_name.to_string() + "/" + name.as_str()
} else {
name.to_string()
};
file_list.push(FileMetaData{
name: show_name,
path: entry.path(),
size: metadata.len(),
time: metadata.modified().map(fmt_system_time).unwrap_or_else(|_| "-".to_string()),
is_dir,
file_type: get_file_extension(&entry.path(), is_dir).unwrap_or_else(|| "-".to_string()),
});
}
// 排序字段
if !args.sort.is_empty() {
for sort_field in args.sort.iter().rev() {
match sort_field {
crate::args::SortField::Name => {
file_list.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
}
crate::args::SortField::Size => {
file_list.sort_by(|a, b| a.size.cmp(&b.size));
}
crate::args::SortField::Time => {
file_list.sort_by(|a, b| a.time.cmp(&b.time));
}
crate::args::SortField::Type => {
file_list.sort_by(|a, b| a.file_type.to_lowercase().cmp(&b.file_type.to_lowercase()));
}
}
}
}
Ok(file_list)
}
// 获取当前文件的后缀名
pub fn get_file_extension(path: &Path, is_dir: bool) -> Option<String> {
if is_dir {
return None;
}
path.extension().and_then(|ext| ext.to_str().map(|s| s.to_string()))
}
// 使用表格形式打印信息
pub fn print_file_list_table(file_list: &[FileMetaData], stats_fields: &[crate::args::StatsField]) {
use prettytable::{Table, Row, Cell};
let mut columns = vec![Cell::new("Name"), Cell::new("Is Directory")];
stats_fields.iter().for_each(|field| {
match field {
crate::args::StatsField::Size => columns.push(Cell::new("Size")),
crate::args::StatsField::Time => columns.push(Cell::new("Modified Time")),
crate::args::StatsField::Type => columns.push(Cell::new("Type")),
crate::args::StatsField::All => {
columns.push(Cell::new("Path"));
columns.push(Cell::new("Size"));
columns.push(Cell::new("Modified Time"));
columns.push(Cell::new("Type"));
}
_ => {}
}
});
let mut table = Table::new();
table.add_row(Row::new(columns));
for file in file_list {
let mut columns = vec![Cell::new(&file.name), Cell::new(&file.is_dir.to_string())];
stats_fields.iter().for_each(|field| {
match field {
crate::args::StatsField::Size => columns.push(Cell::new(&file.size.to_string())),
crate::args::StatsField::Time => columns.push(Cell::new(&file.time)),
crate::args::StatsField::Type => columns.push(Cell::new(&file.file_type)),
crate::args::StatsField::All => {
columns.push(Cell::new(&file.path.to_string_lossy()));
columns.push(Cell::new(&file.size.to_string()));
columns.push(Cell::new(&file.time));
columns.push(Cell::new(&file.file_type));
}
_ => {}
}
});
table.add_row(Row::new(columns));
}
table.printstd();
}
// 使用行形式 打印信息
pub fn print_file_list_row(file_list: &[FileMetaData], stats_fields: &[crate::args::StatsField]) {
stats_fields.iter().for_each(|field| {
match field {
crate::args::StatsField::Name => print!("{}\n", "name"),
crate::args::StatsField::Size => println!("{}\\{}\n ", "Name", "Size"),
crate::args::StatsField::Time => println!("{}\\{}\n ", "Name", "Time"),
crate::args::StatsField::Type => println!("{}\\{}\n ", "Name", "Type"),
crate::args::StatsField::All => {
println!("{}\\{}\\{}\\{}\n ", "Name", "Size", "Modified Time", "Type");
}
_ => {}
}
});
for file in file_list {
stats_fields.iter().for_each(|field| {
match field {
crate::args::StatsField::Name => print!("{}\n", file.name),
crate::args::StatsField::Size => println!("{}\\{}\n ", file.name, file.size),
crate::args::StatsField::Time => println!("{}\\{}\n ", file.name, file.time),
crate::args::StatsField::Type => println!("{}\\{}\n ", file.name, file.file_type),
crate::args::StatsField::All => {
println!("{}\\{}\\{}\\{}\n ", file.name, file.size, file.time, file.file_type);
}
_ => {println!("\n "); }
}
});
}
}
mod.rs
rust
pub mod file_help;
// 直接重导出,方便在顶层 utils 下使用
pub use file_help::get_current_dir;
// 导出查询文件下内容函数
pub use file_help::{get_file_list, print_file_list_table, print_file_list_row};
args.rs
rust
//
use std::path::PathBuf;
use clap::{Parser,ArgAction, ValueEnum, ValueHint};
/// A simple file manager written in Rust
#[derive(Parser, Debug, Clone)] // <- 增加 Clone
#[command(name="fm", author, version, about, long_about = None)]
pub struct Args {
/// 位置参数:按名字包含匹配(可选)
/// 示例: fm main.rs
#[arg(value_name = "NAME")]
pub name: Option<String>,
/// 正则匹配 (可选)
/// 示例: fm -r ".rs" 或者 fm -r "^main.*\\.rs$"
#[arg(short='r', long="regex", value_name = "REGEX")]
pub regex: Option<String>,
/// 支持展示隐藏的文件 (可选)
/// 示例: fm -a
#[arg(short='a', long="all", action=ArgAction::SetTrue)]
pub all: bool,
/// 是否递归查询(可选)
// ArgAction::SetTrue 表示:当该选项出现在命令行里时,把对应的布尔字段设置为 true(不出现则保持默认,通常为 false)。适用于"开关型"无值参数。
/// 示例: fm -r ".rs"
#[arg(short='R', long="recursive", action=ArgAction::SetTrue)]
pub recursive: bool,
/// 递归层级 (可选)
/// 示例: fm -r ".rs" -d 2
#[arg(short='d', long="depth", num_args= 0.. , default_missing_value = "10", value_name = "DEPTH")]
pub depth: Option<u8>,
/// 排序字段 (可选, 支持多值: name,size,time,type)
/// 示例:
/// fm -r ".rs" -s ==> 默认值为 name 等同于 fm -r ".rs" -s name
/// fm -r ".rs" -s ===> 按 size 进行排序
/// fm -r ".rs" -s time size name ==> 依次按 time,size,name 进行排序
#[arg(short='s', long="sort", value_enum, num_args = 0.. , default_missing_value = "name", value_name = "SORT_FIELD")]
pub sort: Vec<SortField>,
/// 显示统计信息(可选,支持多值,仅出现 -t 不写值时,默认为all)
/// 示例:
/// fm -r ".rs" -t ==> 等同于 fm -r ".rs" -t all
/// fm -r ".rs" -t all ==> 显示所有统计
/// fm -r ".rs" -t size time type ==> 显示大小,时间,类型统计
/// fm -r ".rs" -t size ==> 仅显示总大小统计
/// fm -r ".rs" -t type ==> 仅显示类型统计
#[arg(short='t', long="stats", value_enum, num_args = 0.. , default_missing_value = "all", default_value = "name", value_name = "STATS_FIELD")]
pub stats: Vec<StatsField>,
/// 输出格式配置 (可选): csv, 默认为 csv
/// 示例: fm -r ".rs" -o json
/// fm -r ".rs" -o csv
#[arg(short='o', long="output", value_enum, num_args = 0.., default_missing_value = "csv", value_name = "FORMAT")]
pub output: Option<OutputFormat>,
/// 工作目录 (可选)
// 示例: fm -r ".rs" -w /home/user/work
#[arg(short='w', long="workdir", value_hint=ValueHint::DirPath, value_name = "WORKDIR")]
pub workdir: Option<PathBuf>,
}
#[derive(Clone, Debug, Copy, ValueEnum)]
pub enum SortField {
Name,
Size,
Time,
Type,
}
#[derive(Clone, Debug, Copy, ValueEnum)]
pub enum OutputFormat {
Csv,
Line,
}
#[derive(Clone, Debug, Copy, ValueEnum)]
pub enum StatsField {
All,
Name,
Time,
Size,
Type,
}
main.rs
rust
// fm - 文件管理器
// A simple file manager written in Rust
mod args;
use args::Args;
// 这一行必须加 否则无法直接调用parse方法
use clap::Parser;
mod utils;
// 直接引入函数(因为在 mod.rs 里 pub use 了)
use utils::{get_file_list, print_file_list_table, print_file_list_row};
fn main() {
let args_opts = Args::parse();
let file_list = get_file_list(&args_opts, None).unwrap();
// 打印参数 和格式化输出结果
let stats_fields = &args_opts.stats;
// 设置输出的格式
let output_format = args_opts.output.unwrap();
if let args::OutputFormat::Csv = output_format {
print_file_list_table(&file_list, stats_fields);
} else {
// 拼接字符串 按行输出
print_file_list_row(&file_list, stats_fields);
}
// println!("{:?}\n当前目录为{:#?}", args_opts, file_list);
}
Cargo.toml
toml
[package]
name = "fm"
version = "0.1.0"
edition = "2024"
[dependencies]
chrono = "0.4.42"
clap = { version = "4.5.40", features = ["derive"] }
comfy-table = "7.2.1"
prettytable = "0.10.0"
regex = "1.12.2"
功能演示
既然开发了一个很完整的命令行工具,那肯定是要用起来的,所以这里提供了Mac和Windows两种不同的安装方式供您选择:
Mac
rust
// 使用brew来进行安装
brew tap MaAmos/tap
brew fm
Windows
直接去当前连接下载可执行exe文件即可:github.com/MaAmos/fm/r...
效果演示
js
// 当前是在fm文件夹下进行效果演示
-- fm
+------------+--------------+
| Name | Is Directory |
+------------+--------------+
| Cargo.toml | false |
+------------+--------------+
| src | true |
+------------+--------------+
-- fm -R
+------------------------+--------------+
| Name | Is Directory |
+------------------------+--------------+
| Cargo.toml | false |
+------------------------+--------------+
| src/utils/mod.rs | false |
+------------------------+--------------+
| src/utils/file_help.rs | false |
+------------------------+--------------+
| src/utils | true |
+------------------------+--------------+
| src/main.rs | false |
+------------------------+--------------+
| src/args.rs | false |
+------------------------+--------------+
| src | true |
+------------------------+--------------+
// 其他的命令 欢迎大家直接安装体验~~~
过程问题&&解决方案
Rust的编译器是一个功能很强大的编译器,编译报错的时候都会给出相应的解决方案,所以看到编译报错,不要慌,细心地看一下报错内容,一般都会把答案写在里面
命令行结构体相关问题
-
clap是什么,怎么用?
答:Rust的命令行参数解析库,负责解析参数,生成 --help/--version、类型校验、错误提示、自动补全脚本等。
目前使用derive模式,简单来说就是下面几步:
- 安装依赖
:cargo add clap --features derive
- 创建命令行结构体 :参考上面的实际代码
- 在main.rs中引入该结构体,调用parse方法解析参数,最后得到一个命令行结构体实例,就可以进行后续的各种流程了...
- 安装依赖
-
the trait bound
&str: IntoResettableis not satisfied
答:定义参数缩写时使用了双引号(
&str
), 而不是单引号(char
), 两个类型没有办法自动转换,#[arg(short="d", ...)]
-->#[arg(short='d', ...)]
-
the method
value_parserexists for reference
&&&&&&_infer_ValueParser_for, but its trait bounds were not
satisfied答:定义结构体参数时使用了clap自带的 value_num类型来标记参数为枚举值类型,但是自己定义的枚举值
StatsField
并没有实现clap对应的trait特征ValueEnum
rust
use clap::{ValueEnum};
...
#[derive(Clone, Debug, Copy, ValueEnum)]
pub enum StatsField {
All,
Name,
Time,
Size,
Type,
}
写在最后
纵然 Rust 之路崎岖,步步不停,所见皆风景。加油!!!