Rust综合实践——模拟cmd的文件操作

Rust综合实践——模拟cmd的文件操作

这是我用Rust写的第一个项目:实现一个cmd,可以从标准输入中读取命令,命令包括:

  • ls:列出当前目录的文件列表
  • touch: 创建指定名称的文件
  • cat:显示指定文件的内容
  • mkdir: 创建指定名称的目录
  • cp:复制文件
  • mv:重命名文件或目录
  • rm: 删除文件或目录
  • exit:退出

当然,不可能完全实现上面linux命令的全部功能,只是为了学习文件操作,实现最基本的功能。

框架搭建

使用cargo创建项目:

cargo new cmd

将上述命令定义为枚举:

// 定义命令枚举
enum Command {
    Ls,
    Touch(String),
    Cat(String),
    Mkdir(String),
    Cp(String, String),
    Mv(String, String),
    Rm(String)
}

为枚举实现公有的execute方法和其他私有方法:

// 定义命令枚举
enum Command {
    Ls,
    Touch(String),
    Cat(String),
    Mkdir(String),
    Cp(String, String),
    Mv(String, String),
    Rm(String)
}

// 为枚举实现方法
impl Command {
    // 执行命令
    pub fn execute(&self) {
        match self {
            Command::Ls => Command::ls(),
            Command::Touch(path) => Command::touch(path.to_string()),
            Command::Cat(path) => Command::cat(path.to_string()),
            Command::Mkdir(path) => Command::mkdir(path.to_string()),
            Command::Cp(from, to) => Command::cp(from.to_string(), to.to_string()),
            Command::Mv(from, to) => Command::mv(from.to_string(), to.to_string()),
            Command::Rm(path) => Command::rm(path.to_string())
        }
    }

    // 列出当前目录的文件列表
    fn ls() {}

    // 创建指定名称的文件
    fn touch(path: String) {}

    // 显示指定文件的内容
    fn cat(path: String) {}

    // 创建指定名称的目录
    fn mkdir(path: String) {}

    // 复制文件或目录
    fn cp(from: String, to: String) {}

    // 移动文件或目录
    fn mv(from: String, to: String) {}

    // 删除文件或目录
    fn rm(path: String) {}
}

主函数中循环读取标准输入,通过输入创建不同的枚举对像并执行命令:

use std::io;

// 将输入的命令解析为枚举
fn get_command(cmd: String) -> Option<Command> {
    let mut inputs: Vec<&str> = cmd.split(' ').collect();   // 以空格分隔

    // 为了避免取值时发生panic,在此将命令以空字符串填充
    for _ in 3-inputs.len()..3 {
        inputs.push("");
    }

    // 根据不同的命令,构造不同的枚举
    match inputs[0] {
        "ls" => Some(Command::Ls),
        "touch" => Some(Command::Touch(inputs[1].to_string())),
        "cat" => Some(Command::Cat(inputs[1].to_string())),
        "mkdir" => Some(Command::Mkdir(inputs[1].to_string())),
        "cp" => Some(Command::Cp(inputs[1].to_string(), inputs[2].to_string())),
        "mv" => Some(Command::Mv(inputs[1].to_string(), inputs[2].to_string())),
        "rm" => Some(Command::Rm(inputs[1].to_string())),
        _ => None
    }
}

// 主函数
fn main() {
    loop {
        print!("> ");
        io::stdout().flush().unwrap();      // 不添加此操作,提示符不能显示
        
        let mut input = String::new();
        io::stdin().read_line(&mut input).unwrap();

        let cmd = input.trim();
        if cmd == "exit" {
            break;
        }

        // 判断命令的有效性
        match get_command(cmd.to_string()) {
            Some(cmd) => cmd.execute(),
            None => println!("未知命令")
        } 
    }
}

至此,这个项目的框架部分搭建完成了。剩下的工作就是填充枚举中的每一个功能方法。

ls

读取目录下的子目录和文件,有两个方式:

使用std::fs模块的read_dir方法:

std::fs

使用std::path::Path模块的read_dir方法

let path = Path::new(".");
path.read_dir()

这两个方法都返回Result<ReadDir, Error>。ReadDir,实现了迭代器特性,可以直接使用for循环遍历:

let path = Path::new(".");

// 读取目录信息
match path.read_dir() {
	Ok(direntrys) => {
    	// 遍历目录
        for item in direntrys {
        }
    },
    Err(e) => {}
}

遍历得到的是Result<DirEntry, Error>。DirEntry具有如下方法:

  • file_name():取得文件名
let name = String::from(entry.file_name().to_str().unwrap_or(""));
  • file_type():取得文件类型
if let Ok(ft) = entry.file_type() {
    if ft.is_dir() {
        print!("目录\t\t");
    } else if ft.is_file() {
        print!("文件\t\t");
    } else if ft.is_symlink() {
        print!("符号链接\t\t");
    } else {
        print!("未知\t\t");
    }
} else {
    print!("未知\t");
}
  • metadata():取得文件元数据
if let Ok(metadata) = entry.metadata() {}

metadata()返回Result<Metadata, Error>。Metadata包含如下方法:

  • file_type():与DirEntry的file_type功能相同
  • is_dir():是否为目录
  • is_file():是否为文件
  • len():取得文件大小,单位为bytes
  • permissions():取得文件权限,目前,仅有是否有只读一项权限
  • modified():取得文件的最后修改时间
  • accessed():取得文件的最后访问时间
  • created():取得文件的创建时间

created()、accessed()、modified()返回的是SystemTime结构,这种结构并不容易转换为字符串。好在,有一个第三方库:chrono,使用它可以方便的对时间类型进行操作:

extern crate chrono; 
use chrono::offset::Local; 	// 如果要用UTC时间,可以把Local换成Utc
use chrono::DateTime;

let ct = metadata.created().unwrap_or(SystemTime::now());
let dt: DateTime<Local> = ct.into(); 
print!("{}\t", dt.format("%Y-%m-%d %T"));

有了上面这些基础,ls方法的代码就呼之欲出了:

fn ls() { 
    let path = Path::new(".");
    if path.is_dir() {
        // 读取目录信息
        match path.read_dir() {
            Ok(de) => {
                // 打印表头
                println!("文件名\t\t文件类型\t权限\t大小\t创建时间\t\t访问时间\t\t修改时间");

                // 遍历目录
                for item in de {
                    match item {
                        Ok(entry) => {
                            // 取得实体属性
                            match entry.metadata() {
                                Ok(metadata) => {
                                    // 文件名
                                    let name = String::from(entry.file_name().to_str().unwrap_or(""));
                                    print!("{}", name);
                                    match name.len() {
                                        0..=9 => print!("\t\t"),
                                        9..=20 => print!("\t"),
                                        _ => print!("\t")
                                    }                             

                                    // 文件类型
                                    if let Ok(ft) = entry.file_type() {
                                        if ft.is_dir() {
                                            print!("目录\t\t");
                                        } else if ft.is_file() {
                                            print!("文件\t\t");
                                        } else if ft.is_symlink() {
                                            print!("符号链接\t\t");
                                        } else {
                                            print!("未知\t\t");
                                        }
                                    } else {
                                        print!("未知\t");
                                    }

                                    // 权限
                                    let p = metadata.permissions();
                                    if p.readonly() {
                                        print!("只读\t", );
                                    } else {
                                        print!("读写\t", );
                                    }                                        

                                    // 大小
                                    print!("{:?}\t", metadata.len());

                                    // 创建时间
                                    let ct = metadata.created().unwrap_or(SystemTime::now());
                                    let dt: DateTime<Local> = ct.into(); 
                                    print!("{}\t", dt.format("%Y-%m-%d %T"));

                                    // 最后访问时间
                                    let ct = metadata.accessed().unwrap_or(SystemTime::now());
                                    let dt: DateTime<Local> = ct.into(); 
                                    print!("{}\t", dt.format("%Y-%m-%d %T"));

                                    // 最后修改时间
                                    let ct = metadata.modified().unwrap_or(SystemTime::now());
                                    let dt: DateTime<Local> = ct.into(); 
                                    print!("{}\t", dt.format("%Y-%m-%d %T"));

                                    // 结束
                                    println!("");
                                },
                                Err(e) => {
                                    println!("取得元数据失败: {:?}", e);
                                },
                            }
                        },
                        Err(e) => {
                            println!("取得文件实体失败: {:?}", e);
                        },
                    }
                }
            },
            Err(e) => {
                println!("读取目录失败: {:?}", e);
            }
        };

    } else {
        println!("{:?} 不是目录", path);
    }
}

touch

Rust的创建文件的方式与其他语言有些许不同。大部分语言受C的影响,创建文件与打开文件是合并在一起的,通过参数控制当文件不存在时是否创建。而Rust打开文件和创建文件是分开的。

Rust的文件操作封装在std::fs::File库中,创建文件可以调用create方法。create方法接收的参数可以是Path对象,也可以是字符串。它返回Result<File, Error>枚举。

touch方法的代码如下:

fn touch(path: String) {
    // 检查文件路径是否输入
    if path.len() == 0 {
        println!("请输入要创建的文件路径");
        return;
    }

    // 转换为路径对象
    let p = Path::new(&path);

    // 检查文件是否存在
    if p.is_file() || p.is_dir() {
        println!("文件已经存在");
        return;
    }

    match File::create(p) {
        Ok(_) => println!("创建文件成功: {:?}", p),
        Err(e) => println!("创建文件失败: {:?}", e)
    }
}

create方法并不管文件是否存在,都会进行创建,如果文件已经存在时,会将原文件清空。所以,需要自行判断是否真正需要创建。

cat

打开文件需要调用std::fs::File::open方法,open方法与create方法类似,接收的参数可以是Path对象,也可以是字符串,返回Result<File, Error>枚举。File是文件对象,默认的,open以只读方式打开文件,如果需要以追加等其他方式打开,需要与std::fs::OpenOptions配合。创建OpenOptions可以通过两种方式:

  • File::with_options()
  • OpenOptions::new()

这两种方式是等价的。OpenOptions可以通过以下方法来设置不同的打开方式:

  • read(bool):打开文件是否具有读取权限
  • write(bool):打开文件是否具有写入权限
  • append(bool):是否以追加方式打开文件
  • truncate(bool):打开文件后是否截断文件,即是否清空文件
  • create(bool):当文件不存在时是否创建文件
  • create_new(bool):当文件不存在时创建文件,文件存在时返回失败

OpenOptions支持链式表达,比如,当需要以只写且追加的方式打开文件,文件不存在时创建新文件,可以这么写:

OpenOptions::new().read(false).write(true).append(true).create(true).open("test.txt");

默认的open返回的File对象没有读取文件的方法,想要读取文件,要引用std::io::Read:

use std::io::Read;

相应的,如果要写入文件,需要引入std::io::Write。引入后,就可以使用一系列read方法读取文件内容:

  • read:读取文件内容到缓冲区(数组),返回读取的大小,读取的大小最大不超过缓冲区的大小
  • read_vectored:与read功能相同,不同在于read_vectored支持缓冲区的切片
  • read_exact:读取文件内容到缓冲区(数组),返回读取的大小为缓冲区大小,如果读取的内容不足缓冲区大小,则返回错误
  • read_to_end:读取文件所有内容到缓冲区(数组),当缓冲区大小不足时返回错误
  • read_to_string:前面的都是读取到缓冲区中,此方法是将内容读取到String中

根据这些知识,cat方法的代码如下:

fn cat(path: String) {
    // 检查文件路径是否输入
    if path.len() == 0 {
        println!("请输入要显示的文件路径");
        return;
    }

    match File::open(path) {
        Ok(mut f) => {
            let mut content = String::new();
            match f.read_to_string(&mut content) {
                Ok(_) => println!("{}", content),
                Err(e) => println!("读取文件失败:{}", e)
            }
        },
        Err(e) => println!("打开文件出错:{}", e)
    };
}

功能是实现了,这种方法有一定的弊端,它需要将文件的内容一次性读取到内存中,这无疑是浪费内存空间的,期望的做法是一次只读取一行,直到读取完毕。要实现这样的效果,需要使用std::io::BufReader,同时,引入std::io::prelude:😗:

use std::io::prelude::*;
use std::io::BufReader;

注意,没有引用std::io::prelude::*时,BufReader对象是没有read_line方法的,只有两者同时引入才可以。优化后的代码如下:

fn cat(path: String) {
        // 检查文件路径是否输入
        if path.len() == 0 {
            println!("请输入要显示的文件路径");
            return;
        }

        match File::open(path) {
            Ok(f) => {
                let mut buf = BufReader::new(f);
                let mut content = String::new();
                while let Ok(size) = buf.read_line(&mut content) {
                    if size > 0 {
                        print!("{}", content);
                        content.clear();        // 每次需要将content清空,否则读取的内容会追加到content结尾
                    } else {
                        println!("");
                        break
                    }
                }
            },
            Err(e) => println!("打开文件出错:{}", e)
        };
    }

mkdir

Rust创建目录的方式比较简单,fs库中有两个方法可以调用:

  • create_dir:创建最后一级目录,若前面的路径不存在则返回错误
  • create_dir_all:递归的创建每一级目录
fn mkdir(path: String) {
    // 检查路径是否输入
    if path.len() == 0 {
        println!("请输入要创建的目录路径");
        return;
    }

    // 练习使用unwrap_or_else进行错误处理
    fs::create_dir_all(path).unwrap_or_else(|e| println!("创建目录失败:{}", e));
}

这里使用了unwrap_or_else进行错误处理,unwrap_or_else在Ok时返回Ok内部的值,当Err时调用闭包。

cp

复制文件也很简单,调用fs::copy方法就可以了:

fn cp(from: String, to: String) {
    // 检查路径是否输入
    if from.len() == 0 {
        println!("请输入要复制的文件路径");
        return;
    }

    if to.len() == 0 {
        println!("请输入目标文件路径");
        return;
    }

    fs::copy(from, to).unwrap_or_else(|e| { println!("创建目录失败:{}", e); 0});
}

这里的错误处理与创建目录的略有不同,create_dir_all本身是没有返回值的,所以闭包也不需要返回,而copy会返回复制的字节数(u64),所以闭包也需要返回u64。

mv

fs::rename方法可以实现重命名操作,无论是文件还是目录,都可以用它来完成:

fn mv(from: String, to: String) {
    // 检查路径是否输入
    if from.len() == 0 {
        println!("请输入要复制的文件路径");
        return;
    }

    if to.len() == 0 {
        println!("请输入目标文件路径");
        return;
    }

    fs::rename(from, to).unwrap_or_else(|e| println!("文件改名失败:{}", e));
}

不过这与mv命令不同,它不能实现移动。

rm

删除操作要区分文件还是目录了,fs库提供了三个方法:

  • remove_dir:删除空目录
  • remove_dir_all:删除目录及其子目录和文件
  • remove_file:删除文件
fn rm(path: String) {
    // 检查路径是否输入
    if path.len() == 0 {
        println!("请输入要删除和文件或目录路径");
        return;
    }

    let p = Path::new(&path);
    if p.is_dir() {
        fs::remove_dir_all(p).unwrap_or_else(|e| println!("删除目录失败:{}", e));
    } else if p.is_file() {
        fs::remove_file(p).unwrap_or_else(|e| println!("删除文件失败:{}", e));
    }
}
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值