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