1. 文件读写

1.1 读取文件的字符串行

  向文件写入三行信息,然后使用 BufRead::lines 创建的迭代器 Lines 读取文件,一次读回一行。File 模块实现了提供 BufReader 结构体的 Read trait。File::create 打开文件 File 进行写入,File::open 则进行读取。

use std::fs::File;
use std::io::{BufRead, BufReader, Error, Write};

fn main() -> Result<(), Error> {
let path = "lines.txt";

let mut output = File::create(path)?;
write!(output, "Rust\n💖\n呵呵")?;

let input = File::open(path)?;
let buffered = BufReader::new(input);

for line in buffered.lines() {
println!("{}", line?);
}

Ok(())
}
  • 运行cargo run输出
Rust
💖
呵呵

1.2 避免读取写入同一文件

  需要安装same-file库,可通过cargo add same-file 命令安装

[dependencies]
same-file = "1.0.6"

  对文件使用 same_file::Handle 结构体,可以测试文件句柄是否等同。在本实例中,将对要读取和写入的文件句柄进行相等性测试。

use same_file::Handle;
use std::fs::File;
use std::io::{BufRead, BufReader, Error, ErrorKind};
use std::path::Path;

fn main() -> Result<(), Error> {
let path_to_read = Path::new("new.txt");

let stdout_handle = Handle::stdout()?;
let handle = Handle::from_path(path_to_read)?;

if stdout_handle == handle {
return Err(Error::new(ErrorKind::Other, "您正在读取和写入同一个文件"));
} else {
let file = File::open(&path_to_read)?;
let file = BufReader::new(file);
for (num, line) in file.lines().enumerate() {
println!("{} : {}", num, line?.to_uppercase());
}
}

Ok(())
}
  • 运行cargo run输出显示文件 new.txt 的内容。

  • 运行cargo run >> ./new.txt报错,因为是同一文件。输出

Error: Custom { kind: Other, error: "您正在读取和写入同一个文件" }

1.3 使用内存映射随机访问文件

  需要安装memmap库,可通过cargo add memmap命令安装

[dependencies]
memmap = "0.7.0"

  使用 memmap 创建文件的内存映射,并模拟文件的一些非序列读取。使用内存映射意味着您仅需索引一个切片,而不是使用 seek 方法来导航整个文件。Mmap::map 函数假定内存映射后的文件没有被另一个进程同时更改,否则会出现竞态条件。

use memmap::Mmap;
use std::fs::File;
use std::io::{Write, Error};

fn main() -> Result<(), Error> {
write!(File::create("content.txt")?, "My hovercraft is full of eels!")?;

let file = File::open("content.txt")?;
let map = unsafe { Mmap::map(&file)? };

let random_indexes = [0, 1, 2, 19, 22, 10, 11, 29];
assert_eq!(&map[3..13], b"hovercraft");
let random_bytes: Vec<u8> = random_indexes.iter()
.map(|&idx| map[idx])
.collect();
assert_eq!(&random_bytes[..], b"My loaf!");
Ok(())
}
  • 运行cargo run校验

2 目录遍历

2.1 过去 24 小时内修改过的文件名

  需要安装error_chain库,可通过cargo add error_chain命令安装

[dependencies]
error-chain = "0.12.4"

  通过调用 env::current_dir 获取当前工作目录,然后通过 fs::read_dir 读取目录中的每个条目,通过 DirEntry::path 提取条目路径,以及通过通过 fs::Metadata 获取条目元数据。Metadata::modified 返回条目自上次更改以来的运行时间 SystemTime::elapsed。Duration::as_secs 将时间转换为秒,并与 24 小时(24 60 60 秒)进行比较。Metadata::is_file 用于筛选出目录。

use error_chain::error_chain;

use std::{env, fs};

error_chain! {
foreign_links {
Io(std::io::Error);
SystemTimeError(std::time::SystemTimeError);
}
}

fn main() -> Result<()> {
let current_dir = env::current_dir()?;
println!("在过去 24 小时内修改的条目{:?}:", current_dir);

for entry in fs::read_dir(current_dir)? {
let entry = entry?;
let path = entry.path();

let metadata = fs::metadata(&path)?;
let last_modified = metadata.modified()?.elapsed()?.as_secs();

if last_modified < 24 * 3600 && metadata.is_file() {
println!(
"最后修改:{:?} 秒,只读:{:?},大小:{:?} 字节,文件名:{:?}",
last_modified,
metadata.permissions().readonly(),
metadata.len(),
path.file_name().ok_or("没有文件名")?
);
}
}

Ok(())
}
  • 运行cargo run输出
在过去 24 小时内修改的条目"/root/rustcookbook/filetest":
最后修改:118 秒,只读:false,大小:237 字节,文件名:"Cargo.toml"
最后修改:107 秒,只读:false,大小:4336 字节,文件名:"Cargo.lock"

2.2 查找给定路径的循环

  使用 same_file::is_same_file 检测给定路径的循环。例如,可以通过软连接(符号链接)在 Unix 系统上创建循环:

mkdir -p /tmp/foo/bar/baz
ln -s /tmp/foo/ /tmp/foo/bar/baz/qux
  • 下面的实例将断言存在一个循环。
use same_file::is_same_file;
use std::io;
use std::path::{Path, PathBuf};

fn contains_loop<P: AsRef<Path>>(path: P) -> io::Result<Option<(PathBuf, PathBuf)>> {
let path = path.as_ref();
let mut path_buf = path.to_path_buf();
while path_buf.pop() {
if is_same_file(&path_buf, path)? {
return Ok(Some((path_buf, path.to_path_buf())));
} else if let Some(looped_paths) = contains_loop(&path_buf)? {
return Ok(Some(looped_paths));
}
}
return Ok(None);
}

fn main() {
assert_eq!(
contains_loop("/tmp/foo/bar/baz/qux/bar/baz").unwrap(),
Some((
PathBuf::from("/tmp/foo"),
PathBuf::from("/tmp/foo/bar/baz/qux")
))
);
}
  • 运行cargo run校验

2.3 递归查找重名文件

  需要安装walkdir库,可通过cargo add walkdir命令安装

[dependencies]
walkdir = "2.3.3"

  在当前目录中递归查找重复的文件名,只打印一次。

use std::collections::HashMap;
use walkdir::WalkDir;

fn main() {
let mut filenames = HashMap::new();

for entry in WalkDir::new(".")
.into_iter()
.filter_map(Result::ok)
.filter(|e| !e.file_type().is_dir())
{
let f_name = String::from(entry.file_name().to_string_lossy());
let counter = filenames.entry(f_name.clone()).or_insert(0);
*counter += 1;

if *counter == 2 {
println!("{}", f_name);
}
}
}
  • 运行cargo run输出(基本上是target目录下的文件)
invoked.timestamp
root-output
output
stderr
...
dep-lib-adler

2.4 使用给定断言递归查找所有文件

  在当前目录中查找最近一天内修改的 JSON 文件。使用 follow_links 确保软链接(符号链接)像普通目录和文件一样被按照当前查找规则执行。

use error_chain::error_chain;

use walkdir::WalkDir;

error_chain! {
foreign_links {
WalkDir(walkdir::Error);
Io(std::io::Error);
SystemTime(std::time::SystemTimeError);
}
}

fn main() -> Result<()> {
for entry in WalkDir::new(".")
.follow_links(true)
.into_iter()
.filter_map(|e| e.ok())
{
let f_name = entry.file_name().to_string_lossy();
let sec = entry.metadata()?.modified()?;

if f_name.ends_with(".json") && sec.elapsed()?.as_secs() < 86400 {
println!("{}", f_name);
}
}

Ok(())
}
  • 运行cargo run输出(基本上是target目录下的文件)
.rustc_info.json
lib-gimli.json
...
lib-adler.json
bin-filetest.json

2.5 跳过隐藏文件遍历目录

  递归下行到子目录的过程中,使用 filter_entry 对目录中的条目传递 is_not_hidden 断言,从而跳过隐藏的文件和目录。Iterator::filter 可应用到要检索的任何目录 WalkDir::DirEntry,即使父目录是隐藏目录。根目录 “.” 的检索结果,通过在断言 is_not_hidden 中使用 WalkDir::depth 参数生成。

use walkdir::{DirEntry, WalkDir};

fn is_not_hidden(entry: &DirEntry) -> bool {
entry
.file_name()
.to_str()
.map(|s| entry.depth() == 0 || !s.starts_with("."))
.unwrap_or(false)
}

fn main() {
WalkDir::new(".")
.into_iter()
.filter_entry(|e| is_not_hidden(e))
.filter_map(|v| v.ok())
.for_each(|x| println!("{}", x.path().display()));
}
  • 运行cargo run输出(基本上是target目录下的文件)
./Cargo.toml
./src
./src/main.rs
./target
./target/debug
./target/debug/build
...
./Cargo.lock

2.6 在给定深度的目录,递归计算文件大小

  通过WalkDir::min_depthWalkDir::max_depth 方法,可以灵活设置目录的递归深度。下面的实例计算了 3 层子文件夹深度的所有文件的大小总和,计算中忽略根文件夹中的文件。

use walkdir::WalkDir;

fn main() {
let total_size = WalkDir::new(".")
.min_depth(1)
.max_depth(3)
.into_iter()
.filter_map(|entry| entry.ok())
.filter_map(|entry| entry.metadata().ok())
.filter(|metadata| metadata.is_file())
.fold(0, |acc, m| acc + m.len());

println!("总大小: {} bytes.", total_size);
}
  • 运行cargo run输出
总大小: 5247240 bytes.

2.7 递归查找所有 png 文件

  需要安装glob"库,可通过cargo add glob命令安装

[dependencies]
glob = "0.3.1"

  递归地查找当前目录中的所有 PNG 文件。在本实例中,** 模式用于匹配当前目录及其所有子目录。在路径任意部分使用 ** 模式,例如,/media/**/*.png 匹配 media 及其子目录中的所有 PNG 文件。

use error_chain::error_chain;

use glob::glob;

error_chain! {
foreign_links {
Glob(glob::GlobError);
Pattern(glob::PatternError);
}
}

fn main() -> Result<()> {
for entry in glob("**/*.png")? {
println!("{}", entry?.display());
}

Ok(())
}
  • 运行cargo run输出
src/test.png
test.png

2.8 忽略文件名大小写,使用给定模式查找所有文件

  在当前目录中查找与正则表达模式 img_[0-9]*.png 匹配的所有图像文件。一个自定义 MatchOptions 结构体被传递给 glob_with 函数,使全局命令模式下不区分大小写,同时保持其他选项的默认值 Default,注:原教程查看/media/目录下的图片。

use error_chain::error_chain;
use glob::{glob_with, MatchOptions};

error_chain! {
foreign_links {
Glob(glob::GlobError);
Pattern(glob::PatternError);
}
}

fn main() -> Result<()> {
let options = MatchOptions {
case_sensitive: false,
..Default::default()
};

for entry in glob_with("./**/img_[0-9]*.png", options)? {
println!("{}", entry?.display());
}

Ok(())
}
  • 运行cargo run输出
img_0.png
src/img_9.png