< 返回我的博客

MaAmos 发表于 2025-10-20 16:56

Tags:实战课程,命令行工具

关于本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.命令行如何使用?

之所以将此作为第一个模块,是因为需求拆解仅能得到孤立的功能点,而功能点如何转化为代码结构,中间还缺失了关键一环:用户使用场景。明确用户如何与软件交互,才能将抽象的功能转化为具体的操作流程。这一理解是后续开发工作的基石,它直接决定了我们如何设计代码架构与模块

// 默认用法
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

// 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 就可以得到一个默认的命令参数解析后的结构体实例了

// 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函数中也已经拿到了相关的结构体参数,下面的话我们来跟据参数一步步实现不同的逻辑:

文件辅助函数

其实我们细想一下这个功能最核心的一个点:拿到对应目录下的文件列表,这是我们的核心能力,然后我们在根据上面不同的参数逻辑来进行过滤,排序等操作,最后在进行不同格式的输出;所以我们可以首先定义一个查询文件列表的函数,这里的话我们单独定义一个文件处理的模块,用来处理所有跟文件相关的逻辑:

// 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

这里我们统一把开头为.的文件定义为隐藏文件

// 其实我们只需要拿到文件名 判断文件名开头是不是`.`即可
...
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方法)

// 
...
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包来进行正则匹配

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来进行排序,如果传参就按照参数来进行依次排序:

...
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!即可;
对于输出统计信息,就可以输出时遍历统计信息的值,分别进行拼接就可以了;

// 依然是在 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中直接引用,然后调用对应的方法就可以了:


// 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);
}

完整代码

目录结构

├── fm
    ├── src
    |    ├── utils
    |    |    |—— file_help.rs // 核心处理逻辑
    |    |    └── mod.rs // 模块导出文件
    |    └── args.rs // 命令行结构体
    |    └── main.rs // 入口文件
    ├── Cargo.toml // 项目配置文件

file_help.rs

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

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

//
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

// 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

[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

// 使用brew来进行安装
brew tap MaAmos/tap
brew fm

Windows

直接去当前连接下载可执行exe文件即可:https://github.com/MaAmos/fm/releases/tag/v1.0.1

效果演示

// 当前是在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

  use clap::{ValueEnum};
  ...
  #[derive(Clone, Debug, Copy, ValueEnum)] 
  pub enum StatsField {
      All,
      Name,
      Time,
      Size,
      Type,
  }

写在最后

纵然 Rust 之路崎岖,步步不停,所见皆风景。加油!!!

评论区

写评论

还没有评论

1 共 0 条评论, 1 页