Rust实战教程--文件管理命令行工具

关于本Rust实战教程系列:

  • 定位:一份从Rust基础迈向项目实战的练习记录。
  • 内容:聚焦实战中遇到的具体问题、解决思路与心得总结。
  • 读者 :适合有Rust基础,但急需项目经验巩固知识的开发者。资深玩家可忽略
  • 计划:本期为系列首篇,将根据我的学习进度持续更新。

简要说明

Rust命令行工具(CLI)用于查询当前文件夹下文件的功能,支持一些常用功能:查询、排序、过滤、统计分析等

需求拆解

  1. 列出当前文件夹下的所有文件和文件夹(不递归/递归可选)。
  2. 支持显示文件的详细信息(如大小、修改时间、类型等)。
  3. 支持通过参数过滤(如只显示某种类型的文件,或按名称过滤)。
  4. 支持排序(如按名称、大小、时间排序)。
  5. 支持输出格式(如表格、JSON、纯文本)。
  6. 支持显示隐藏文件(可选)。
  7. 支持统计信息(如文件总数、总大小等,可选)。

实现思路

  1. 使用clap库来解析命令行参数,支持递归、过滤、排序等选项;
  2. 使用std::fs::read_dir遍历当前目录;
  3. 获取文件元数据:std:fs::metadata,提取大小、修改时间、类型等;
  4. 根据参数对文件列表进行过滤和排序等;
  5. 格式化输出,可以用tabwriter库美化表格输出,或者serde_json输出JSON;
  6. 统计信息汇总展示,包含递归结果等;

温馨提示

本文的章节结构遵循我的实际开发流程。如果您在跟随实践时遇到报错或卡点,建议直接跳转至文末的「过程问题&&解决方案」章节。这里汇总了开发中可能遇到的各类问题,希望能助您快速排查,高效解决,继续迎接下一个挑战。

实现步骤

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,
}

有几个点来做些简单的解释:

  1. 参数根据使用场景返回不同的数据类型,如Option、String、Enum、Vec等,在实际使用中需要注意区分不同类型的处理方式和判空逻辑;
  2. short:声明命令行中对应参数的缩写 比如:fm -stats 缩写就是 fm -t
  3. value_enum:声明该参数为一个枚举类型,可以通过default_missing_value中的值应该是字符串,而不是枚举值;
  4. 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 LINECSV就是按照表格形式来输出,这里我们也是直接引入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的编译器是一个功能很强大的编译器,编译报错的时候都会给出相应的解决方案,所以看到编译报错,不要慌,细心地看一下报错内容,一般都会把答案写在里面

命令行结构体相关问题

  1. clap是什么,怎么用?

    答:Rust的命令行参数解析库,负责解析参数,生成 --help/--version、类型校验、错误提示、自动补全脚本等。

    目前使用derive模式,简单来说就是下面几步:

    • 安装依赖 :cargo add clap --features derive
    • 创建命令行结构体参考上面的实际代码
    • 在main.rs中引入该结构体,调用parse方法解析参数,最后得到一个命令行结构体实例,就可以进行后续的各种流程了...
  2. the trait bound &str: IntoResettable is not satisfied

    答:定义参数缩写时使用了双引号(&str), 而不是单引号(char), 两个类型没有办法自动转换, #[arg(short="d", ...)] --> #[arg(short='d', ...)]

  3. 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 之路崎岖,步步不停,所见皆风景。加油!!!

相关推荐
li理4 小时前
鸿蒙相机开发入门篇(官方实践版)
前端
webxin6664 小时前
页面动画和延迟加载动画的实现
前端·javascript
逛逛GitHub4 小时前
这个牛逼的股票市场平台,在 GitHub 上开源了。
前端·github
细节控菜鸡4 小时前
【排查实录】Web 页面能打开,服务器能通接口,客户端却访问失败?原因全在这!
运维·服务器·前端
用户5277137976194 小时前
银河麒麟部署自托管Sentry流程及问题记录(暂未成功)
全栈
今天头发还在吗4 小时前
React + Ant Design 日期选择器避免显示“Invalid Date“的解决方案
前端·react.js·前端框架·ant design
时雨__5 小时前
利用AndVX6开发流程图——问题总结
前端
云枫晖5 小时前
深入浅出npm:现代JavaScript项目基石
前端·javascript·node.js